Click here to Skip to main content
15,861,168 members
Articles / Desktop Programming / Universal Windows Platform

UWP Form Validation with Calcium

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
22 Jan 2017CPOL9 min read 15.9K   194   3   4
Implementing synchronous and asynchronous form validation for XAML based apps.

Image 1

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:

C#
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.

C#
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:

C#
[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:

C#
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.

C#
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.

C#
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"));
                     }

                     /* Simulate asynchronous validation. */
                     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

XML
<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:

C#
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:

C#
bool IsSubmitEnabled(object arg)
{
       return !HasErrors;
}

The Submit method validates the properties and displays a simple message using Calcium’s DialogService, as shown:

C#
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:

C#
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

C#
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

C#
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));
              }

              /* Prevents access to internal closure warning. */
              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

C#
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:

 

C#
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

C#
async Task<ValidationCompleteEventArgs> BeginGetPropertyErrorsFromValidator(string propertyName)
{
       Func<object> propertyFunc;

       lock (propertyDictionaryLock)
       {
              if (!propertyDictionary.TryGetValue(propertyName, out propertyFunc))
              {
                     /* No property registered with that name. */
                     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

C#
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

C#
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

  • First published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.

Daniel is the developer behind several acclaimed mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions

 
QuestionJammer Likes this A LOT TOO Pin
Jammer14-Feb-17 8:19
Jammer14-Feb-17 8:19 
AnswerRe: Jammer Likes this A LOT TOO Pin
Daniel Vaughan16-Feb-17 10:38
Daniel Vaughan16-Feb-17 10:38 
QuestionPete likes this a lot. Pin
Pete O'Hanlon23-Jan-17 0:07
subeditorPete O'Hanlon23-Jan-17 0:07 
AnswerRe: Pete likes this a lot. Pin
Daniel Vaughan23-Jan-17 1:51
Daniel Vaughan23-Jan-17 1:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.