如何通知 WPF DataGrid 中的一行其单元格之一已以编程方式修改?
How do I notify a row in a WPF DataGrid that one of its cells was programmatically modified?
我正在为我的 WPF 项目创建 DataGridRadioButtonColumn
。这是它的样子:
public class DataGridRadioButtonColumn : DataGridBoundColumn
{
private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();
public string Group { get; set; }
public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("DefaultGroup"));
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// Only generate the buttons once, to preserve the Group.
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
return radioButton;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
// Use the same one we generated before.
return _buttons[cell];
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
var radioButton = editingElement as RadioButton;
if (radioButton == null) return null;
return radioButton.IsChecked;
}
}
下面是它的使用示例:
<local:DataGridRadioButtonColumn
Width="0.33*"
Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Group="Group1"
Header="PRI" />
一切都如我所愿,包括单击最初选择的单选按钮 "unchecks"。这种取消检查通过组依赖性 属性.
起作用
我遇到的唯一问题是未选中的单选按钮没有在网格中注册为行编辑。只有我点击的单选按钮注册了一个行编辑,为了正确保存数据两行(包含我点击的单选按钮的那一行,以及包含未选中的单选按钮)必须保存。
如何告知单选按钮未选中的数据行已被编辑,以便它正确更新 DataGrid 绑定集合中的匹配元组?
请注意,我在每一行中使用的模型中实现了 IEditableObject
接口,因此它不像仅依赖 INotifyPropertyChanged
那样简单。它确实需要在DataGrid 行中触发一个BeginEdit()
。我不认为单选按钮的编程清除无论如何都会触发 PropertyChanged
,因为更改没有反映在底层模型对象中。
根据要求,这是一个 MCVE(或更好的部分):
APP.XAML
<Application x:Class="WpfApp11.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp11"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
MainWindow.XAML
<Window
x:Class="WpfApp11.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp11"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<DataGrid ItemsSource="{Binding Items, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
<DataGridTextColumn
Width="0.7*"
Binding="{Binding Path=Name, Mode=TwoWay}"
Header="Name" />
<local:DataGridRadioButtonColumn
Width="0.3*"
Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Group="Group1"
Header="PRI" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
型号
using PropertyChanged; //Fody
namespace WpfApp11
{
[AddINotifyPropertyChangedInterface] // PropertyChanged.Fody
public class Model
{
public string Name { get; set; }
public bool? PrimaryChecked
{
get;
set;
}
}
}
视图模型
using System.Collections.ObjectModel;
using PropertyChanged; // Fody
namespace WpfApp11
{
[AddINotifyPropertyChangedInterface] // PropertyChanged.Fody
public class ViewModel
{
public ViewModel()
{
Items = new ObservableCollection<Model>
{
new Model {Name = "George"},
new Model {Name = "Fred"},
new Model {Name = "Tom"},
};
}
public ObservableCollection<Model> Items { get; set; }
}
}
如您所见,这段代码并不起眼。
这里是有趣的地方。让我们在模型上放置一个 IEditableObject
实现。 IEditableObject
被DataGrid识别;它允许您为每个数据行提供更改跟踪和撤消功能等功能:
public class Model : EditableValidatableObject<Model>
{
public string Name { get; set; }
public bool? PrimaryChecked
{
get;
set;
}
}
EditableValidatableObject.cs
using PropertyChanged;
using System;
using System.ComponentModel;
namespace WpfApp11
{
/// <summary>
/// Provides an implementation of the IEditableObject and INotifyDataErrorInfo interfaces for data transfer objects.
/// </summary><remarks>
/// The IEditableObject interface is typically used to capture the BeginEdit, EndEdit, and CancelEdit semantics of a DataRowView.
/// Making something an IEditableObject enables full editing and undo capabilities in a DataGrid.
///
/// The INotifyDataErrorInfo implementation uses Validation Attributes to validate the values of properties on the DTO.
/// This information is used to indicate that a value entered by the user is invalid.
///
/// See T_Asset.cs and T_TestPoint.cs for usage examples.
/// </remarks>
[AddINotifyPropertyChangedInterface]
public abstract class EditableValidatableObject<T> : AnnotationValidationViewModel, IEditableObject
{
/// <summary>
/// Constructor, sets up the INotifyDataErrorInfo implementation.
/// </summary>
private T Cache { get; set; }
private object CurrentModel { get { return this; } }
public RelayCommand CancelEditCommand
{
get { return new RelayCommand(CancelEdit); }
}
private bool IsDirty
{
get
{
if (Cache == null) return false;
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite)
continue;
var oldValue = info.GetValue(Cache, null);
var currentValue = info.GetValue(CurrentModel, null);
if (oldValue == null && currentValue != null)
return true;
if (oldValue != null && !oldValue.Equals(currentValue))
return true;
}
return false;
}
}
#region IEditableObject Implementation
public bool Added { get; set; }
public bool Edited { get; set; }
public bool Deleted { get; set; }
public void BeginEdit()
{
Cache = Activator.CreateInstance<T>();
var type = CurrentModel.GetType();
//Set Properties of Cache
foreach (var info in type.GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(CurrentModel, null);
Cache.GetType().GetProperty(info.Name).SetValue(Cache, oldValue, null);
}
if (!Added && !Deleted && IsDirty)
{
Edited = true;
}
}
public virtual void EndEdit()
{
if (!Added && !Deleted && IsDirty)
{
Edited = true;
}
Cache = default(T);
}
public void CancelEdit()
{
if (Cache == null) return;
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(Cache, null);
CurrentModel.GetType().GetProperty(info.Name).SetValue(CurrentModel, oldValue, null);
}
}
#endregion
}
}
AnnotationValidationViewModel
平平无奇;它只是 INotifyDataErrorInfo
的一个实现,它使用数据注释进行验证。
上面 IEditableObject
实现的关键部分是 BeginEdit()
方法,数据网格行使用该方法向基础模型发出已发生编辑的信号。单击单选按钮时会调用此方法,但不会在自动取消选中其他单选按钮时调用。
因为 BeginEdit()
永远不会在未选中的行上调用,所以 Edited 属性 永远不会被设置。我依靠 Edited 属性 来知道我需要将哪些记录保存回数据库。
经过深思熟虑,我决定对我的 DataGridRadioButtonColumn
实现进行一些更改。现在看起来像这样:
public class DataGridRadioButtonColumn : DataGridBoundColumn
{
private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();
private Dictionary<RadioButton, dynamic> _models = new Dictionary<RadioButton, dynamic>();
public string Group { get; set; }
public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("Group1"));
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
radioButton.Unchecked += RadioButton_Unchecked;
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
_models.Add(radioButton, dataItem);
return radioButton;
}
private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
{
var button = sender as RadioButton;
dynamic model = _models[button];
try
{
model.Edited = true;
}
catch { }
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return _buttons[cell];
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
var radioButton = editingElement as RadioButton;
if (radioButton == null) return null;
return radioButton.IsChecked;
}
}
这是它的工作原理。我添加了一个字典来捕获每个单选按钮的模型,并且在创建单选按钮时,我将模型添加到新字典并挂钩单选按钮上的 Unchecked
事件:
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
radioButton.Unchecked += RadioButton_Unchecked; // Added
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
_models.Add(radioButton, dataItem); // Added
return radioButton;
}
然后,我只是在事件触发时在模型中设置 Edited
属性:
private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
{
var button = sender as RadioButton;
dynamic model = _models[button];
try
{
// Notify IEditableObject implementation, if it exists.
model.Edited = true;
}
catch { }
}
我正在为我的 WPF 项目创建 DataGridRadioButtonColumn
。这是它的样子:
public class DataGridRadioButtonColumn : DataGridBoundColumn
{
private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();
public string Group { get; set; }
public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("DefaultGroup"));
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
// Only generate the buttons once, to preserve the Group.
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
return radioButton;
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
// Use the same one we generated before.
return _buttons[cell];
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
var radioButton = editingElement as RadioButton;
if (radioButton == null) return null;
return radioButton.IsChecked;
}
}
下面是它的使用示例:
<local:DataGridRadioButtonColumn
Width="0.33*"
Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Group="Group1"
Header="PRI" />
一切都如我所愿,包括单击最初选择的单选按钮 "unchecks"。这种取消检查通过组依赖性 属性.
起作用我遇到的唯一问题是未选中的单选按钮没有在网格中注册为行编辑。只有我点击的单选按钮注册了一个行编辑,为了正确保存数据两行(包含我点击的单选按钮的那一行,以及包含未选中的单选按钮)必须保存。
如何告知单选按钮未选中的数据行已被编辑,以便它正确更新 DataGrid 绑定集合中的匹配元组?
请注意,我在每一行中使用的模型中实现了 IEditableObject
接口,因此它不像仅依赖 INotifyPropertyChanged
那样简单。它确实需要在DataGrid 行中触发一个BeginEdit()
。我不认为单选按钮的编程清除无论如何都会触发 PropertyChanged
,因为更改没有反映在底层模型对象中。
根据要求,这是一个 MCVE(或更好的部分):
APP.XAML
<Application x:Class="WpfApp11.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp11"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
MainWindow.XAML
<Window
x:Class="WpfApp11.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp11"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<DataGrid ItemsSource="{Binding Items, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
<DataGridTextColumn
Width="0.7*"
Binding="{Binding Path=Name, Mode=TwoWay}"
Header="Name" />
<local:DataGridRadioButtonColumn
Width="0.3*"
Binding="{Binding PrimaryChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Group="Group1"
Header="PRI" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
型号
using PropertyChanged; //Fody
namespace WpfApp11
{
[AddINotifyPropertyChangedInterface] // PropertyChanged.Fody
public class Model
{
public string Name { get; set; }
public bool? PrimaryChecked
{
get;
set;
}
}
}
视图模型
using System.Collections.ObjectModel;
using PropertyChanged; // Fody
namespace WpfApp11
{
[AddINotifyPropertyChangedInterface] // PropertyChanged.Fody
public class ViewModel
{
public ViewModel()
{
Items = new ObservableCollection<Model>
{
new Model {Name = "George"},
new Model {Name = "Fred"},
new Model {Name = "Tom"},
};
}
public ObservableCollection<Model> Items { get; set; }
}
}
如您所见,这段代码并不起眼。
这里是有趣的地方。让我们在模型上放置一个 IEditableObject
实现。 IEditableObject
被DataGrid识别;它允许您为每个数据行提供更改跟踪和撤消功能等功能:
public class Model : EditableValidatableObject<Model>
{
public string Name { get; set; }
public bool? PrimaryChecked
{
get;
set;
}
}
EditableValidatableObject.cs
using PropertyChanged;
using System;
using System.ComponentModel;
namespace WpfApp11
{
/// <summary>
/// Provides an implementation of the IEditableObject and INotifyDataErrorInfo interfaces for data transfer objects.
/// </summary><remarks>
/// The IEditableObject interface is typically used to capture the BeginEdit, EndEdit, and CancelEdit semantics of a DataRowView.
/// Making something an IEditableObject enables full editing and undo capabilities in a DataGrid.
///
/// The INotifyDataErrorInfo implementation uses Validation Attributes to validate the values of properties on the DTO.
/// This information is used to indicate that a value entered by the user is invalid.
///
/// See T_Asset.cs and T_TestPoint.cs for usage examples.
/// </remarks>
[AddINotifyPropertyChangedInterface]
public abstract class EditableValidatableObject<T> : AnnotationValidationViewModel, IEditableObject
{
/// <summary>
/// Constructor, sets up the INotifyDataErrorInfo implementation.
/// </summary>
private T Cache { get; set; }
private object CurrentModel { get { return this; } }
public RelayCommand CancelEditCommand
{
get { return new RelayCommand(CancelEdit); }
}
private bool IsDirty
{
get
{
if (Cache == null) return false;
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite)
continue;
var oldValue = info.GetValue(Cache, null);
var currentValue = info.GetValue(CurrentModel, null);
if (oldValue == null && currentValue != null)
return true;
if (oldValue != null && !oldValue.Equals(currentValue))
return true;
}
return false;
}
}
#region IEditableObject Implementation
public bool Added { get; set; }
public bool Edited { get; set; }
public bool Deleted { get; set; }
public void BeginEdit()
{
Cache = Activator.CreateInstance<T>();
var type = CurrentModel.GetType();
//Set Properties of Cache
foreach (var info in type.GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(CurrentModel, null);
Cache.GetType().GetProperty(info.Name).SetValue(Cache, oldValue, null);
}
if (!Added && !Deleted && IsDirty)
{
Edited = true;
}
}
public virtual void EndEdit()
{
if (!Added && !Deleted && IsDirty)
{
Edited = true;
}
Cache = default(T);
}
public void CancelEdit()
{
if (Cache == null) return;
foreach (var info in CurrentModel.GetType().GetProperties())
{
if (!info.CanRead || !info.CanWrite) continue;
var oldValue = info.GetValue(Cache, null);
CurrentModel.GetType().GetProperty(info.Name).SetValue(CurrentModel, oldValue, null);
}
}
#endregion
}
}
AnnotationValidationViewModel
平平无奇;它只是 INotifyDataErrorInfo
的一个实现,它使用数据注释进行验证。
上面 IEditableObject
实现的关键部分是 BeginEdit()
方法,数据网格行使用该方法向基础模型发出已发生编辑的信号。单击单选按钮时会调用此方法,但不会在自动取消选中其他单选按钮时调用。
因为 BeginEdit()
永远不会在未选中的行上调用,所以 Edited 属性 永远不会被设置。我依靠 Edited 属性 来知道我需要将哪些记录保存回数据库。
经过深思熟虑,我决定对我的 DataGridRadioButtonColumn
实现进行一些更改。现在看起来像这样:
public class DataGridRadioButtonColumn : DataGridBoundColumn
{
private Dictionary<DataGridCell, RadioButton> _buttons = new Dictionary<DataGridCell, RadioButton>();
private Dictionary<RadioButton, dynamic> _models = new Dictionary<RadioButton, dynamic>();
public string Group { get; set; }
public static readonly DependencyProperty GroupProperty = RadioButton.GroupNameProperty.AddOwner(
typeof(DataGridRadioButtonColumn), new FrameworkPropertyMetadata("Group1"));
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
radioButton.Unchecked += RadioButton_Unchecked;
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
_models.Add(radioButton, dataItem);
return radioButton;
}
private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
{
var button = sender as RadioButton;
dynamic model = _models[button];
try
{
model.Edited = true;
}
catch { }
}
protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return _buttons[cell];
}
protected override object PrepareCellForEdit(FrameworkElement editingElement, RoutedEventArgs editingEventArgs)
{
var radioButton = editingElement as RadioButton;
if (radioButton == null) return null;
return radioButton.IsChecked;
}
}
这是它的工作原理。我添加了一个字典来捕获每个单选按钮的模型,并且在创建单选按钮时,我将模型添加到新字典并挂钩单选按钮上的 Unchecked
事件:
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
if (_buttons.ContainsKey(cell))
{
return (_buttons[cell]);
}
var radioButton = new RadioButton { GroupName = Group };
radioButton.Unchecked += RadioButton_Unchecked; // Added
BindingOperations.SetBinding(radioButton, ToggleButton.IsCheckedProperty, Binding);
_buttons.Add(cell, radioButton);
_models.Add(radioButton, dataItem); // Added
return radioButton;
}
然后,我只是在事件触发时在模型中设置 Edited
属性:
private void RadioButton_Unchecked(object sender, RoutedEventArgs e)
{
var button = sender as RadioButton;
dynamic model = _models[button];
try
{
// Notify IEditableObject implementation, if it exists.
model.Edited = true;
}
catch { }
}