5

我有一个 ObservableCollection 类型的数据集合(例如 myClassTypes 实例)。在一些用户操作之后,这个 myClassTypes 填充了 ViewModel 中的值。在视图中,有一个文本框,用户可以在其中输入文本。我需要根据 myClassTypes 值验证文本框数据。因此,如果 myClassTypes 包含用户在文本框中插入的文本,则通过验证,否则将失败。我的代码片段是:ViewModel:

public ObservableCollection < MyClassType > ViewModelClassTypes {
    get {

        return _myClassTypes;
    }
    set {
        _myClassTypes = value;
        NotifyOfPropertyChange(() = >MyClassTypes);
    }
}

public class TestValidationRule: ValidationRule {
    public ObservableCollection < MyClassType > MyClassTypes {
        get = >(ObservableCollection < MyClassType > ) GetValue(MyClassTypesProperty);
        set = >SetValue(MyClassTypesProperty, value);
    }
}

仅供参考:MyClassTypesProperty 是一个依赖属性

我的 View.xaml 是:

<TextBox>
    <TextBox.Text>
        <Binding UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <validationRules:TestValidationRule MyClassTypes="{Binding ViewModelClassTypes}"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

我无法在 MyClassTypes 中获取 ViewModelClassTypes 填充值。谁能建议我做错了什么?

4

1 回答 1

13

自 .Net 4.5 以来实现数据验证的首选方式是让您的视图模型实现INotifyDataErrorInfo(来自Technet的示例,来自MSDN (Silverlight)的示例)。

注意:INotifyDataErrorInfo替换过时的IDataErrorInfo.


INotifyDataErrorInfo工作原理

当 的ValidatesOnNotifyDataErrors属性Binding设置为true时,绑定引擎将在绑定源上搜索INotifyDataErrorInfo实现并订阅INotifyDataErrorInfo.ErrorsChanged事件。

如果ErrorsChanged绑定源的事件被引发并INotifyDataErrorInfo.HasErrors评估为true,则绑定引擎将调用INotifyDataErrorInfo.GetErrors(propertyName)实际源属性的方法以检索相应的错误消息,然后将可自定义的验证错误模板应用于目标控件以可视化验证错误。
默认情况下,在验证失败的元素周围会绘制一个红色边框。

此验证反馈可视化过程仅在特定数据绑定上设置为 且Binding.ValidatesOnNotifyDataErrors设置为或时执行。trueBinding.ModeBindingMode.TwoWayBindingMode.OneWayToSource

如何实施INotifyDataErrorInfo

以下示例显示了属性验证的三种变体,使用

  • a ValidationRule(封装实际数据验证实现的类)
  • Lambdas(或委托)
  • 验证属性(用于装饰已验证的属性)。

代码未经测试。这些片段应该都可以工作,但可能由于输入错误而无法编译。此代码旨在提供有关如何实现INotifyDataErrorInfo接口的简单示例。


视图模型.cs

视图模型负责验证自己的属性以确保模型的数据完整性。
从 .NET 4.5 开始,推荐的方式是让视图模型实现INotifyDataErrorInfo接口。关键是为每个属性或规则
分别实现。ValidationRule

扩展ValidationRule是可选的。我选择扩展ValidationRule是因为它已经提供了一个完整的验证 API,并且如果需要,可以通过绑定验证重用实现。
基本上,属性验证的结果应该是bool指示验证失败或成功的结果,以及可以显示给用户以帮助他修复输入的消息。

