Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/wpf/14.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Wpf 如何添加验证以查看模型属性或如何实现INotifyDataErrorInfo_Wpf_Validation_Mvvm_Viewmodel_Inotifydataerrorinfo - Fatal编程技术网

Wpf 如何添加验证以查看模型属性或如何实现INotifyDataErrorInfo

Wpf 如何添加验证以查看模型属性或如何实现INotifyDataErrorInfo,wpf,validation,mvvm,viewmodel,inotifydataerrorinfo,Wpf,Validation,Mvvm,Viewmodel,Inotifydataerrorinfo,我有一个ObservaleCollection类型的数据集合(比如说实例为myClassTypes)。在一些用户操作之后,这个myClassTypes在ViewModel中填充了值。在视图中,有一个文本框,用户可以在其中输入文本。我需要根据myClassTypes值验证textbox数据。因此,如果myClassTypes包含用户在textbox中插入的文本,则验证将通过,否则将失败。 我的代码片段是: 视图模型: public ObservableCollection < MyClass

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

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);
    }
}
PublicObservableCollectionViewModelClassType{
得到{
return\u myClassTypes;
}
设置{
_myClassTypes=值;
NotifyOfPropertyChange(()=>MyClassTypes);
}
}
公共类TestValidationRule:ValidationRule{
公共ObservableCollectionMyClassType{
get=>(ObservableCollection)GetValue(MyClassTypesProperty);
set=>SetValue(MyClassTypesProperty,value);
}
}
仅供参考:MyClassTypesProperty是一个依赖属性

My View.xaml是:

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


我无法在MyClassTypes中获取ViewModelClassTypes填充值。有人能告诉我我做错了什么吗?

自从.Net 4.5以来,实现数据验证的首选方法是让您的视图模型实现(示例来自,示例来自)

注意:
INotifyDataErrorInfo
替换过时的
IDataErrorInfo


INotifyDataErrorInfo的工作原理 当
Binding
validateNotifyDataErrors
属性设置为
true
时,绑定引擎将搜索绑定源上的
INotifyDataErrorInfo
实现,并订阅
INotifyDataErrorInfo.ErrorsAnged
事件

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

此验证反馈可视化过程仅在
绑定时执行。ValidateAnnotifyDataErrors
在特定数据绑定上设置为
true
,并且
绑定模式
设置为
BindingMode.TwoWay
BindingMode.OneWayToSource

如何实现INotifyDataErrorInfo 以下示例显示了使用
ValidationRule
(封装实际数据验证实现)和lambda(或委托)的默认验证。最后一个示例演示了如何使用验证属性实现数据验证

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


ViewModel.cs

视图模型负责验证其属性,以确保模型的数据完整性。
从.NET 4.5开始,推荐的方法是让视图模型实现
INotifyDataErrorInfo
接口。
关键是每个属性或规则都有单独的
ValidationRule
实现:

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);
    } 
  }

  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 List<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; }

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

要启用可视化数据验证反馈,必须在每个相关的
绑定上将
绑定.validateAnnotifyDataErrors
属性设置为
true
。然后WPF框架将显示控件的默认错误反馈。注意:要使此工作正常,
绑定.Mode
必须是
OneWayToSource
TwoWay
(这是
TextBox.Text
属性的默认值):

除了我提供的链接之外,你可以在网上找到许多例子

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

您可以更改
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 '@'.");
}
<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 x:Key=ValidationErrorTemplate>
    <StackPanel>
        <!-- Placeholder for the DataGridTextColumn itself -->
        <AdornedElementPlaceholder />
        <ItemsControl ItemsSource="{Binding}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
// 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 '@'.");
}
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{    
  private string userInputAttributeValidation;
 
  [Required(ErrorMessage = "Value is required.")]
  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; }    
}