I'm not a big fan of attribute-based validation because I find there are often complex requirements that involve asynchronous operations that often break the attribute model. I prefer more fluent validation using rules that can be shared between the client and server and can handle asynchronous scenarios.
For those times when I do have to use a class with attribute validations, it doesn't make sense to ignore them and throw in an entirely new model. Many developers are aware that you can create a "magic" form using the DataForm
control, or a "magic" grid with the DataGrid
control, and they will automatically honor validation attributes. If you are using your own view models and wiring your own forms, you can still take advantage of data annotations but it requires a little extra work.
In this example I'll use WCF RIA services because it can wire the example up quickly, it automatically projects the data annotations, and because I get a lot of people who ask me for examples using WCF RIA. The example here will work just as well if you use your own POCO classes with annotations in a shared library and send them across the wire with a REST service using JSON.
First, a simple class to encapsulate some data that is decorated with the annotations:
public class Contact { [Key] public int Id { get; set; } [Required] public string FirstName { get; set; } public string LastName { get; set; } [Required] [RegularExpression(@"^[a-zA-Z0-9._%-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$", ErrorMessage = "Email is invalid.")] public string Email { get; set; } }
Next, a simple domain service that exposes what WCF RIA wants to think it is talking to a database layer. Note I am stubbing the CRUD to allow for edits on the client but don't really care about processing them for this example. The one query always returns an example with my information in it.
[EnableClientAccess()] public class ContactDomainService : DomainService { public Contact GetContact(int contactId) { return new Contact { Id = contactId, FirstName = "Jeremy", LastName = "Likness", Email = "[email protected]" }; } [Insert] public void InsertContact(Contact contact) { } [Delete] public void DeleteContact(Contact contact) { } [Update] public void UpdateContact(Contact contact) { } }
This is where the beauty of WCF RIA services comes into play, and I won't deny it: build the application, and you're ready to go with grabbing and manipulating the data on the client. Before you build the view model you can create a helper class that uses the data annotations validation to process the attributes on the entity. While I wouldn't normally overload the same class to also take on the work of a command, the helper will also implement ICommand
to demonstrate how it can be used to also control whether or not the user is able to save the form based on validations (this keeps the example simple - normally I would have a second command class that interacts with the validation helper).
The bulk of the class is straightforward. It is typed to a class that implements property change notification, and registers for this (it also exposes a method to release the registration to avoid issues with memory leaks). Whenever a property changes, it uses reflection to grab the value and then passes all of the information over to the data annotations validation class. That class will throw an exception if the validation fails, and the helper traps this and uses it to keep track of properties that have validation errors before re-throwing it for the Silverlight validation system to use. I'm keeping it simple here and using strings but you can tweak it based on this post.
public class ValidationHelper<T> : ICommand where T : class, INotifyPropertyChanged { private readonly T _instance; private readonly Dictionary<string,bool> _hasErrors = new Dictionary<string, bool>(); public ValidationHelper(T instance) { _instance = instance; _instance.PropertyChanged += _InstancePropertyChanged; } public void Release() { if (_instance != null) { _instance.PropertyChanged -= _InstancePropertyChanged; } } private void _InstancePropertyChanged(object sender, PropertyChangedEventArgs e) { _hasErrors[e.PropertyName] = false; var value = typeof (T) .GetProperty(e.PropertyName) .GetGetMethod() .Invoke(_instance, null); try { Validator.ValidateProperty( value, new ValidationContext(_instance, null, null) { MemberName = e.PropertyName }); } catch { _hasErrors[e.PropertyName] = true; throw; } finally { RaiseCanExecuteChange(); } } }
The implementation of the command interface takes one more step. Because a user might change some text and then click submit before validation fires, the command implementation will validate the entire object before determining whether or not it is safe to submit. The command is conditioned by the existence of any errors in the error dictionary. Note that the execute methods uses a different approach that doesn't throw exceptions and simply flags the individual errors generated by the call.
public bool CanExecute(object parameter) { return _hasErrors.Keys.All(key => !_hasErrors[key]); } public void Execute(object parameter) { var errors = new List<ValidationResult>(); if (Validator.TryValidateObject( _instance, new ValidationContext(_instance, null, null), errors)) { MessageBox.Show("Success!"); } else { foreach (var member in errors.SelectMany(error => error.MemberNames)) { _hasErrors[member] = true; } RaiseCanExecuteChange(); } }
The view model keeps an instance of the validation helper and tracks changes to the current contact, spinning up a new helper as needed. It also exposes an edit flag to disable editing while it is loading the data.
public class MainViewModel : INotifyPropertyChanged { private ValidationHelper<Contact> _helper; private Contact _contact; private readonly string[] _properties = new[] {"FirstName", "LastName", "Email"}; protected Contact CurrentContact { get { return _contact; } set { if (_helper != null) { _helper.Release(); _helper = null; } _contact = value; if (_contact != null) { CanEdit = true; _helper = new ValidationHelper<Contact>(value); RaisePropertyChange("SaveCommand"); } RaisePropertyChange("Contact"); foreach(var property in _properties) { RaisePropertyChange(property); } } } private bool _canEdit; public bool CanEdit { get { return _canEdit; } set { _canEdit = value; RaisePropertyChange("CanEdit"); } } }
It exposes the validation helper as a command and also projects all of the properties on the underlying contact object:
public ICommand SaveCommand { get { return _helper; } } public string FirstName { get { return CurrentContact == null ? string.Empty : CurrentContact.FirstName; } set { CurrentContact.FirstName = value; } } public string LastName { get { return CurrentContact == null ? string.Empty : CurrentContact.LastName; } set { CurrentContact.LastName = value; } } public string Email { get { return CurrentContact == null ? string.Empty : CurrentContact.Email; } set { CurrentContact.Email = value; } }
In the constructor, it either creates a design-time instance or uses WCF RIA services to load an instance of the contact record:
public MainViewModel() { if (DesignerProperties.IsInDesignTool) { CurrentContact = new Contact { Id = 1, Email = "[email protected]", FirstName = "Jeremy", LastName = "Likness" }; return; } var ctx = new ContactDomainContext(); var query = ctx.GetContactQuery(1); ctx.Load( query, contact => { CurrentContact = contact.Entities.Single(); }, null); }
The Xaml to wire this up is straightforward - here is an example combination of a label and a text box:
<TextBlock Text="Last Name:" Grid.Row="1"/> <TextBox Text="{Binding Path=LastName, Mode=TwoWay, ValidatesOnExceptions=True, NotifyOnValidationError=True}" IsEnabled="{Binding Path=CanEdit}" Grid.Row="1" Grid.Column="1"/>
That's it - run the application and edit the fields. You'll see all of the attribute validations come across nicely. If you delete the first name, then click the submit button without tabbing out of the field, the validations will still fire and the submit button will disable itself until you fix the error. As you can see, it's very straightforward to tap into existing attribute-based validations. If you are implementing one of the newer interfaces like INotifyDataErrorInfo
, you can use the "try" methods on the validator to avoid throwing exceptions, and parse the results to set errros on the underlying interface instead.
You can download the source for this example here (don't forget to set the web project as the startup project and the aspx page as the start page).