通过ViewModel验证模型

本文关键字:模型 验证 ViewModel 通过 | 更新日期: 2023-09-27 18:18:54

我想知道如何做验证mvvm的方式。我在网上看到了很多关于这个话题的内容,但似乎没有什么能涵盖我的情况,但也许我只是以错误的方式接近它。我有一个ValidableModel基类,我的其他模型从它继承:

public abstract class ValidableModel : IDataErrorInfo
{
    protected Type _type;
    protected readonly Dictionary<string, ValidationAttribute[]> _validators;
    protected readonly Dictionary<string, PropertyInfo> _properties;
    public ValidableModel()
    {
        _type = this.GetType();
        _properties = _type.GetProperties().ToDictionary(p => p.Name, p => p);
        _validators = _properties.Where(p => _getValidations(p.Value).Length != 0).ToDictionary(p => p.Value.Name, p => _getValidations(p.Value));
    }
    protected ValidationAttribute[] _getValidations(PropertyInfo property)
    {
        return (ValidationAttribute[])property.GetCustomAttributes(typeof(ValidationAttribute), true);
    }
    public string this[string columnName]
    {
        get
        {
            if (_properties.ContainsKey(columnName))
            {
                var value = _properties[columnName].GetValue(this, null);
                var errors = _validators[columnName].Where(v => !v.IsValid(value)).Select(v => v.ErrorMessage).ToArray();
                Error = string.Join(Environment.NewLine, errors);
                return Error;
            }
            return string.Empty;
        }
    }
    public string Error
    {
        get;
        set;
    }
}
public class SomeModelWithManyFields : ValidableModel {
    [Required(ErrorMessage = "required stuff")]
    public string Stuff { get; set; }
    [Required(ErrorMessage = "another required stuff")]
    public string OtherStuff { get; set; }
    // and so on
}

这只是一个例子-在现实中我的模型有更多的字段(显然:))。在ViewModel中,我公开了模型的整个实例。所有这些看起来都很自然——如果我要暴露每个模型的每个字段,那么我就会有很多重复的代码。最近,我开始怀疑我处理这个问题的方法是否正确。有没有一种方法来验证我的模型没有代码复制,而不是做这个模型,但在ViewModel上?

通过ViewModel验证模型

试试这个,

EntityBase.cs//这个类有验证逻辑,所有想要验证的实体都必须继承这个类

[DataContract(IsReference = true)]
[Serializable]
public abstract class EntityBase : INotifyPropertyChanged, IDataErrorInfo
{
    #region Fields
    //This hold the property name and its value
    private Dictionary<string, object> _values = new Dictionary<string, object>();
    #endregion Fields
    #region Action
    //Subscribe this event if want to know valid changed
    public event Action IsValidChanged;
    #endregion
    #region Protected
    protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
    {
        string propertyName = GetPropertyName(propertySelector);
        SetValue(propertyName, value);
    }
    protected void SetValue<T>(string propertyName, T value)
    {
        if (string.IsNullOrEmpty(propertyName))
            throw new ArgumentException("Invalid property name", propertyName);
        _values[propertyName] = value;
        NotifyPropertyChanged(propertyName);
        if (IsValidChanged != null)
            IsValidChanged();
    }
    protected T GetValue<T>(Expression<Func<T>> propertySelector)
    {
        string propertyName = GetPropertyName(propertySelector);
        return GetValue<T>(propertyName);
    }
    protected T GetValue<T>(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            throw new ArgumentNullException("invalid property name",propertyName);
        object value;
        if (!_values.TryGetValue(propertyName, out value))
        {
            value = default(T);
            _values.Add(propertyName, value);
        }
        return (T)value;
    }
    protected virtual string OnValidate(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
            throw new ArgumentNullException("propertyName","invalid property name");
        string error = string.Empty;
        object value = GetValue(propertyName);
        //Get only 2 msgs
        var results = new List<ValidationResult>(2);
        bool result = Validator.TryValidateProperty(value,new ValidationContext(this, null, null){MemberName = propertyName},results);
        //if result have errors or for the first time dont set errors
        if (!result && (value == null || ((value is int || value is long) && (int)value == 0) || (value is decimal && (decimal)value == 0)))
            return null;
        if (!result)
        {
            ValidationResult validationResult = results.First();
            error = validationResult.ErrorMessage;
        }
        return error;
    }
    #endregion Protected
    #region PropertyChanged
    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler == null)
            return;
        var e = new PropertyChangedEventArgs(propertyName);
        handler(this, e);
    }
    protected void NotifyPropertyChanged<T>(Expression<Func<T>> propertySelector)
    {
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged == null)
            return;
        string propertyName = GetPropertyName(propertySelector);
        propertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion PropertyChanged
    #region Data Validation
    string IDataErrorInfo.Error
    {
        get
        {
            throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
        }
    }
    string IDataErrorInfo.this[string propertyName]
    {
        get { return OnValidate(propertyName); }
    }
    #endregion Data Validation
    #region Privates
    private static string GetPropertyName(LambdaExpression expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression == null)
        {
            throw new InvalidOperationException();
        }
        return memberExpression.Member.Name;
    }
    private object GetValue(string propertyName)
    {
        object value = null;
        if (!_values.TryGetValue(propertyName, out value))
        {
            PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
            if (propertyDescriptor == null)
                throw new ArgumentNullException("propertyName","invalid property");
            value = propertyDescriptor.GetValue(this);
            if (value != null)
                _values.Add(propertyName, value);
        }
        return value;
    }
    #endregion Privates
    #region Icommand Test
    public bool IsValid
    {
        get
        {
            if (_values == null)
                return true;
            //To validate each property which is in _values dictionary
            return _values
                .Select(property => OnValidate(property.Key))
                .All(errorMessages => errorMessages != null && errorMessages.Length <= 0);
        }
    }
    #endregion Icommand Test
}

