Validate model thru ViewModel

I'm wondering about how to do validation the mvvm way. I saw lots of content on this topic on the web, but nothing seems to cover my situation, but maybe I'm just approaching it the wrong way. I have a ValidableModel base class from which my other models inherit:

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
}

This is just an example - in reality my models have more fields (obviously :) ). Now, in my ViewModel I'm exposing whole instance of my model. All this seemed natural - if I'd be exposing every field of every model then I'd have lots of duplicated code. Recently I started wondering if I'm approaching this problem correct. Is there a way to validate my models without code duplication, and not by doing this on the model, but on the ViewModel?


Try this,

EntityBase.cs //This class has Validation logic and all the entites you want to validate must inherit this class

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

Order Entity

    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>

You can try and test this code if it fit your needs. Currently it works for PropertyChange however we can make some changes can make it to work for bot PropertyChange or on some button click . Its 3:00 am already so i gotta sleep.

Update Validating from ViewModel using ValidationExtension

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

Entity

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

ViewModel

    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 and xaml.cs is same as above. Order.ValidateObject validates the object on SaveCommand. Now if you want to Validate on PropertyChange from ViewModel then your ViewModel will have to Listen PropertyChanged event of Order and will have to call ValidateProperty of ValidationExtension like

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

I hope this will help.

链接地址: http://www.djcxy.com/p/20282.html

上一篇: 在PHP中调用REST API

下一篇: 通过ViewModel验证模型