C# 使用MVVM进行正确的验证

C# 使用MVVM进行正确的验证,c#,wpf,validation,mvvm,C#,Wpf,Validation,Mvvm,警告:非常长和详细的职位 好的,使用MVVM时在WPF中进行验证。我现在读了很多东西,看了很多问题,尝试了很多方法,但在某个时候,每件事都感觉有点不对劲,我真的不知道如何正确地做™. 理想情况下,我希望在视图模型中使用;我就是这么做的。但是,有不同的方面使得此解决方案不是整个验证主题的完整解决方案 形势 让我们采用下面的简单形式。正如你所见,这没什么特别的。我们只有两个文本框,分别绑定到视图模型中的string和int属性。此外,我们还有一个绑定到ICommand的按钮 因此,对于验证,我们现

警告:非常长和详细的职位

好的,使用MVVM时在WPF中进行验证。我现在读了很多东西,看了很多问题,尝试了很多方法,但在某个时候,每件事都感觉有点不对劲,我真的不知道如何正确地做™.

理想情况下,我希望在视图模型中使用;我就是这么做的。但是,有不同的方面使得此解决方案不是整个验证主题的完整解决方案

形势 让我们采用下面的简单形式。正如你所见,这没什么特别的。我们只有两个文本框,分别绑定到视图模型中的
string
int
属性。此外,我们还有一个绑定到
ICommand
的按钮