订单实体

    public class OrderEntity:EntityBase
{
    [Required(ErrorMessage="Name is Required")]
    public string Name
    {
        get { return GetValue(() => Name); }
        set { SetValue(() => Name, value); }
    }
    [Required(ErrorMessage = "OrderNumber is Required")]
    public string OrderNumber
    {
        get { return GetValue(() => OrderNumber); }
        set { SetValue(() => OrderNumber, value); }
    }
    [Required(ErrorMessage = "Quantity is Required")]
    [Range(20,75,ErrorMessage="Quantity must be between 20 and 75")]
    public int Quantity
    {
        get { return GetValue(() => Quantity); }
        set { SetValue(() => Quantity, value); }
    }
    public short Status { get; set; }
}

ViewModel:

public class ViewModel : INotifyPropertyChanged
{
    public ViewModel()
    {
        Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
        Order.IsValidChanged += Order_IsValidChanged;
    }
    void Order_IsValidChanged()
    {
        if (SaveCommand != null)//RaiseCanExecuteChanged so that Save button disable on error 
            SaveCommand.RaiseCanExecuteChanged();
    }
    OrderEntity order;
    public OrderEntity Order
    {
        get { return order; }
        set { order = value; OnPropertychanged("Order"); }
    }
    MyCommand saveCommand;
    public MyCommand SaveCommand
    {
        get { return saveCommand ?? (saveCommand = new MyCommand(OnSave, () => Order != null && Order.IsValid)); }
    }
    void OnSave(object obj)
    {
        //Do save stuff here
    }
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertychanged(string propName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }
}

xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }
}

xaml

    <StackPanel>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="4"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="4"></RowDefinition>
        <RowDefinition Height="auto"></RowDefinition>
        <RowDefinition Height="4"></RowDefinition>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto"/>
        <ColumnDefinition Width="4"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
        <TextBlock Text="Order Name" Grid.Row="0" Grid.Column="0"/>
        <TextBox Text="{Binding Order.Name, ValidatesOnDataErrors=True}" Grid.Row="0" Grid.Column="2"/>
    <TextBlock Text="Order Number" Grid.Row="2" Grid.Column="0"/>
        <TextBox Text="{Binding Order.OrderNumber, ValidatesOnDataErrors=True}" Grid.Row="2" Grid.Column="2"/>
    <TextBlock Text="Quantity" Grid.Row="4" Grid.Column="0"/>
        <TextBox Text="{Binding Order.Quantity, ValidatesOnDataErrors=True}" Grid.Row="4" Grid.Column="2"/>
</Grid>
    <Button Command="{Binding SaveCommand}" Content="Save"/>
</StackPanel>

您可以尝试测试这段代码,如果它符合您的需要。目前它适用于PropertyChange,但是我们可以做一些改变,可以使它为bot PropertyChange或一些按钮点击工作。已经凌晨三点了,我得睡觉了。

更新使用ValidationExtension从ViewModel验证

