WPF Commanding and Data Annotations Validation
by Peter Daukintis
I recently ran into the need to create a simple WPF app with a data form front end and which should provide some simple validation feedback to the user. I thought this would be pretty straightforward using the WPF commanding support and Data Annotations to provide some simple validation metadata. I wanted to use an M-V-VM approach so that I could unit test my View Model code. This is a screenshot of the prototype app:
The save button should remain disabled until the fields have all been entered correctly and also some feedback should be available to the user about which field is preventing the validation as below.
First I started by drawing on a few external resources; the Relay Command (see http://msdn.microsoft.com/en-us/magazine/dd419663.aspx) from Josh Smith and I also used a type safe INotifyPropertyChanged extension method to save me from typos when using INPC.
public static class Extensions { public static void Raise(this PropertyChangedEventHandler handler, Expression<Func<object, object>> x) { if (handler == null) return; var body = x.Body as MemberExpression; if (body == null) throw new ArgumentException("'x' should be a member expression"); var vmExpression = body.Expression as ConstantExpression; if (vmExpression == null) return; var vmlambda = Expression.Lambda(vmExpression); var vmFunc = vmlambda.Compile(); var vm = vmFunc.DynamicInvoke(); string propertyName = body.Member.Name; var e = new PropertyChangedEventArgs(propertyName); handler(vm, e); } }
So, I began by creating my View Model class and binding it to the UI similar to the following:
public class ViewModel : INotifyPropertyChanged { public ICommand StoreCommand { get; set; } public ICommand BrowseCommand { get; set; } private string _title = string.Empty; public string Title { get { return _title; } set { if (_title != value) { _title = value; PropertyChanged.Raise(t => Title); } } }
The only code I had in my code-behind file was the following to databind the view model to the ui:
public MainWindow() { InitializeComponent(); Loaded += MainWindow_Loaded; } void MainWindow_Loaded(object sender, RoutedEventArgs e) { DataContext = new ViewModel(); }
So, I created two commands, store and browse, which I bound to the xaml like this:
<Button Content="Save" Grid.Row="5" HorizontalAlignment="Right" Margin="0,0,8,6" Name="button1" Width="91" Height="29" VerticalAlignment="Bottom" Grid.Column="1" MinHeight="20" Command="{Binding Path=StoreCommand}" CommandParameter="{Binding}"/>
Now, using the Relay Command it is simple to wire up the commands to methods in my view model for Execute and canExecute:
public ViewModel() { StoreCommand = new RelayCommand(ExecuteSave, CanExecuteSave); BrowseCommand = new RelayCommand(ExecuteBrowse, CanExecuteBrowse); }
The main method here is CanExecuteSave since this will determine when my save button is enabled/disabled. So, in order for this method to return true I must have fully valid data. Although there are a few variations in how you approach validation in WPF it felt natural to me to try out using Data Annotations since I have used these for validation in Asp.Net MVC applications. These work by marking the data properties with attributes which are defined in the System.ComponentModel.DataAnnotations namespace. So, I just added a reference to this assembly and marked up my view model’s properties, like this:
private string _artist = string.Empty; [StringLength(50, MinimumLength = 1, ErrorMessage = "Artist must be between 1 and 50 characters long.")] public string Artist { get { return _artist; } set { if (_artist != value) { _artist = value; PropertyChanged.Raise(t => Artist); } } }
You can either use the provided attributes, such as StringLength, Required, etc. or you can provide a custom attribute as I did for validating the existence of a file.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false)] public class FileExistsAttribute : ValidationAttribute { public override bool IsValid(object value) { return File.Exists(value as string); } }
Now, with the metadata in place my CanExecuteSave method can be implemented by reflecting over the attributes and calling the IsValid method on each one. This is achieved by calling the following method:
public static class Validator { public static IEnumerable<ErrorInfo> GetErrors(object instance) { var metadataAttrib = instance.GetType().GetCustomAttributes(typeof (MetadataTypeAttribute), true).OfType <MetadataTypeAttribute>().FirstOrDefault(); var buddyClassOrModelClass = metadataAttrib != null ? metadataAttrib.MetadataClassType : instance.GetType(); var buddyClassProperties = TypeDescriptor.GetProperties(buddyClassOrModelClass).Cast<PropertyDescriptor>(); var modelClassProperties = TypeDescriptor.GetProperties(instance.GetType()).Cast<PropertyDescriptor>(); return from buddyProp in buddyClassProperties join modelProp in modelClassProperties on buddyProp.Name equals modelProp.Name from attribute in buddyProp.Attributes.OfType<ValidationAttribute>() where !attribute.IsValid(modelProp.GetValue(instance)) select new ErrorInfo(buddyProp.Name, attribute.FormatErrorMessage(string.Empty), instance); }
And simply calling it,
public bool CanExecuteSave(object parameter) { IEnumerable<ErrorInfo> errorInfos = Validator.GetErrors(this); return errorInfos.Count() < 1; }
This takes care of enabling and disabling my Save button, so I just need to provide validation feedback for the data fields and I’m done. So, I need another static method on my Validator class:
public static IEnumerable<ErrorInfo> GetErrors(object instance, string property, string value) { var info = instance.GetType().GetProperty(property); return (from va in info.GetCustomAttributes(true).OfType<ValidationAttribute>() where !va.IsValid(value) select new ErrorInfo(property, va.FormatErrorMessage(string.Empty), instance)).ToList(); }
This I can call on a particular property and pass it an arbitrary value and it will return me an error report!
I wrapped a call to this up in a private method on the view model.
private void ValidateProperty(string value, string propertyName) { IEnumerable<ErrorInfo> errorInfos = Validator.GetErrors(this, propertyName, value); if (errorInfos.Count() > 0) { throw new ApplicationException(errorInfos.First().FormatErrorMessage); } }
And, insert a call to this inside each of my properties that I have annotated like the following:
private string _title = string.Empty; [StringLength(50, MinimumLength = 1, ErrorMessage = "Title must be between 1 and 50 characters long.")] public string Title { get { return _title; } set { if (_title != value) { _title = value; ValidateProperty(value, "Title"); PropertyChanged.Raise(t => Title); } } }
I don’t like the non-type-safe way I pass in the property name to the ValidateProperty call so in future I may use the same trick as for the Raise extension method, but for now…
A few more tweaks to the xaml and we’re done:
<TextBox Style="{StaticResource ResourceKey=textBoxStyle}" Grid.Column="1" Height="23" HorizontalAlignment="Stretch" Name="titleTextBox" VerticalAlignment="Center" Margin="6,13.5,8,13.5"> <TextBox.Text> <Binding Path="Title" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True"> <Binding.ValidationRules> <ExceptionValidationRule /> <!--<source:RequiredRule />--> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
The ExceptionvalidationRule sets up the validation to respond to exceptions thrown in the property setter. Note, I had previously experimented with custom ValidationRule derived classes to provide the validation but I never got this to work as well for my scenario.
it just remains to define the textBoxStyle used as the style can be used to control the visual feedback given to the user:
<Window.Resources> <Style x:Key="textBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="FontSize" Value="12"></Setter> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> </Window.Resources>
And that is it!
Not quite as straightforward as I expected but the result is a pretty impressively flexible solution.
Technorati Tags: Data,Annotations,Validation,feedback,user,unit,View,Model,code,prototype,Relay,Command,magazine,Josh,Smith,extension,method,INPC,Raise,PropertyChangedEventHandler,handler,Expression,Func,Body,MemberExpression,ArgumentException,member,ConstantExpression,Lambda,Compile,Name,PropertyChangedEventArgs,ViewModel,ICommand,StoreCommand,BrowseCommand,_title,Title,InitializeComponent,sender,RoutedEventArgs,DataContext,commands,Button,Content,Save,Grid,HorizontalAlignment,Margin,Width,VerticalAlignment,Bottom,Column,Path,CommandParameter,wire,Execute,RelayCommand,ExecuteSave,CanExecuteSave,ExecuteBrowse,CanExecuteBrowse,Although,System,ComponentModel,DataAnnotations,reference,_artist,StringLength,MinimumLength,ErrorMessage,Artist,characters,custom,existence,AttributeUsage,AttributeTargets,Field,Parameter,AllowMultiple,FileExistsAttribute,ValidationAttribute,IsValid,File,Validator,IEnumerable,ErrorInfo,GetErrors,instance,GetType,GetCustomAttributes,MetadataTypeAttribute,OfType,FirstOrDefault,MetadataClassType,TypeDescriptor,GetProperties,Cast,PropertyDescriptor,equals,GetValue,FormatErrorMessage,Count,info,ToList,error,ApplicationException,TextBox,Style,StaticResource,ResourceKey,Stretch,Center,Text,UpdateSourceTrigger,NotifyOnValidationError,True,ValidationRules,ExceptionValidationRule,RequiredRule,Note,ValidationRule,classes,scenario,Resources,TargetType,Type,Setter,FontSize,Value,Trigger,HasError,ToolTip,RelativeSource,Static,Self,ErrorContent,result,solution,Extensions,methods,variations,Errors,metadata,vmExpression,vmlambda,vmFunc,propertyName,xaml,bool,metadataAttrib,buddyClassOrModelClass,buddyClassProperties,modelClassProperties,buddyProp,modelProp,errorInfos,textBoxStyleWindows Live Tags: Data,Annotations,Validation,feedback,user,unit,View,Model,code,prototype,Relay,Command,magazine,Josh,Smith,extension,method,INPC,Raise,PropertyChangedEventHandler,handler,Expression,Func,Body,MemberExpression,ArgumentException,member,ConstantExpression,Lambda,Compile,Name,PropertyChangedEventArgs,ViewModel,ICommand,StoreCommand,BrowseCommand,_title,Title,InitializeComponent,sender,RoutedEventArgs,DataContext,commands,Button,Content,Save,Grid,HorizontalAlignment,Margin,Width,VerticalAlignment,Bottom,Column,Path,CommandParameter,wire,Execute,RelayCommand,ExecuteSave,CanExecuteSave,ExecuteBrowse,CanExecuteBrowse,Although,System,ComponentModel,DataAnnotations,reference,_artist,StringLength,MinimumLength,ErrorMessage,Artist,characters,custom,existence,AttributeUsage,AttributeTargets,Field,Parameter,AllowMultiple,FileExistsAttribute,ValidationAttribute,IsValid,File,Validator,IEnumerable,ErrorInfo,GetErrors,instance,GetType,GetCustomAttributes,MetadataTypeAttribute,OfType,FirstOrDefault,MetadataClassType,TypeDescriptor,GetProperties,Cast,PropertyDescriptor,equals,GetValue,FormatErrorMessage,Count,info,ToList,error,ApplicationException,TextBox,Style,StaticResource,ResourceKey,Stretch,Center,Text,UpdateSourceTrigger,NotifyOnValidationError,True,ValidationRules,ExceptionValidationRule,RequiredRule,Note,ValidationRule,classes,scenario,Resources,TargetType,Type,Setter,FontSize,Value,Trigger,HasError,ToolTip,RelativeSource,Static,Self,ErrorContent,result,solution,Extensions,methods,variations,Errors,metadata,vmExpression,vmlambda,vmFunc,propertyName,xaml,bool,metadataAttrib,buddyClassOrModelClass,buddyClassProperties,modelClassProperties,buddyProp,modelProp,errorInfos,textBoxStyle
Comments