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. 后记
写到这里,虽然问题解决了,但是还是疑惑,为什么呢?希望各位大神给解惑!