如果出现验证错误,我们所要做的就是生成错误消息,将其添加到私有字符串集合中,以允许我们的INotifyDataErrorInfo.GetErrors(propertyName)实现从该集合返回正确的错误消息并引发INotifyDataErrorInfo.ErrorChanged事件以通知 WPF 绑定引擎有关错误:

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
  // Example property, which validates its value before applying it
  private string userInput;
  public string UserInput
  { 
    get => this.userInput; 
    set 
    { 
      // Validate the value
      ValidateProperty(value);

      this.userInput = value; 
      OnPropertyChanged();
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, List<string>>();
    this.ValidationRules = new Dictionary<string, List<ValidationRule>>();

    // Create a Dictionary of validation rules for fast lookup. 
    // Each property name of a validated property maps to one or more ValidationRule.
    this.ValidationRules.Add(nameof(this.UserInput), new List<ValidationRule>() {new UserInputValidationRule()});
  }

  // Validation method. 
  // Is called from each property which needs to validate its value.
  // Because the parameter 'propertyName' is decorated with the 'CallerMemberName' attribute.
  // this parameter is automatically generated by the compiler. 
  // The caller only needs to pass in the 'propertyValue', if the caller is the target property's set method.
  public bool ValidateProperty<TValue>(TValue propertyValue, [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    this.Errors.Remove(propertyName); 
    OnErrorsChanged(propertyName); 

    if (this.ValidationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
    {
      // Apply all the rules that are associated with the current property 
      // and validate the property's value
      propertyValidationRules
        .Select(validationRule => validationRule.Validate(propertyValue, CultureInfo.CurrentCulture))
        .Where(result => !result.IsValid)
        .ToList()
        .ForEach(invalidResult => AddError(propertyName, invalidResult.ErrorContent as string));

      return !PropertyHasErrors(propertyName);
    }

    // No rules found for the current property
    return true;
  }   

  // Adds the specified error to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  public void AddError(string propertyName, string errorMessage, bool isWarning = false)
  {
    if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
    {
      propertyErrors = new List<string>();
      this.Errors[propertyName] = propertyErrors;
    }

    if (!propertyErrors.Contains(errorMessage))
    {
      if (isWarning) 
      {
        // Move warnings to the end
        propertyErrors.Add(errorMessage);
      }
      else 
      {
        propertyErrors.Insert(0, errorMessage);
      }
      OnErrorsChanged(propertyName);
    } 
  }

  // Optional method to check if a certain property has validation errors
  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  // The WPF binding engine will listen to this event
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // This implementatio of GetErrors returns all errors of the specified property. 
  // If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  // This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors retirn true
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out List<string> errors) 
        ? errors 
        : new List<string>();

  // Returns 'true' if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)        
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  protected virtual void OnErrorsChanged(string propertyName)
  {
    this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
  }

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<String, List<String>> Errors { get; }

  // Maps a property name to a list of ValidationRules that belong to this property
  private Dictionary<String, List<ValidationRule>> ValidationRules { get; }
}

UserInputValidationRule.cs

此示例验证规则扩展ValidationRule并检查输入是否以“@”字符开头。如果不是,它返回一个无效ValidationResult的错误消息,可以显示给用户以帮助他修复他的输入。

public class UserInputValidationRule : ValidationRule
{        
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
    if (!(value is string userInput))
    {
      return new ValidationResult(false, "Value must be of type string.");    
    }

    if (!userInput.StartsWith("@"))
    {
      return new ValidationResult(false, "Input must start with '@'.");    
    }

    return ValidationResult.ValidResult;
  }
}

主窗口.xaml

要启用可视数据验证反馈,Binding.ValidatesOnNotifyDataErrors必须将属性设置为true每个相关Binding的,即其中的源Binding是经过验证的属性。然后 WPF 框架将显示控件的默认错误反馈。
注意使这项工作Binding.Mode必须是OneWayToSourceTwoWay(这是TextBox.Text属性的默认值):

<Window>
    <Window.DataContext>
        <ViewModel />       
    </Window.DataContext>
    
    <!-- Important: set ValidatesOnNotifyDataErrors to true to enable visual feedback -->
    <TextBox Text="{Binding UserInput, ValidatesOnNotifyDataErrors=True}" 
             Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" />  
</Window>

以下是自定义验证错误模板的示例。
默认的视觉错误反馈是经过验证的元素周围的简单红色边框。如果您想自定义视觉反馈,例如,允许向用户显示错误消息,您可以定义一个自定义并通过附加属性(见上文)ControlTemplate将其分配给经过验证的元素(在本例中为)。 以下启用显示与已验证属性关联的错误消息列表:TextBoxValidation.ErrorTemplate
ControlTemplate
在此处输入图像描述

<ControlTemplate x:Key="ValidationErrorTemplate">
  <StackPanel>
    <Border BorderBrush="Red" 
            BorderThickness="1">
      
      <!-- Placeholder for the DataGridTextColumn itself -->
      <AdornedElementPlaceholder x:Name="AdornedElement"  />
    </Border>

    <Border Background="White" 
            BorderBrush="Red" 
            Padding="4"
            BorderThickness="1,0,1,1" 
            HorizontalAlignment="Left">
      <ItemsControl ItemsSource="{Binding}" HorizontalAlignment="Left">
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding ErrorContent}" 
                       Foreground="Red"/>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </Border>
  </StackPanel>
</ControlTemplate>

我建议将 的实现与实现一起移动INotifyDataErrorInfo到基类(例如抽象ViewModel类)中,INotifyPropertyChanged并让所有视图模型继承它。这使验证逻辑可重用并保持您的视图模型类干净。

您可以更改示例的实现细节INotifyDataErrorInfo以满足需求。

评论

作为替代方法,ValidationRule可以用委托替换以启用 Lambda 表达式或方法组:

// Example uses System.ValueTuple
public bool ValidateProperty<TValue>(
  TValue value, 
  Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate, 
  [CallerMemberName] string propertyName = null)  
{  
  // Clear previous errors of the current property to be validated 
  this.Errors.Remove(propertyName); 
  OnErrorsChanged(propertyName); 

  // Validate using the delegate
  (bool IsValid, IEnumerable<string> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, string.Empty);

  if (!validationResult.IsValid)
  {
    // Store the error messages of the failed validation
    foreach (string errorMessage in validationResult.ErrorMessages)
    {
      // See previous example for implementation of AddError(string,string):void
      AddError(propertyName, errorMessage);
    }
  } 

  return validationResult.IsValid;
}   

