Captain’s blog, stardate 2015.09.30. All the crew's Tricorders were successfully upgraded to Windows 10. We are eager to continue to seek out new life and new civilizations. At the moment, the registration app for new intelligent life forms is being rewritten for the Universal Windows Platform. Here’s how the prototype looks like:
As you see, the app is complaining whenever the user types or selects an illegal value in one of the input controls. Here's how we do this.
The System.ComponentModel.DataAnnotations members
The engineering team of the .NET core put the old System.ComponentModel namespace in it. This namespace hosts the classic series of Data Annotations - a set of attributes that apply to properties of models and describe validation rules. Here’s a list of data annotations that may be useful in UWP apps. The names speak for themselves:
- Required
- CreditCard
- EmailAddress
- PhoneNumber
- Range
- MinLength
- MaxLenght
- RegularExpression
- Enumeration
- URL
These attributes go back a long time: classes such as ScaffoldColumn and UIHintAttribute were created for ASP.NET WebForms (the Dynamic Data feature in Visual Studio 2008). Later these annotations were used in the validation infrastructure for the Entity Framework (that’s were the attributes like Key, Association, ConcurrencyCheck and DisplayColumn come from). In the current era, data annotation attributes are often referred to as MVC Validation. But –as already recorded- they’re available for the Universal Windows Platform now. Here’s an example of using some of the out-of-the-box data annotations in a viewmodel. The Name property should be filled, and the Location should consist of a Quadrant (A, B, C, or D) plus a Sector (2 to 5 digits), so they're decorated with a [Required] and a [RegularExpression] respectively:
[Required(ErrorMessage = "Name is required.")]
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
[RegularExpression(@"[ABCD]\d{2,5}", ErrorMessage="Location is Quadrant (A -> D) and Sector (2 -> 5 digits)")]
public string Location
{
get { return location; }
set { SetProperty(ref location, value); }
}
The Prism 6 validation infrastructure
Just putting this code in your UWP app is like sending the first away team to a newly discovered planet: nothing will return. The data annotations are part of the class definition, but at runtime there’s actually nothing looking at them. UWP lacks the infrastructure to listen for property changes and immediately validate the new values against the rules that were defined by the attributes. That infrastructure is defined, it's called the Validator, and a corresponding class exists on the UWP. But it only serves as a base class, waiting for your own implementation. So there is room for an engine, but no actual engine.
Well, here's the good news: the UWP Prism 6 team has built us a Warp Drive. It's called the BindableValidator. It applies validation rules that are encoded as data annotation attributes on the entities’ properties. That's just what we were looking for. You’ll need two beam up two NuGet packages to get the entire infrastructure on board: the Prism.Core and Prism.Windows packages.
That was the good news, here's the bad news: the Prism.Windows package is still in the spacedock - not officially released. Now I'm not known for my patience, so I copied the source –we have replicators- of the relevant classes directly into the sample app. Here’s an overview of these classes:
- BindableBase: an implementation of INotifyPropertyChanged.
- BindableValidator: runs validation rules of an entity, and stores a collection of errors of the properties that did not pass validation.
- IValidatableBindableBase: describes validation support for (view)model classes that contain validation rules.
- ValidatableBindableBase: a default implementation of IValidatableBindableBase.
You hook into this validation framework by letting your viewmodels implement the IValidatableBindableBase interface. The easiest way to do do that is to inherit from ValidatableBindableBase:
internal class CivilizationViewModel : ValidatableBindableBase
{
// ...
}
Your class now gets an Errors property, which actually refers to the validator itself. The validation is run on each property change or whenever the ValidateProperties method is called. The Validator also provides an indexer property, that uses the property names as keys and the error list for each property as values. That’s a complex structure, but the XAML syntax allows us to use indexes in binding expressions. Here’s how Starfleet’s new app binds a textbox to the Name property, and a textblock to the first error in its corresponding error list:
<TextBox Text="{Binding Name, Mode=TwoWay}"
Header="Name" />
<TextBlock Text="{Binding Errors[Name][0]}"
Foreground="Red"
HorizontalAlignment="Right" />
Whenever the Name property is changed (by the user through the TextBox, or programmatically), it is validated. If it’s not filled, then Errors[Name] will be updated and appear in the TextBlock. Fascinating.
If you prefer a central errors summary somewhere on the page, then just bind an ItemsControl to Errors.Errors:
<ItemsControl x:Name="ErrorList"
ItemsSource="{Binding Errors.Errors}" />
This is the blue list at the bottom of the app's page.
Building a custom property validator
The list of out-of-the-box validation rules is short, but it’s easy to create your own validation attributes: all you need is to subclass ValidationAttribute and provide an implementation for the IsValid method. Here’s a sample NumericAttribute that checks whether the input can be mapped to an integer value:
/// <summary>
/// Sample custom validation for Integer values.
/// </summary>
public class NumericAttribute: ValidationAttribute
{
public override bool IsValid(object value)
{
// The [Required] attribute should test this.
if (value == null)
{
return true;
}
int result;
return int.TryParse(value.ToString(), out result);
}
}
Here’s how the attribute is used in the viewmodel of the New Civilization Registration Form:
[Numeric(ErrorMessage = "Population should be numeric.")]
public string EstimatedPopulation
{
get { return estimatedPopulation; }
set { SetProperty(ref estimatedPopulation, value); }
}
Observe that the property in that viewmodel is of type String, not Integer. You have to make sure that the binding between the property and the textbox's text in the view doesn’t break.
Comparing two properties
Data annotations are set at property level. What if you want to compare two properties, e.g. to validate a date range? Out of the box, there is only one attribute that compares two properties: Compare. Unfortunately it only checks for equality of two values. You can use it to verify the confirmation of passwords or email addresses.
Here's an example of a validation attribute that compares two DateTime properties, and verifies whether it is a valid range (the validated DateTime should be later than the other one). That 'other' property's name is stored in a property, and looked up at validation time through reflection.Logically speaking, the property with the attribute is compared to the property in the attribute:
/// <summary>
/// Checks whether a DateTime is later than another Property.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class LaterThanPropertyAttribute : ValidationAttribute
{
string propertyName;
public LaterThanPropertyAttribute(string propertyName)
{
this.propertyName = propertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext obj)
{
if (value == null)
{
return ValidationResult.Success;
}
var propertyInfo = obj.ObjectInstance.GetType().GetProperty(this.propertyName);
if (propertyInfo == null)
{
// Should actually throw an exception.
return ValidationResult.Success;
}
dynamic otherValue = propertyInfo.GetValue(obj.ObjectInstance);
if (otherValue == null)
{
return ValidationResult.Success;
}
// Unfortunately we have to use the DateTime type here.
//var compare = ((IComparable<DateTime>)otherValue).CompareTo((DateTime)value);
var compare = otherValue.CompareTo((DateTime)value);
if (compare >= 0)
{
return new ValidationResult(this.ErrorMessage);
}
return ValidationResult.Success;
}
}
Unfortunately I had to use the DateTime type in the code. If you want to compare two integers, then you have to create a new class…
Here’s how the attribute is used to check if the date that a new civilization joined Starfleet comes after the date that it was discovered:
[LaterThanPropertyAttribute("DiscoveryDate", ErrorMessage="Affiliation date should come after date of first contact.")]
public DateTime MembershipDate
{
get { return membershipDate; }
set { SetProperty( ref membershipDate, value); }
}
Observe that this rule is defined on MembershipDate, so it is NOT triggered when the user changes the DiscoveryDate. If you want this to happen, then you have to hook up the validation call in the property’s setter:
public DateTime DiscoveryDate
{
get { return discoveryDate; }
set
{
SetProperty(ref discoveryDate, value);
// This is the right way to link property validations.
Errors.ValidateProperty("MembershipDate");
}
}
What if you want to use more than two properties in a validation rule
(like “the date that a new civilization joined Star Fleet should come
after the date that it was discovered, unless they are time travelers”)?
Well, technically it’s possible, but your source code would look more
like Klingon than C#. We're close to the limits of the property-based validation infrastructure here. If you want more (like comparing with properties of other viewmodels), then you’re in for a Kobayashi Maru [that means: you have to change the source code]. Anyway, by modifying the BindableValidator or by creating a brand new one, you will be able to boldly go where no man has gone before. The current Prism infrastructure provides a solid base for that.
The code
The source code of the sample app is here on GitHub. At this moment it contains a copy of some source files of Prism 6. I will remove these when the official NuGet packages are released.
Live long and prosper!
XAML Brewer