如何向ViewModel属性添加验证

时间:2019-06-15 00:34:09

标签: wpf

我有一个ObservableCollection类型的数据收集(例如,实例为myClassTypes)。在执行一些用户操作之后,此myClassTypes用ViewModel中的值填充。在视图中,有一个TextBox,用户可以在其中输入文本。我需要根据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填充的值。有人可以建议我在做什么错吗?

1 个答案:

答案 0 :(得分:0)

自.Net 4.5开始实施验证的首选方法是让您的视图模型实施INotifyDataErrorInfo MSDNExampleExample from MSDN (Silverlight)。注意:INotifyDataErrorInfo取代了过时的IDataErrorInfo

实现INotifyDataErrorInfo时,必须提供一个HasErrors属性,您可以从视图中绑定该属性以显示错误消息。另外,我们还必须引入一个属性作为TextBox的绑定目标到视图模型。我们将其称为UserInput


INotifyDataErrorInfo的工作方式

ValidatesOnNotifyDataErrors的{​​{1}}属性设置为Binding时,绑定引擎将在绑定源上搜索true实现以订阅{{1 }}事件。

如果引发INotifyDataErrorInfo事件并且ErrorsChanged的值为ErrorsChanged,则绑定将为实际属性调用HasErrors方法,以检索特定的错误消息并应用可定制的验证错误模板以可视化该错误。默认情况下,未通过验证的元素周围会绘制红色边框。

如何实现true

视图模型负责验证其属性。因此,它必须实现GetErrors()。关键是每个属性都有单独的INotifyDataErrorInfo

视图模型可能如下所示:

INotifyDataErrorInfo

The ValidationRule

public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    public ViewModel()
    {
      this.errors = new Dictionary<string, List<string>>();
      this.validationRules = new Dictionary<string, List<ValidationRule>>();

      this.validationRules.Add(nameof(this.UserInput), new List<ValidationRule>() { new TestValidationRule() });
    }


    public bool ValidateProperty(object value, [CallerMemberName] string propertyName = null)  
    {  
        lock (this.syncLock)  
        {  
            if (!this.validationRules.TryGetValue(propertyName, out List<ValidationRule> propertyValidationRules))
            {
              return;
            }  

            // Clear previous errors of the current property to be validated 
            if (this.errors.ContainsKey(propertyName))  
            {
               this.errors.Remove(propertyName);  
               OnErrorsChanged(propertyName);  
            }

            // Apply all the rules that are associated with the current property 
            propertyValidationRules.ForEach(
              (validationRule) => 
              {
                ValidationResult result = validationRule.Validate(value, CultuteInfo.CurrentCulture);
                if (!result.IsValid)
                {
                  AddError(propertyName, result.ErrorContent, false);
                } 
              }               
        }  
    }   

    // 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 collection changes. 
    public void AddError(string propertyName, string error, bool isWarning)
    {
        if (!this.errors.ContainsKey(propertyName))
        {
           this.errors[propertyName] = new List<string>();
        }

        if (!this.errors[propertyName].Contains(error))
        {
            if (isWarning) 
            {
              this.errors[propertyName].Add(error);
            }
            else 
            {
              this.errors[propertyName].Insert(0, error);
            }
            RaiseErrorsChanged(propertyName);
        }
    }

    // Removes the specified error from the errors collection if it is
    // present. Raises the ErrorsChanged event if the collection changes.
    public void RemoveError(string propertyName, string error)
    {
        if (this.errors.ContainsKey(propertyName) &&
            this.errors[propertyName].Contains(error))
        {
            this.errors[propertyName].Remove(error);
            if (this.errors[propertyName].Count == 0)
            {
              this.errors.Remove(propertyName);
            }
            RaiseErrorsChanged(propertyName);
        }
    }

    #region INotifyDataErrorInfo Members

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (String.IsNullOrEmpty(propertyName) || 
            !this.errors.ContainsKey(propertyName)) return null;
        return this.errors[propertyName];
    }

    public bool HasErrors
    {
        get { return errors.Count > 0; }
    }

    #endregion

    #region INotifyDataErrorInfo Members

    public event PropertyChangedEventHandler PropertyChanged;

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

    #endregion

    private MyClassType _myClassTypes;
    public ObservableCollection<MyClassType> ViewModelClassTypes
    {
        get
        {
            return this._myClassTypes;
        }
        set
        {
            this._myClassTypes = value;
            if (!this.validationRules.TryGetValue(nameof(this.UserInput), out List<ValidationRule> userInputValidationRules))
            {
                TestValidationRule userInputRule = userInputValidationRules.Where((rule) => rule is TestValidationRule).FirstOrDefault();
                userInputRule.MyClassTypes = this._myClassTypes; 
            }  
            OnPropertyChanged();;
        }
    }

    private double userInput;
    public double UserInput
    { 
      get => userInput; 
      set 
      { 
        if (ValidateProperty(value))
        {
          this.userInput = value; 
          OnPropertyChanged();
        }
      }
    }


    private Dictionary<String, List<String>> errors;

    // The ValidationRules for each property
    private Dictionary<String, List<ValidationRule>> validationRules;
    private object syncLock = new object();
}

视图:

ValidationRule

以下是验证错误模板,以防您要自定义外观(可选)。通过附加的属性public class TestValidationRule : ValidationRule { public TestValidationRule() { this.MyClassTypes = new List<MyClassType>(); } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { if (!value is string userInput) { throw new InvalidArgumentException(); } // Replace with your validation implementation if (this.MyClassTypes.Contains(userInput)) { return ValidationResult.ValidResult; } else { return new ValidationResult(false, "MyClassType doesn't exist."); } } public List<MyClassType> MyClassTypes { get; set; } } (在上文中),在已验证元素(在本例中为<Grid> <Grid.DataContext> <ViewModel x:Name="ViewModel" /> </Grid.DataContext> <TextBox x:Name="Input" Text = "{Binding UserInput, Mode=OneWayToSource, ValidatesOnNotifyDataErrors=True}" Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}" /> </Grid> )上进行了设置:

TextBox

在我提供的链接旁边,您会在网络上找到许多示例。

我建议将Validation.ErrorTemplate的实现转移到基类(例如<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> INotifyPropertyChanged`中,并让所有视图模型都继承它。这使验证逻辑可重用,并保持视图模型类整洁。

您可以更改INotifyDataErrorInfo的实现细节以符合要求。

备注:该代码未经测试。这些代码片段应该可以使用,但是旨在提供一个示例,说明如何实现BaseViewModel) together with接口。