In this chapter, we will learn about validations. We will also look at a clean way to do validation with what WPF bindings already support but tying it into MVVM components.
When your application starts accepting data input from end users you need to consider validating that input.
Make sure it conforms to your overall requirements.
WPF has some great builds and features in the binding system for validating input and you can still leverage all those features when doing MVVM.
Keep in mind that the logic that supports your validation and defines what rules exist for what properties should be part of the Model or the ViewModel, not the View itself.
You can still use all the ways of expressing validation that are supported by WPF data binding including −
In general, INotifyDataErrorInfo is recommended and was introduced to WPF .net 4.5 and it supports querying the object for errors associated with properties and it also fixes a couple of deficiencies with all the other options. Specifically, it allows asynchronous validation. It allows properties to have more than one error associated with them.
Let’s take a look at an example in which we will add validation support to our input view, and in large application you will probably need this a number of places in your application. Sometimes on Views, sometimes on ViewModels and sometimes on these helper objects there are wrappers around model objects.
It’s a good practice for putting the validation support in a common base class that you can then inherit from different scenarios.
The base class will support INotifyDataErrorInfo so that that validation gets triggered when properties change.
Create add a new class called ValidatableBindableBase. Since we already have a base class for a property change handling, let’s derive the base class from it and also implement the INotifyDataErrorInfo interface.
Following is the implementation of ValidatableBindableBase class.
using System; using System.Collections.Generic; using System.ComponentModel; //using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; namespace MVVMHierarchiesDemo { public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo { private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>(); public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { }; public System.Collections.IEnumerable GetErrors(string propertyName) { if (_errors.ContainsKey(propertyName)) return _errors[propertyName]; else return null; } public bool HasErrors { get { return _errors.Count > 0; } } protected override void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null) { base.SetProperty<T>(ref member, val, propertyName); ValidateProperty(propertyName, val); } private void ValidateProperty<T>(string propertyName, T value) { var results = new List<ValidationResult>(); //ValidationContext context = new ValidationContext(this); //context.MemberName = propertyName; //Validator.TryValidateProperty(value, context, results); if (results.Any()) { //_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList(); } else { _errors.Remove(propertyName); } ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName)); } } }
Now add AddEditCustomerView and AddEditCustomerViewModel in respective folders. Following is the code of AddEditCustomerView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d = "http://schemas.microsoft.com/expression/blend/2008" xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> </Grid.RowDefinitions> <Grid x:Name = "grid1" HorizontalAlignment = "Left" DataContext = "{Binding Customer}" Margin = "10,10,0,0" VerticalAlignment = "Top"> <Grid.ColumnDefinitions> <ColumnDefinition Width = "Auto" /> <ColumnDefinition Width = "Auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> <RowDefinition Height = "Auto" /> </Grid.RowDefinitions> <Label Content = "First Name:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "0" VerticalAlignment = "Center" /> <TextBox x:Name = "firstNameTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "0" Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Last Name:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "1" VerticalAlignment = "Center" /> <TextBox x:Name = "lastNameTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "1" Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Email:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "2" VerticalAlignment = "Center" /> <TextBox x:Name = "emailTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "2" Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> <Label Content = "Phone:" Grid.Column = "0" HorizontalAlignment = "Left" Margin = "3" Grid.Row = "3" VerticalAlignment = "Center" /> <TextBox x:Name = "phoneTextBox" Grid.Column = "1" HorizontalAlignment = "Left" Height = "23" Margin = "3" Grid.Row = "3" Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}" VerticalAlignment = "Center" Width = "120" /> </Grid> <Grid Grid.Row = "1"> <Button Content = "Save" Command = "{Binding SaveCommand}" HorizontalAlignment = "Left" Margin = "25,5,0,0" VerticalAlignment = "Top" Width = "75" /> <Button Content = "Add" Command = "{Binding SaveCommand}" HorizontalAlignment = "Left" Margin = "25,5,0,0" VerticalAlignment = "Top" Width = "75" /> <Button Content = "Cancel" Command = "{Binding CancelCommand}" HorizontalAlignment = "Left" Margin = "150,5,0,0" VerticalAlignment = "Top" Width = "75" /> </Grid> </Grid> </UserControl>
Following is the AddEditCustomerViewModel implementation.
using MVVMHierarchiesDemo.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.ViewModel { class AddEditCustomerViewModel : BindableBase { public AddEditCustomerViewModel() { CancelCommand = new MyIcommand(OnCancel); SaveCommand = new MyIcommand(OnSave, CanSave); } private bool _EditMode; public bool EditMode { get { return _EditMode; } set { SetProperty(ref _EditMode, value);} } private SimpleEditableCustomer _Customer; public SimpleEditableCustomer Customer { get { return _Customer; } set { SetProperty(ref _Customer, value);} } private Customer _editingCustomer = null; public void SetCustomer(Customer cust) { _editingCustomer = cust; if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged; Customer = new SimpleEditableCustomer(); Customer.ErrorsChanged += RaiseCanExecuteChanged; CopyCustomer(cust, Customer); } private void RaiseCanExecuteChanged(object sender, EventArgs e) { SaveCommand.RaiseCanExecuteChanged(); } public MyIcommand CancelCommand { get; private set; } public MyIcommand SaveCommand { get; private set; } public event Action Done = delegate { }; private void OnCancel() { Done(); } private async void OnSave() { Done(); } private bool CanSave() { return !Customer.HasErrors; } } }
Following is the implementation of SimpleEditableCustomer class.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MVVMHierarchiesDemo.Model { public class SimpleEditableCustomer : ValidatableBindableBase { private Guid _id; public Guid Id { get { return _id; } set { SetProperty(ref _id, value); } } private string _firstName; [Required] public string FirstName { get { return _firstName; } set { SetProperty(ref _firstName, value); } } private string _lastName; [Required] public string LastName { get { return _lastName; } set { SetProperty(ref _lastName, value); } } private string _email; [EmailAddress] public string Email { get { return _email; } set { SetProperty(ref _email, value); } } private string _phone; [Phone] public string Phone { get { return _phone; } set { SetProperty(ref _phone, value); } } } }
When the above code is compiled and executed, you will see the following window.
When you press the Add Customer button you will see the following view. When the user leaves any field empty, then it will become highlighted and the save button will become disabled.