public static class ValidationExtension
{
    public static void ValidateObject<T>(this T obj) where T : INotifyErrorObject
    {
        if (obj == null)
            throw new ArgumentNullException("object to validate cannot be null");
        obj.ClearErrors();//clear all errors
        foreach (var item in GetProperties(obj))
        {
            obj.SetError(item.Name, string.Join(";", ValidateProperty(obj,item).ToArray())); //Set or remove error
        }
    }
    public static void ValidateProperty<T>(this T obj,string propName) where T : INotifyErrorObject
    {
        if (obj == null || string.IsNullOrEmpty(propName))
            throw new ArgumentNullException("object to validate cannot be null");
        var propertyInfo = GetProperty(propName, obj);
        if (propertyInfo != null)
        {
            obj.SetError(propertyInfo.Name, string.Join(";", ValidateProperty(obj,propertyInfo).ToArray())); //Set or remove error
        }
    }
    public static IEnumerable<string> ValidateProperty<T>(this T obj,PropertyInfo propInfo)
    {
        if (obj == null || propInfo == null)
            throw new ArgumentNullException("object to validate cannot be null");
        var results = new List<ValidationResult>();
        if (!Validator.TryValidateProperty(propInfo.GetValue(obj), new ValidationContext(obj, null, null) { MemberName = propInfo.Name }, results))
            return results.Select(s => s.ErrorMessage);
        return Enumerable.Empty<string>();
    }
    static IEnumerable<PropertyInfo> GetProperties(object obj)
    {
        return obj.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0).Select(p => p);
    }
    static PropertyInfo GetProperty(string propName, object obj)
    {
        return obj.GetType().GetProperties().FirstOrDefault(p =>p.Name==propName && p.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0);
    }
}

EntityBase

public interface INotifyErrorObject : INotifyPropertyChanged, IDataErrorInfo
{
      void SetError(string propertyName, string error);
      void ClearErrors();
}
public class EntityBaseBase : INotifyErrorObject
{
  Dictionary<string, string> validationErrors;
public void SetError(string propName, string error)
{ 
    string obj=null;
    if (validationErrors.TryGetValue(propName, out obj))
    {
        if (string.IsNullOrEmpty(error)) //Remove error
            validationErrors.Remove(propName);
        else if (string.CompareOrdinal(error, obj) == 0) //if error is same as previous return
            return;
        else
            validationErrors[propName] = error; //set error
    }
    else if (!string.IsNullOrEmpty(error))
        validationErrors.Add(propName, error);
    RaisePropertyChanged(propName);
}
public void ClearErrors()
{
    var properties = validationErrors.Select(s => s.Value).ToList();
    validationErrors.Clear();
    //Raise property changed to reflect on UI
    foreach (var item in properties)
    {
        RaisePropertyChanged(item);
    }
}
public EntityBaseBase()
{
    validationErrors = new Dictionary<string, string>();
}  
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propName)
{
    if (PropertyChanged != null && !string.IsNullOrEmpty(propName))
        PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
public string Error
{
    get { throw new NotImplementedException(); }
}
public string this[string columnName]
{
    get 
    {
        string obj=null;
        if (validationErrors.TryGetValue(columnName, out obj))
            return obj;
        else
            return null;
    }
}
}

实体
        public class OrderEntity : EntityBaseBase
    {
        string name;
        [Required(ErrorMessage = "Name is Required")]
        public string Name
        {
            get { return name; }
            set { name = value; RaisePropertyChanged("Name"); }
        }
        string orderNumber;
        [Required(ErrorMessage = "OrderNumber is Required")]
        public string OrderNumber
        {
            get { return orderNumber; }
            set { orderNumber = value; RaisePropertyChanged("OrderNumber"); }
        }
        int quantity;
        [Required(ErrorMessage = "Quantity is Required")]
        [Range(20, 75, ErrorMessage = "Quantity must be between 20 and 75")]
        public int Quantity
        {
            get { return quantity; }
            set { quantity = value; RaisePropertyChanged("Quantity"); }
        }
        public short Status { get; set; }
    }

视图模型

    public class ViewModel : INotifyPropertyChanged
{
    public ViewModel()
    {
        Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
    }
    OrderEntity order;
    public OrderEntity Order
    {
        get { return order; }
        set { order = value; OnPropertychanged("Order"); }
    }
    MyCommand saveCommand;
    public MyCommand SaveCommand
    {
        get { return saveCommand ?? (saveCommand = new MyCommand(OnSave, () => Order != null)); }
    }
    //ValidateObject on Some button Command
    void OnSave(object obj)
    {
        Order.ValidateObject();
    }
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertychanged(string propName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }
}

xaml和xaml.cs同上。秩序。ValidateObject在SaveCommand上验证对象。现在,如果你想从ViewModel上验证PropertyChange那么你的ViewModel将不得不监听PropertyChanged事件的顺序并将不得不调用ValidateProperty的ValidationExtension,如

        public ViewModel()
    {
        Order = new OrderEntity { Name = "someOrder",OrderNumber="my order", Quantity = 23 };
        Order.PropertyChanged += (o, args) => ((INotifyErrorObject)o).ValidateProperty(args.PropertyName);
    }