1. 源起
最近在用做一個WPF項目,其中有個界面需要在DataGrid單元格中綁定的TextBox中輸入數據,並需要將數據傳播回數據源。這里的xaml代碼是這樣的:
<DataGrid Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding DataList}" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False" > <DataGrid.Columns> <DataGridTextColumn Header="序號" Width="60" Binding="{Binding Id}"/> <DataGridTextColumn Header="名稱" Width="*" Binding="{Binding Name}"/> <DataGridTemplateColumn Header="別名" Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <Grid> <TextBox Width="100" Text="{Binding AliasName}"/> </Grid> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid>
別名這一列用的是DataGridTemplateColumn,DataTemplate中用了一個TextBox綁定了數據源中的AliasName屬性。
ViewModel是這樣的:
public class MainWindowViewModel : PropertyChangedBase { private List<NotifyModel> _dataList=new List<NotifyModel>(); public List<NotifyModel> DataList { get => _dataList; set => SetValue(ref _dataList, value, nameof(DataList)); } public void InitDataList() { for (int i = 0; i < 10; i++) { var c = (char) (65 + i); DataList.Add(new NotifyModel { Id = i+1, Name = $"{c}{c}{c}", }); } } }
PropertyChangeBase類為自定義:
/// <summary> /// 用於通知屬性變更的基類 /// </summary> public class PropertyChangedBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public void SetValue<T>(ref T field, T value, string propertyName) { if (EqualityComparer<T>.Default.Equals(field, value)) return; field = value; OnPropertyChanged(propertyName); } }
NotifyModel:
public class NotifyModel { public int Id { get; set; } public string Name { get; set; } public string AliasName { get; set; } }
到了這里,我滿以為問題就解決了,但是為了保險起見,我還是開了個測試項目,准備測試一下這么干有沒有問題(還沒有在列表中這么用過^_^)。
2. 意料之外的結果

如上,我在第一行的“別名”框中輸入了一行字符,按照設想,點擊Test按鈕后,變更應該已經反饋到數據源了,即:_view.DataList[0].AliasName的值應該是“change notify”。為了驗證,我在按鈕的click事件中查看了數據源的AliasName屬性值,結果卻大出意料:

TextBox中的內容並沒有傳播到數據源。第一反應是TextBox的Text屬性進行綁定時,Mode應該設置為TwoWay,但是在MSDN中關於Binding.Mode屬性很清楚的說明了,控件的可編輯屬性,默認為雙向綁定:
One of the BindingMode values. The default is Default, which returns the default binding mode value of the target dependency property. However, the default value varies for each dependency property. In general, user-editable control properties, such as those of text boxes and check boxes, default to two-way bindings, whereas most other properties default to one-way bindings.
Binding.Mode取BindMode的其中一個值。默認值為Default,該值返回目標依賴屬性默認的BindingMode值。但是,對不同的依賴屬性,其默認值也不一樣。通常來說,控件的可編輯屬性(如TextBox/CheckBox),默認為雙向綁定(TwoWay),其他屬性默認為單向綁定(OneWay)。
抱着萬一的想法(^_^),手動設置一下Text屬性的BindingMode試試:
<TextBox Width="100" Text="{Binding AliasName,Mode=TwoWay}"/>

問題依舊……
3.對比
是代碼有問題嗎?哪里寫的不對,導致界面上的變更傳播不到數據源?為了對比,在界面上添加了一個新的綁定屬性:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="60"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Grid.Row="0" HorizontalAlignment="Right" Orientation="Horizontal"> <TextBlock Text="模塊名稱" VerticalAlignment="Center" Margin="5"/> <TextBox Text="{Binding ModuleName,Mode=TwoWay}" Width="150" Height="30" VerticalAlignment="Center" TextAlignment="Left" VerticalContentAlignment="Center"/> <Button Content="Test" Width="100" Height="30" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="10,0" Click="Test_OnClick"/> </StackPanel> <DataGrid Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding DataList}" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False" > <DataGrid.Columns> <DataGridTextColumn Header="序號" Width="60" Binding="{Binding Id}"/> <DataGridTextColumn Header="名稱" Width="*" Binding="{Binding Name}"/> <DataGridTemplateColumn Header="別名" Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <Grid> <TextBox Width="100" Text="{Binding AliasName,Mode=TwoWay}"/> </Grid> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> </Grid>
在界面上新增加了一個“模塊名稱”的項目,該項目同樣是一個TextBox。在ViewModel中新增一個ModuleName屬性作為其數據源:
private string _moduleName = ""; public string ModuleName { get => _moduleName; set => SetValue(ref _moduleName, value, nameof(ModuleName)); }
在Test按鈕的Click事件中同樣查看_view.ModuleName:

斷點查看:

這時我們發現,ModuleName框的變更通知到數據源了。看來我們的ViewModel沒有問題,可能和數據源被綁定到DataGrid有關,歸根結底,應該是和數據綁定方式有關。
4. 解決過程
個人感覺,這里最簡單的解決方案,還是從綁定上着手,查看MSDN文檔中的Binding類,發現了它有個UpdateSourceTrigger屬性,文檔中對它的介紹是:
Gets or sets a value that determines the timing of binding source updates.
獲取或設置一個值,該值確定綁定源的更新時機。
MSDN上對它的說明如下:
One of the UpdateSourceTrigger values. The default is Default, which returns the default UpdateSourceTrigger value of the target dependency property. However, the default value for most dependency properties is PropertyChanged, while the Text property has a default value of LostFocus.
UpdateSourceTrigger屬性取UpdateSourceTrigger枚舉的值之一。默認值為Default,該值返回目標依賴屬性的默認UpdateSourceTrigger值。但是,對於大多數依賴屬性來說,默認值為PropertyChanged,而Text屬性的默認值為LostFocus。
也就是說,我們這里綁定的TextBox,其綁定的默認UpdateSourceTrigger值應該是LostFocus,回顧一下對ModuleName的綁定,也證實了這一點:當設置完“模塊名稱”框后,點擊Test按鈕,按鈕獲取焦點,而TextBox失去焦點,這時,將變更傳播到_view.ModuleName。但是,為什么DataGrid中的別名列就是不行呢?@_@……
要不,顯式設置一下這個試試?
<TextBox Width="100" Text="{Binding AliasName,Mode=TwoWay,UpdateSourceTrigger=LostFocus}"/>

竟!然!!成!!!了!!!!
可是,為什么呢……@_@……
再看看和BindingMode有沒有關系,去掉Text綁定中的Mode:
<TextBox Width="100" Text="{Binding AliasName,UpdateSourceTrigger=LostFocus}"/>

數據也能傳播回數據源,看來這里可以不設置(MSDN中關於Text順序的綁定模式,默認為TwoWay的說法看來沒問題 ≧◇≦)。
5. 源碼
6. 后記
寫到這里,雖然問題解決了,但是還是疑惑,為什么呢?希望各位大神給解惑!
