我想知道如何验证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上?
答案 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);
}
我希望这会有所帮助。