如何使用验证来防止用户重复 Datagrid 值?

问题描述 投票:0回答:1

我正在尝试找出如何阻止用户在数据网格中添加或重命名字段(如果他们选择的值重复)。例如,如果有一个 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>
wpf validation mvvm datagrid
1个回答
0
投票

您应该实施

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
必须扩展其行为,以便通过比较来定义和评估实例的唯一性。

  1. 实现
    IEquatable
    并重写
    object.Equals
    方法来定义对象相等性(行唯一性)。
  2. 实现
    IEditableObject
    以启用编辑状态跟踪。然后,
    Person
    将引发我们为此目的引入的
    Person.Edited
    事件。
  3. 实现
    INotifyDataErrorInfo
    以启用属性验证。
  4. 实现自定义
    Person
    容器,使所有者能够方便地观察
    Person.Edited
    事件,而无需为每个项目注册处理程序。
  5. 使用简单样式触发器和模板来布局错误视觉效果,实现 DataGrid 的视觉错误反馈。

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="&#xEA3B;"
                   FontFamily="Segoe MDL2 Assets"
                   AutomationProperties.Name="Favorite"
                   VerticalAlignment="Center"
                   Foreground="Red"
                   FontSize="14" />
        <TextBlock Text="&#xE783;"
                   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>
© www.soinside.com 2019 - 2024. All rights reserved.