通过ViewModel验证模型

时间:2014-08-19 19:12:18

标签: c# wpf validation mvvm

我想知道如何验证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上?

1 个答案:

答案 0 :(得分:2)

试试这个,

EntityBase.cs //此类具有验证逻辑,您要验证的所有entites必须继承此类

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

视图模型:

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或点击一些按钮。已经凌晨3点了,我得睡觉了。

  
    

更新使用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与上述相同。 Order.ValidateObject验证SaveCommand上的对象。现在,如果你想从ViewModel对PropertyChange进行验证,那么你的ViewModel将不得不调用Order的PropertyChanged事件,并且必须调用ValidationExtension的ValidateProperty,如

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

我希望这会有所帮助。