我正在尝试找出如何阻止用户在数据网格中添加或重命名字段(如果他们选择的值重复)。例如,如果有一个 Person 对象的目录,如果有人尝试添加已存在的名称,他们会收到错误。或者,如果他们将数据网格中的现有值编辑为已存在的名称,则会引发错误并阻止他们执行此操作。如果用户以编程方式尝试将同名的人添加到目录中,则很容易防止这种情况,但我不知道如果视图上发生更改(例如在数据网格中),如何检查这一点。
人物类别:
namespace WpfApp1
{
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public Person(string name)
{
Name = name;
}
public void OnPropertyChanged([CallerMemberName] string name = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
目录类:
namespace WpfApp1
{
public class Directory
{
public ObservableCollection<Person> People { get; set; }
public Directory()
{
People = new ObservableCollection<Person>();
}
public void RemovePerson(Person person)
{
if (People.Contains(person))
{
People.Remove(person);
}
}
public void AddPerson(Person person)
{
if (NameExists(person))
{
MessageBox.Show("Cannot have duplicate names!");
return;
}
People.Add(person);
}
public bool NameExists(Person person)
{
return People.Any(p => p.Name == person.Name);
}
}
}
MainWindow.xaml.cs:
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public Directory Directory { get; set; }
public MainWindow()
{
InitializeComponent();
Directory = new Directory();
Person p1 = new Person("Bob");
Person p2 = new Person("John");
Person p3 = new Person("Al");
Directory.AddPerson(p1);
Directory.AddPerson(p2);
Directory.AddPerson(p3);
dgDirectory.ItemsSource = Directory.People;
}
}
}
MainWindow.xaml:
<Window x:Class="WpfApp1.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<DataGrid x:Name="dgDirectory" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
您应该实施
INotifyDataErrorInfo
。
如果您只使用行验证,那么您可以使用
DataGrid
的内置行验证功能(它基本上与提供视觉错误反馈有关)。但是,当与单元格验证一起使用时,它的行为不正确,其中使用 INotifyDataErrorInfo
而不是绑定验证来执行验证。这是因为 DataGrid
处理列和列绑定的特殊方式。
因此本示例选择通过简单样式触发器来实现自定义验证错误反馈。
我认为这是一个两级验证:1)行项目本身,它实现
INotifyDataErrorInfo
来验证每个属性是否正确输入(例如格式、长度等),其中上下文是项目本身。 2) 源集合的所有者,在所有项目的上下文中执行项目验证(例如重复项、条目总数等)。一般来说,2) 是关于根据外部上下文(对行项目隐藏的上下文)验证行项目。Person
实现 INotifyDataErrorInfo
。
关键是使用
DataGrid
的内置功能来处理IEditableObject
的实现。这样您就可以等待用户完成行编辑,而不需要 UI 相关事件。编辑完成后,我们可以通知管理 Person
源集合的所有者有关更改的信息,以便所有者可以执行涉及所有者上下文的附加验证。在您的示例中,您希望确保数据集(表行)的唯一性。
在表数据结构中,有两种类型的唯一性:1)行和2)列。例如,“Name = John; Lastname = Doe”和“Name = John; Lastname = Wick”通常不被视为相同的数据集(行),尽管“Name”列包含重复项。唯一性 1) 使用简单的对象相等性进行检查,而唯一性情况 2) 需要定义唯一性的规则的高级实现。
因此,
Person
必须扩展其行为,以便通过比较来定义和评估实例的唯一性。
IEquatable
并重写 object.Equals
方法来定义对象相等性(行唯一性)。IEditableObject
以启用编辑状态跟踪。然后,Person
将引发我们为此目的引入的 Person.Edited
事件。INotifyDataErrorInfo
以启用属性验证。Person
容器,使所有者能够方便地观察 Person.Edited
事件,而无需为每个项目注册处理程序。Person.cs
public class Person : IEditableObject, INotifyDataErrorInfo, INotifyPropertyChanged, IEquatable<Person>
{
// Nested type which is used as data store for the Person
// to enable simple data editing and roll-back
private class PersonData
{
public PersonData()
{
}
// Copy constructor
public PersonData(PersonData personData)
{
Name = personData.Name;
}
public string Name { get; set; }
}
public string Name
{
get => _personData.Name;
set
{
_personData.Name = value;
// Example property validation to provide cell erors
Validate(
value,
val => val is not null && val.Contains("@") ? (false, new[] { "Contains 1" }) : (true, Enumerable.Empty<string>()));
OnPropertyChanged();
}
}
// We introduce this collection for binding the error template so that it
// can show all current error messages without accessing
// the _errorMessages Dictionary
public List<string>? InstanceErrors
=> _errorMessages?.Values.SelectMany(message => message).ToList();
public event PropertyChangedEventHandler? PropertyChanged;
private PersonData _backupData;
private PersonData _personData;
private const string InstanceErrorsIdentifier = "";
public Person(string name)
{
_personData = new PersonData();
_errorMessages = new Dictionary<string, List<string>>();
Name = name;
}
public void OnPropertyChanged([CallerMemberName] string name = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
#region IEditable implementation
public bool IsEditing { get; private set; }
public event EventHandler<EditedEventArgs> Edited;
void IEditableObject.BeginEdit()
=> CreateBackup();
void IEditableObject.CancelEdit()
=> RestoreFromBackup();
void IEditableObject.EndEdit()
=> CommitEdit();
private void CreateBackup()
{
_backupData = new PersonData(_personData);
IsEditing = true;
}
private void RestoreFromBackup()
{
_personData = _backupData;
IsEditing = false;
}
private void CommitEdit()
{
IsEditing = false;
OnEdited();
}
private void OnEdited()
{
ClearErrors(InstanceErrorsIdentifier);
var eventArgs = new EditedEventArgs(this);
Edited?.Invoke(this, eventArgs);
if (!eventArgs.IsValid)
{
AddErrors(InstanceErrorsIdentifier, eventArgs.ErrorMessages);
}
}
#endregion IEditable implementation
#region Implementation of INotifyDataErrorInfo
// Call this method from the properties to validate their value
private void Validate<TValue>(
TValue value,
Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate,
[CallerMemberName] string propertyName = null)
{
ArgumentNullException.ThrowIfNullOrWhiteSpace(propertyName);
ClearErrors(propertyName);
(bool isValid, IEnumerable<string> ErrorMessages) result = validationDelegate.Invoke(value);
if (!result.isValid)
{
AddErrors(propertyName, result.ErrorMessages);
}
}
private void AddErrors(string propertyName, IEnumerable<string> errorMessages)
{
_errorMessages.Add(propertyName, errorMessages.ToList());
OnErrorsChanged(propertyName);
}
private void ClearErrors(string propertyName)
{
_errorMessages.Remove(propertyName);
OnErrorsChanged(propertyName);
}
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
private void OnErrorsChanged(string propertyName)
{
OnPropertyChanged(nameof(HasErrors));
OnPropertyChanged(nameof(InstanceErrors));
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
private readonly Dictionary<string, List<string>> _errorMessages;
public bool HasErrors => _errorMessages.Any();
public IEnumerable GetErrors(string? propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
return _errorMessages.Values.SelectMany(message => message).ToList();
}
else
{
return _errorMessages.TryGetValue(propertyName, out List<string> errors)
? errors
: Enumerable.Empty<string>();
}
}
#endregion Implementation of INotifyDataErrorInfo
#region Implementation of IEquatable
public bool Equals(Person? other) => other.Name.Equals(Name, StringComparison.OrdinalIgnoreCase);
#endregion Implementation of IEquatable
public override bool Equals(object? obj) => obj is Person other ? Equals(other) : false;
public override int GetHashCode() => HashCode.Combine(this.Name);
public override string? ToString() => Name;
}
EditedEventArgs.cs
public class EditedEventArgs : EventArgs
{
public EditedEventArgs(Person person)
{
Person = person;
_errorMessages = new List<string>();
ErrorMessages = new ReadOnlyCollection<string>(_errorMessages);
}
public void SetErrors(IEnumerable<string> errors) => _errorMessages.AddRange(errors);
public Person Person { get; }
public ReadOnlyCollection<string> ErrorMessages { get; }
public bool IsValid => !_errorMessages.Any();
private readonly List<string> _errorMessages;
}
PersonCollection.cs
[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class PersonCollection : ObservableCollection<Person>
{
#region Overrides of ObservableCollection<TItem>
/// <inheritdoc />
protected override void InsertItem(int index, Person item)
{
base.InsertItem(index, item);
StartListenToItemPropertyChanged(item);
}
/// <inheritdoc />
protected override void RemoveItem(int index)
{
if (index < this.Count)
{
Person item = this.Items[index];
StopListenToItemPropertyChanged(item);
}
base.RemoveItem(index);
}
/// <inheritdoc />
protected override void ClearItems()
{
foreach (Person item in this.Items)
{
StopListenToItemPropertyChanged(item);
}
base.ClearItems();
}
/// <inheritdoc />
protected override void SetItem(int index, Person item)
{
if (index < this.Count)
{
Person oldItem = this.Items[index];
StopListenToItemPropertyChanged(oldItem);
StartListenToItemPropertyChanged(item);
}
base.SetItem(index, item);
}
#endregion Overrides of ObservableCollection<TItem>
private void StartListenToItemPropertyChanged(Person item)
=> WeakEventManager<Person, EditedEventArgs>.AddHandler(item, nameof(Person.Edited), OnPersonChanged);
private void StopListenToItemPropertyChanged(Person item)
=> WeakEventManager<Person, EditedEventArgs>.RemoveHandler(item, nameof(Person.Edited), OnPersonChanged);
public event EventHandler<EditedEventArgs> PersonChanged;
private void OnPersonChanged(object item, EditedEventArgs e)
=> this.PersonChanged?.Invoke(item, e);
}
目录.cs
public class Directory
{
public PersonCollection People { get; }
private readonly EqualityComparer<Person> _personComparer;
public Directory()
{
People = new PersonCollection();
People.PersonChanged += OnPersonChanged;
// We use the uniqueness that Person defines
_personComparer = EqualityComparer<Person>.Create((person1, person2) => person1.Equals(person2), null);
}
private void OnPersonChanged(object? sender, EditedEventArgs e)
{
if (People.Contains(e.Person, _personComparer))
{
// Invalidate the Person to report a row error
// and provide error messages
e.SetErrors(new[] { "Person already exists1" });
}
}
public void RemovePerson(Person person)
{
if (People.Contains(person))
{
People.Remove(person);
}
}
public void AddPerson(Person person)
{
if (NameExists(person))
{
MessageBox.Show("Cannot have duplicate names!");
return;
}
People.Add(person);
}
public bool NameExists(Person person)
{
return People.Any(p => p.Name == person.Name);
}
}
MainWindow.xaml
<UserControl x:Class="Net.Wpf.PrintableContentHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Net.Wpf"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<Style x:Key="DefaultDataGridRowStyle"
TargetType="DataGridRow"
BasedOn="{StaticResource {x:Type DataGridRow}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Item.HasErrors, Mode=OneWay}"
Value="True">
<Setter Property="BorderBrush"
Value="Red" />
<Setter Property="BorderThickness"
Value="1" />
</DataTrigger>
</Style.Triggers>
</Style>
<ControlTemplate x:Key="DefaultOverrideRowValidationErrorTemplate" />
<DataTemplate x:Key="DefaultRowHeaderTemplate" />
<DataTemplate x:Key="RowHeaderValidationErrorTemplate"
DataType="{x:Type local:Person}">
<Grid>
<TextBlock Text=""
FontFamily="Segoe MDL2 Assets"
AutomationProperties.Name="Favorite"
VerticalAlignment="Center"
Foreground="Red"
FontSize="14" />
<TextBlock Text=""
FontFamily="Segoe MDL2 Assets"
AutomationProperties.Name="Favorite"
VerticalAlignment="Center"
Foreground="White"
FontSize="16" />
<Popup x:Name="NotificationPopup"
AllowsTransparency="True"
IsOpen="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Path=Item.HasErrors, Mode=OneWay}"
StaysOpen="True"
PlacementTarget="{Binding RelativeSource={RelativeSource AncestorType=DataGridRowHeader}}"
Placement="Bottom">
<Border Background="White"
BorderBrush="Red"
BorderThickness="1"
CornerRadius="4"
Padding="4">
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Path=Item.InstanceErrors[0]}"
Background="White"
Foreground="Red" />
</Border>
</Popup>
</Grid>
</DataTemplate>
<Style x:Key="DefaultDataGridRowHeaderStyle"
TargetType="DataGridRowHeader">
<Setter Property="ContentTemplate"
Value="{StaticResource DefaultRowHeaderTemplate}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasErrors, Mode=OneWay}"
Value="True">
<Setter Property="ContentTemplate"
Value="{StaticResource RowHeaderValidationErrorTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGrid">
<Setter Property="RowValidationErrorTemplate"
Value="{DynamicResource DefaultOverrideRowValidationErrorTemplate}" />
<Setter Property="RowStyle"
Value="{StaticResource DefaultDataGridRowStyle}" />
<Setter Property="RowHeaderStyle"
Value="{StaticResource DefaultDataGridRowHeaderStyle}" />
</Style>
</UserControl.Resources>
<!--
We have to set the RowValidationErrorTemplate to an empty dummy value
to prevent the DataGrid from showing its default
-->
<DataGrid RowValidationErrorTemplate="{StaticResource DefaultOverrideRowValidationErrorTemplate}"
RowHeaderWidth="30"
AutoGenerateColumns="True"
ItemsSource="{Binding People}" />
</Window>