private string userInput;
public string UserInput
{ 
  get => this.userInput; 
  set 
  { 
    // Validate the new property value before it is accepted
    if (ValidateProperty(value, 
      newValue => newValue.StartsWith("@") 
        ? (true, new List<string>()) 
        : (false, new List<string> {"Value must start with '@'."})))
    {
      // Accept the valid value
      this.userInput = value; 
      OnPropertyChanged();
    }
  }
}

// Alternative usage example property which validates its value 
// before applying it using a Method Group.
// Example uses System.ValueTuple.
private string userInputAlternativeValidation;
public string UserInputAlternativeValidation
{ 
  get => this.userInputAlternativeValidation; 
  set 
  { 
    // Use Method group
    if (ValidateProperty(value, AlternativeValidation))
    {
      this.userInputAlternativeValidation = value; 
      OnPropertyChanged();
    }
  }
}

private (bool IsValid, string ErrorMessage) AlternativeValidation(string value)
{
  return value.StartsWith("@") 
    ? (true, string.Empty) 
    : (false, "Value must start with '@'.");
}

数据验证使用ValidationAttribute

这是一个INotifyDataErrorInfo带有ValidationAttribute支持的示例实现,例如MaxLengthAttribute. 该解决方案结合了之前的 Lamda 版本,另外还支持同时使用 Lambda 表达式/委托进行验证:

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{    
  private string userInputAttributeValidation;
 
  [MaxLength(Length = 5, ErrorMessage = "Only five characters allowed.")]
  public string UserInputAttributeValidation
  { 
    get => this.userInputAttributeValidation; 
    set 
    { 
      // Use only the attribute (can be combined with a Lambda or Method group)
      if (ValidateProperty(value))
      {
        this.userInputAttributeValidation = value; 
        OnPropertyChanged();
      }
    }
  }

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, List<string>>();
  }

  // Validate properties using decorated attributes and/or a validation delegate. 
  // The validation delegate is optional.
  public bool ValidateProperty<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate = null, 
    [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    this.Errors.Remove(propertyName); 
    OnErrorsChanged(propertyName); 

    bool isValueValid = ValidatePropertyUsingAttributes(value, propertyName);
    if (validationDelegate != null)
    {
      isValueValid |= ValidatePropertyUsingDelegate(value, validationDelegate, propertyName);
    }

    return isValueValid;
  }     

  // Validate properties using decorated attributes. 
  public bool ValidatePropertyUsingAttributes<TValue>(TValue value, string propertyName)  
  {  
    // The result flag
    bool isValueValid = true;

    // Check if property is decorated with validation attributes
    // using reflection
    IEnumerable<Attribute> validationAttributes = GetType()
      .GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
      ?.GetCustomAttributes(typeof(ValidationAttribute)) ?? new List<Attribute>();

    // Validate using attributes if present
    if (validationAttributes.Any())
    {
      var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
      var validationResults = new List<ValidationResult>();
      if (!Validator.TryValidateProperty(value, validationContext, validationResults))
      {           
        isValueValid = false;

        foreach (ValidationResult attributeValidationResult in validationResults)
        {
          AddError(propertyName, attributeValidationResult.ErrorMessage);
        }
      }
    }

    return isValueValid;
  }       

  // Validate properties using the delegate. 
  public bool ValidatePropertyUsingDelegate<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<string> ErrorMessages)> validationDelegate, 
    string propertyName) 
  {  
    // The result flag
    bool isValueValid = true;

    // Validate using the delegate
    (bool IsValid, IEnumerable<string> ErrorMessages) validationResult = validationDelegate.Invoke(value);

    if (!validationResult.IsValid)
    {
      isValueValid = false;

      // Store the error messages of the failed validation
      foreach (string errorMessage in validationResult.ErrorMessages)
      {
        AddError(propertyName, errorMessage);
      }
    } 

    return isValueValid;
  }       

  // Adds the specified error to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  public void AddError(string propertyName, string errorMessage, bool isWarning = false)
  {
    if (!this.Errors.TryGetValue(propertyName, out List<string> propertyErrors))
    {
      propertyErrors = new List<string>();
      this.Errors[propertyName] = propertyErrors;
    }

    if (!propertyErrors.Contains(errorMessage))
    {
      if (isWarning) 
      {
        // Move warnings to the end
        propertyErrors.Add(errorMessage);
      }
      else 
      {
        propertyErrors.Insert(0, errorMessage);
      }
      OnErrorsChanged(propertyName);
    } 
  }

  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out List<string> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // Returns all errors of a property. If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IEnumerable<string> errors) 
        ? errors 
        : new List<string>();

  // Returns if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  protected virtual void OnErrorsChanged(string propertyName)
  {
    this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
  }

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<String, List<String>> Errors { get; }    
}
于 2019-06-15T06:50:57.930 回答