因此,对于验证,我们现在有两个选择:

  • 只要文本框的值发生变化,我们就可以自动运行验证。因此,当用户输入无效内容时,会得到即时响应。
    • 当出现任何错误时,我们可以进一步禁用按钮
  • 或者,我们只能在按下按钮时显式运行验证,然后显示所有错误(如果适用)。显然,我们不能在这里禁用错误按钮
  • 理想情况下,我希望实现选项1。对于已激活的正常数据绑定,这是默认行为。因此,当文本更改时,绑定会更新源代码并触发该属性的
    IDataErrorInfo
    验证;错误会报告回视图。到目前为止还不错

    视图模型中的验证状态 有趣的是让视图模型或本例中的按钮知道是否存在任何错误。按照
    IDataErrorInfo
    的工作方式,它主要是向视图报告错误。因此,视图可以很容易地查看是否存在任何错误,显示错误,甚至使用显示注释。此外,验证总是针对单个属性进行

    因此,让视图模型知道何时有错误,或者验证是否成功,是一件棘手的事情。一个常见的解决方案是简单地触发视图模型本身中所有属性的
    IDataErrorInfo
    验证。这通常使用单独的
    IsValid
    属性来完成。这样做的好处是,还可以轻松地用于禁用命令。缺点是,这可能会过于频繁地对所有属性运行验证,但大多数验证应该足够简单,不会影响性能。另一个解决方案是使用验证记住哪些属性产生了错误,并只检查这些属性,但这似乎有点过于复杂,在大多数情况下都是不必要的

    底线是,这可以很好地工作
    IDataErrorInfo
    为所有属性提供验证,我们只需在视图模型本身中使用该接口就可以对整个对象运行验证。介绍问题:

    有约束力的例外情况 视图模型使用实际类型作为其属性。因此在我们的示例中,integer属性是一个实际的
    int
    。但是,视图中使用的文本框本机仅支持文本。因此,当绑定到视图模型中的
    int
    时,数据绑定引擎将自动执行类型转换,或者至少会尝试。如果可以在用于数字的文本框中输入文本,则很有可能其中并不总是有有效数字:因此数据绑定引擎将无法转换并抛出
    格式异常

    在视图方面,我们可以很容易地看到这一点。来自绑定引擎的异常会被WPF自动捕获并显示为错误,甚至不需要启用,而setter中抛出的异常则需要启用。不过,错误消息确实有一个通用文本,因此这可能是一个问题。我已经通过使用处理程序解决了这个问题,检查抛出的异常并查看源属性,然后生成一个不太通用的错误消息。所有这些都封装到我自己的绑定标记扩展中,因此我可以拥有我需要的所有默认值

    因此,景色不错。用户犯了错误,看到了一些错误反馈,可以纠正它。但是,视图模型已丢失。由于绑定引擎引发异常,因此源从未更新。因此,视图模型仍然是旧值,而不是显示给用户的值,并且
    IDataErrorInfo
    验证显然不适用

    更糟糕的是,视图模型没有好的方法知道这一点。至少,我还没有找到解决这个问题的好办法。可能的情况是将视图报告返回给视图模型,说明存在错误。这可以通过将属性数据绑定回视图模型来实现(这是不可能直接实现的),因此视图模型可以首先检查视图的状态

    另一个选项是将在
    Binding.UpdateSourceExceptionFilter
    中处理的异常中继到视图模型,这样它也会收到通知。视图模型甚至可以为绑定提供一些接口来报告这些事情,允许定制错误消息,而不是泛型错误消息。但这将创建从视图到视图模型的更强耦合,这是我通常希望避免的

    另一个“解决方案”是去掉所有类型化属性,使用普通的
    string
    属性,并在视图模型中进行转换。这显然会将所有验证转移到视图模型,但也意味着数据绑定引擎通常会处理大量的重复工作。此外,它还会改变视图模型的语义。对我来说,视图是为视图模型构建的,而不是相反的。当然,视图模型的设计取决于我们想象视图要做什么,但仍然存在一些问题
     if (bindingGroup.CommitEdit())
         SaveEverything();
    
    object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
    {
        BindingExpression expr = (bindExpression as BindingExpression);
        if (expr.DataItem is IReceivesBindingErrorInformation)
        {
            ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
        }
    
        // check for FormatException and produce a nicer error
        // ...
     }
    
    HashSet<string> bindingErrors = new HashSet<string>();
    void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
    {
        bindingErrors.Add(path);
    }
    
    protected ObservableCollection<string> errors = new ObservableCollection<string>();
    
    public virtual ObservableCollection<string> Errors
    {
        get { return errors; }
    }
    
    protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();
    
    public ObservableCollection<string> ExternalErrors
    {
        get { return externalErrors; }
    }
    
    public virtual bool HasError
    {
        get { return Errors != null && Errors.Count > 0; }
    }
    
    public override ObservableCollection<string> Errors
    {
        get
        {
            errors = new ObservableCollection<string>();
            errors.AddUniqueIfNotEmpty(this["Name"]);
            errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
            errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
            errors.AddRange(ExternalErrors);
            return errors;
        }
    }
    
    public override string this[string propertyName]
    {
        get
        {
            string error = string.Empty;
            if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
            else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
            else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
            return error;
        }
    }
    
    private void ValidateUniqueName(Genre genre)
    {
        string errorMessage = "The genre name must be unique";
        if (!IsGenreNameUnique(genre))
        {
            if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
        }
        else genre.ExternalErrors.Remove(errorMessage);
    }
    
    private bool IsGenreNameUnique(Genre genre)
    {
        return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
    }
    
    public object FooObject { get; set; } // Implement INotifyPropertyChanged
    
    public int Foo
    {
        get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
    }
    
    public override string this[string propertyName]
    {
        get
        {
            string error = string.Empty;
            if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) 
                error = "Please enter a whole number for the Foo field.";
            ...
            return error;
        }
    }
    
    container.AddHandler(Validation.ErrorEvent, Container_Error);
    
    ...
    
    void Container_Error(object sender, ValidationErrorEventArgs e) {
        ...
    }
    
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
       public event PropertyChangedEventHandler PropertyChanged;
    
       public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
       {
          if (PropertyChanged != null)
             PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
       }
    }
    
    public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
    {
        //This one is part of INotifyDataErrorInfo interface which I will not use,
        //perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; 
    
        //will hold the errors found in validation.
        public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();
    
        //the actual value - notice it is 'int' and not 'string'..
        private int storageCapacityInBytes;
    
        //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
        //we want to consume what the user throw at us and validate it - right? :)
        private string storageCapacityInBytesWrapper;
    
        //This is a property to be served by the View.. important to understand the tactic used inside!
        public string StorageCapacityInBytes
        {
           get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
           set
           {
              int result;
              var isValid = int.TryParse(value, out result);
              if (isValid)
              {
                 storageCapacityInBytes = result;
                 storageCapacityInBytesWrapper = null;
                 RaisePropertyChanged();
              }
              else
                 storageCapacityInBytesWrapper = value;         
    
              HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
           }
        }
    
        //Manager for the dictionary
        private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                if (isValid)
                {
                    if (ValidationErrors.ContainsKey(propertyName))
                        ValidationErrors.Remove(propertyName);
                }
                else
                {
                    if (!ValidationErrors.ContainsKey(propertyName))
                        ValidationErrors.Add(propertyName, validationErrorDescription);
                    else
                        ValidationErrors[propertyName] = validationErrorDescription;
                }
            }
        }
    
        // this is another part of the interface - will be called automatically
        public IEnumerable GetErrors(string propertyName)
        {
            return ValidationErrors.ContainsKey(propertyName)
                ? ValidationErrors[propertyName]
                : null;
        }
    
        // same here, another part of the interface - will be called automatically
        public bool HasErrors
        {
            get
            {
                return ValidationErrors.Count > 0;
            }
        }
    }
    
    public struct Failable<T>
    {
        public T Value { get; private set; }
        public string Text { get; private set; }
        public bool IsValid { get; private set; }
    
        public Failable(T value)
        {
            Value = value;
    
            try
            {
                var converter = TypeDescriptor.GetConverter(typeof(T));
                Text = converter.ConvertToString(value);
                IsValid = true;
            }
            catch
            {
                Text = String.Empty;
                IsValid = false;
            }
        }
    
        public Failable(string text)
        {
            Text = text;
    
            try
            {
                var converter = TypeDescriptor.GetConverter(typeof(T));
                Value = (T)converter.ConvertFromString(text);
                IsValid = true;
            }
            catch
            {
                Value = default(T);
                IsValid = false;
            }
        }
    }
    
    public class StringToFailableConverter<T> : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value.GetType() != typeof(Failable<T>))
                throw new InvalidOperationException("Invalid value type.");
    
            if (targetType != typeof(string))
                throw new InvalidOperationException("Invalid target type.");
    
            var rawValue = (Failable<T>)value;
            return rawValue.Text;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value.GetType() != typeof(string))
                throw new InvalidOperationException("Invalid value type.");
    
            if (targetType != typeof(Failable<T>))
                throw new InvalidOperationException("Invalid target type.");
    
            return new Failable<T>(value as string);
        }
    }
    
    public static class Failable
    {
        public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
        public static StringToFailableConverter<double> DoubleConverter { get; private set; }
    
        static Failable()
        {
            Int32Converter = new StringToFailableConverter<Int32>();
            DoubleConverter = new StringToFailableConverter<Double>();
        }
    }
    
    public Failable<int> NumberValue
    {
        //Custom logic along with validation
        //using IsValid property
    }
    
    <TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>