Introduction
In this article, you see how to perform both synchronous and asynchronous form validation in a UWP app. You see how set up viewmodel properties for validation, both declaratively and programmatically. You look at how to define your validation logic in a single method or to have the validation logic reside on a remote server. You also see how to use UWP dictionary indexers to bind form validation errors to a control in your view. You then explore the innerworkings of the validation system, and see how Calcium decouples validation from your viewmodel so that it can be used with any object implementing INotifyPropertyChanged
.
Background
A few years ago, I wrote about the various approaches to form validation for XAML based apps, which is in both editions of my Windows Phone Unleased books. The book chapter covers the various approaches available at the time: simple exception based validation and the more flexible INotifyDataErrorInfo
interface based approach, which was introduced in Silverlight 4. During the writing of that chapter, I developed an API that makes it pretty easy not only to perform synchronous validation, but also asynchronous validation. I’ve since made the code available in the Calcium MVVM framework, which is available for WPF, Windows Phone, UWP, Xamarin Android and iOS. Just search for Calcium on NuGet, or type "Install-Package Calcium" from the package manager console. The downloadable sample contains a simple example of how to perform synchronous and asynchronous form validation for a UWP app using Calcium.
Getting Started
The Calcium framework relies on an IoC container and a few IoC registerations, for logging, loosely couple messaging, settings, and all those things you end up needing in a reasonably complex application. If you know what you’re doing, you can ‘manually’ initialize Calcium’s infrastructure. But, the easy way to initialize Calcium is to use the Outcoder.ApplicationModel.CalciumSystem
class.
In the downloadable example project I began by initializing Calcium in the App.xaml.cs file, like so:
var calciumSystem = new CalciumSystem();
calciumSystem.Initialize();
The call to Initialize is performed in the OnLaunched
method of the App
class. The call takes place if the rootFrame is null, which indicates that the app is launching from a cold start and that CalciumSystem.Initialize has not been called before. Calcium needs to be initialized after the rootFrame is created so that it can perform navigation monitoring; which among other things allows Calcium to automatically save the state of your view models.
NOTE: If Calcium can’t find your root Frame
object, then it will schedule a retry five times before giving up. The retry gives your app time to initialize.
In Calcium, you subclass the Outcoder.ComponentModel.ViewModelBase
class to provide access to a host of services for your own viewmodels, such as property change notifications, state management, and of course property validation.
In the sample, the MainPageViewModel
is the viewmodel for the MainPage
class.
public class MainPageViewModel : ViewModelBase
{
…
}
MainPageViewModel
contains several properties. We’ll demonstrate the validation using two properties: TextField1
and TextField2
. The ViewModelBase
class contains an Assign
method, which automatically raises property change events on the UI thread and signals to the validation system, that a value has changed. See the following:
[Validate]
public string TextField1
{
get
{
return textField1;
}
set
{
Assign(ref textField1, value);
}
}
As an aside, several years ago, I settled on the method name ‘Assign’ rather than ‘Set’ because Set is a Visual Basic keyword. Some other frameworks use Set, but I didn't want to get tangled up with non-CLS compliance.
You nominate a property for validation by either decorating it with the [Validate]
attribute, or by calling the ViewModelBase
class’s AddValidationProperty
method, as shown:
public MainPageViewModel()
{
AddValidationProperty(() => TextField2);
…
}
When a PropertyChanged
event occurs, the validation system kicks into action, and calls either your overridden GetPropertyErrors
method, or your overridden ValidateAsync
method; depending on whether you need asynchronous support, such as when calling upon a remote server for validation.
If you only require synchronous validation, in that you don’t need to validate data remotely or using potentially high latency database calls, then override the GetPropertyErrors
. See Listing 1.
GetPropertyErrors
returns an IEnumerable<datavalidationerrors>
, which is merged, behind the scenes, into a dictionary that can be used to display errors in your view. You see more on that in a moment.
When creating a DataValidationError
you’ll notice that an integer value is associated with each error. For TextField1
, in the example, 1 indicates that the ID of the error is 1. Because a field may have multiple validation errors, an ID allows us to refer specifically to a particular validation error if we need to, without relying on the error message text.
Listing 1. MainPageViewModel GetPropertyErrors method.
protected override IEnumerable<DataValidationError> GetPropertyErrors(
string propertyName, object value)
{
var result = new List<DataValidationError>();
switch (propertyName)
{
case nameof(TextField1):
if (textField1.Length < 5)
{
result.Add(new DataValidationError(
1, "Length must be greater than 5"));
}
break;
case nameof(TextField2):
if (TextField2 != "Foo")
{
result.Add(new DataValidationError(
2, "Content should be 'Foo'"));
}
break;
}
return result;
}
Asynchronous validation is performed in the same manner. But instead of overriding the ViewModelBase
’s GetPropertyErrors
method, you override the ValidateAsync
method. See Listing 2.
NOTE: Overriding the ValidateAsync
method causes the GetPropertyErrors
method to be ignored.
In the example, we simulate a potentially long running asynchronous validation by awaiting Task.Delay
. The result is returned using a ValidationCompleteEventArgs
instance, which can contain either the list of validation for the specified property, or an error; indicating that validation failed for some reason.
Listing 2. MainViewModel ValidateAsync method.
public override async Task<ValidationCompleteEventArgs> ValidateAsync(
string propertyName, object value)
{
var errorList = new List<DataValidationError>();
switch (propertyName)
{
case nameof(TextField1):
if (textField1.Length < 5)
{
TextField1Busy = true;
errorList.Add(new DataValidationError(
1, "Length must be greater than 5"));
}
await Task.Delay(1000);
TextField1Busy = false;
break;
case nameof(TextField2):
…
break;
}
var result = new ValidationCompleteEventArgs(propertyName, errorList);
return result;
}
Sometimes there is interdependency between properties and you need to validate multiple properties together. To achieve that, override the ViewModelBase
class’s ValidateAllAsync
method, and call the underlying DataErrorNotifier.SetPropertyErrors
method for each property with errors.
Displaying Validation Errors
It’s important to display validation errors in a way that doesn’t disrupt the workflow of the user. In the sample, I display validation errors beneath each text field using a ListBox
. See Listing 3. Fortunately the ListBox
happily expands and contracts depending on whether it has any errors to display.
The ItemSource
property of the ListBox is bound to the collection of validation errors for the associated property. A dictionary indexer is used to retrieve the collection using the property name as a key.
NOTE: Binding to Dictionary indexers is a new feature in Windows 10 Anniversary Update and won’t work in earlier versions of Windows 10. Please also note that it has some limitations. One such limitation is that binding to a custom implementation of an IReadOnlyDictionary
failed in my tests. The binding infrastructure seemed to expect a concrete ReadOnlyDictionary
instance. For this reason, I had to rework the DataErrorNotifier
class to support binding to the ValidationErrors
property. One other important limitation is that dictionary indexer only support string literals.
ProgressRing
controls are used to indicate that validation is in progress. Each ProgressRing
control’s IsActive
property is bound to a corresponding viewmodel property.
The TextBox
control only updates its binding source property when it loses input focus. Unlike WPF and other XAML implementations, TextBox
doesn’t provide an UpdateSourceTrigger
binding property. For that reason, I use Calcium’s UpdateSourceTriggerExtender
attached property, which pushes the update through to the source property whenever the TextBox
’s Text
property changes.
The UpdateSourceTriggerExtender
attached property works with TextBox
and PasswordBox
controls.
NOTE: Calcium’s UpdateSourceTriggerExtender
attached property does not work with x:Bind expressions. You must use the traditional Binding markup extension.
The ViewModelBase
class exposes a ValidationErrors
property.
Listing 3. MainPage.xaml Excerpt
<TextBlock Text="TextField1" Style="{StaticResource LabelStyle}" />
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextField1, Mode=TwoWay}"
xaml:UpdateSourceTriggerExtender.UpdateSourceOnTextChanged="True"
Style="{StaticResource TextFieldStyle}" />
<ProgressRing IsActive="{x:Bind ViewModel.TextField1Busy, Mode=OneWay}"
Style="{StaticResource ProgressRingStyle}" />
</StackPanel>
<ListBox
ItemsSource="{x:Bind ViewModel.ValidationErrors['TextField1']}"
Style="{StaticResource ErrorListStyle}" />
<TextBlock Text="TextField2" Style="{StaticResource LabelStyle}" />
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding TextField2, Mode=TwoWay}"
xaml:UpdateSourceTriggerExtender.UpdateSourceOnTextChanged="True"
Style="{StaticResource TextFieldStyle}" />
<ProgressRing IsActive="{x:Bind ViewModel.TextField2Busy, Mode=OneWay}"
Style="{StaticResource ProgressRingStyle}" />
</StackPanel>
<ListBox
ItemsSource="{x:Bind ViewModel.ValidationErrors['TextField2']}"
Style="{StaticResource ErrorListStyle}" />
<Button Command="{x:Bind ViewModel.SubmitCommand}"
Content="Submit"
Margin="0,12,0,0"/>
The form contains a Submit button, which simulates sending the data off to some remote service. The button is bound to the MainViewModel
class’s SubmitCommand
.
SubmitCommand
is a Calcium DelegateCommand
. It is instantiated in the MainPageViewModel
’s constructor, like so:
public MainPageViewModel()
{
AddValidationProperty(() => TextField2);
submitCommand = new DelegateCommand(Submit, IsSubmitEnabled);
ErrorsChanged += delegate { submitCommand.RaiseCanExecuteChanged(); };
}
The two arguments supplied to the submitCommand
constructor is an action, IsSubmitEnabled
, which determines if the button should be enabled; and a Submit
action, which performs the main action of the button.
IsSubmitEnabled
simply checks whether the form has any errors, like so:
bool IsSubmitEnabled(object arg)
{
return !HasErrors;
}
The Submit
method validates the properties and displays a simple message using Calcium’s DialogService
, as shown:
async void Submit(object arg)
{
await ValidateAllAsync(false);
if (HasErrors)
{
return;
}
await DialogService.ShowMessageAsync("Form submitted.");
}
NOTE: The Calcium framework also supports the notion of asynchronous commands, but that is outside the scope of this article.
The ViewModelBase
class’s ErrorsChanged
event gives us the opportunity to refresh the enabled state of the submitCommand
. The DelegateCommand
’s RaiseCanExecuteChanged
method invokes the IsSubmitEnabled
method, and the IsEnabled
property of the button is updated.
Behind the Scenes
When Calcium’s ViewModelBase
class is instantiated, it creates an instance of a DataErrorNotifier
; passing itself to the DataErrorNotifier
class’s constructor. See Listing 4.
DataErrorNotifier
requires an object that implements INotifyPropertyChanged
, and an object that implements Calcium’s IValidateData
interface. IValidateData
contains a single method definition:
Task<ValidationCompleteEventArgs> ValidateAsync(string memberName, object value);
ValidateAsync
is the method we implemented earlier, and is responsible for validating each property.
By separating the INotifyPropertyChanged
owner and the IValidateData
object, we effectively decouple validation from the viewmodel. So, if we liked, we could provide a completely separate validation subsystem within our application. In addition, validation is not restricted to just viewmodels. You could use the DataErrorNotifier
to provide validation for any object implementing INotifyPropertyChanged
.
Listing 4. DataErrorNotifier constructor
public DataErrorNotifier(INotifyPropertyChanged owner, IValidateData validator)
{
this.validator = ArgumentValidator.AssertNotNull(validator, "validator");
this.owner = ArgumentValidator.AssertNotNull(owner, "owner");
owner.PropertyChanged += HandleOwnerPropertyChanged;
ReadValidationAttributes();
}
Reading the validation attributes for the owner object involves retrieving the PropertyInfo
object for each property in the class, and using the GetCustomAttributes
to look for the existence of the ValidateAttribute
attribute. See Listing 5. If a property is decorated with a Validate
attribute it is added to the list of validated properties.
Listing 5. DataErrorNotifier ReadValidationAttributes method
void ReadValidationAttributes()
{
var properties = owner.GetType().GetTypeInfo().DeclaredProperties;
foreach (PropertyInfo propertyInfo in properties)
{
var attributes = propertyInfo.GetCustomAttributes(
typeof(ValidateAttribute), true);
if (!attributes.Any())
{
continue;
}
if (!propertyInfo.CanRead)
{
throw new InvalidOperationException(string.Format(
"Property {0} must have a getter to be validated.",
propertyInfo.Name));
}
PropertyInfo info = propertyInfo;
AddValidationProperty(
propertyInfo.Name, () => info.GetValue(owner, null));
}
}
If you choose to add your properties using the AddValidationProperty
method, then a delegate is created using the MethodInfo
object’s CreateDelegate
. See Listing 6. Creating a delegate rather than relying on the PropertyInfo
object’s GetValue
method may improve performance. This is because retrieving or setting a value via reflection is notoriously slow. Please note, however, that I’ve yet to implement that for the Validate
attribute.
Listing 6. DataErrorNotifier AddValidationProperty method
public void AddValidationProperty(Expression<Func<object>> expression)
{
PropertyInfo propertyInfo = PropertyUtility.GetPropertyInfo(expression);
string name = propertyInfo.Name;
MethodInfo getMethodInfo = propertyInfo.GetMethod;
Func<object> getter = (Func<object>)getMethodInfo.CreateDelegate(
typeof(Func<object>),
this);
AddValidationProperty(name, getter);
}
When a property changes on the owner object. That is, when it’s PropertyChanged
event is raised, the DataErrorNotifier
object’s BeginGetPropertyErrorsFromValidator
is called, as shown:
async void HandleOwnerPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e?.PropertyName == null)
{
return;
}
await BeginGetPropertyErrorsFromValidator(e.PropertyName);
}
If the property that changed is to be validated then the BeginGetPropertyErrorsFromValidator
calls ValidateAsync
on the IValidateData
instance. See Listing 7.
Listing 7. DataErrorNotifier BeginGetPropertyErrorsFromValidator method
async Task<ValidationCompleteEventArgs> BeginGetPropertyErrorsFromValidator(string propertyName)
{
Func<object> propertyFunc;
lock (propertyDictionaryLock)
{
if (!propertyDictionary.TryGetValue(propertyName, out propertyFunc))
{
return new ValidationCompleteEventArgs(propertyName);
}
}
var result = await validator.ValidateAsync(propertyName, propertyFunc());
ProcessValidationComplete(result);
return result;
}
Before BeginGetPropertyErrorsFromValidator
returns its result, the ValidationCompleteEventArgs
object is passed to the ProcessValidationComplete
method, which adds any resulting validation errors to the errors collection via the SetPropertyErrors
method. See Listing 8.
Listing 8. DataErrorNotifier ProcessValidationComplete method
void ProcessValidationComplete(ValidationCompleteEventArgs e)
{
try
{
if (e.Exception == null)
{
SetPropertyErrors(e.PropertyName, e.Errors);
}
}
catch (Exception ex)
{
var log = Dependency.Resolve<ILog>();
log.Debug("Unable to set property error.", ex);
}
}
SetPropertyErrors
populates an ObservableCollection<DataValidationError>
with the validation errors. See Listing 9. An ObservableCollection
is used because it makes displaying validation changes in the UI a snap. You need only bind to the property name in the ValidationErrors
dictionary, as we saw back in Listing 3.
Listing 9. DataErrorNotifier SetPropertyErrors method
public void SetPropertyErrors(
string propertyName, IEnumerable<DataValidationError> dataErrors)
{
ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
bool raiseEvent = false;
lock (errorsLock)
{
bool created = false;
var errorsArray = dataErrors as DataValidationError[] ?? dataErrors?.ToArray();
int paramErrorCount = errorsArray?.Length ?? 0;
if ((errorsField == null || errorsField.Count < 1)
&& paramErrorCount < 1)
{
return;
}
if (errorsField == null)
{
errorsField = new Dictionary<string, ObservableCollection<DataValidationError>>();
created = true;
}
bool listFound = false;
ObservableCollection<DataValidationError> list;
if (created || !(listFound = errorsField.TryGetValue(propertyName, out list)))
{
list = new ObservableCollection<DataValidationError>();
}
if (paramErrorCount < 1)
{
if (listFound)
{
list?.Clear();
raiseEvent = true;
}
}
else
{
var tempList = new List<DataValidationError>();
if (errorsArray != null)
{
foreach (var dataError in errorsArray)
{
if (created || list.SingleOrDefault(
e => e.Id == dataError.Id) == null)
{
tempList.Add(dataError);
raiseEvent = true;
}
}
}
list.AddRange(tempList);
errorsField[propertyName] = list;
}
}
if (raiseEvent)
{
OnErrorsChanged(propertyName);
}
}
Conclusion
In this article, you saw how to perform both synchronous and asynchronous form validation in a UWP app. You saw how set up viewmodel properties for validation, both declaratively and programmatically. You looked at how to define your validation logic in a single method or to have the validation logic reside on a remote server. You also saw how to use UWP dictionary indexers to bind form validation errors to a control in your view. You then explored the innerworkings of the validation system, and saw how Calcium decouples validation from your viewmodel so that it can be used with any object implementing INotifyPropertyChanged
.
I hope you find this project useful. If so, then please rate it and/or leave feedback below.
History
January 22 2017