In Windows UWP XAML/C# apps, developers will discover that input controls do not support DataAnnotation
or ExceptionValidationRule
or IDataErrorInfo
or INotifyDataErrorInfo
or BindingValidationError
. Even if the platform included these capabilities, there are significant limitations to each that make them a limiting option for application with significant real-world data validation requirements.
These validation libraries provide UWP developers a comprehensive solution to data validation that solves problems in a realistic and usable way. The associated sample application demonstrates the use and syntax. If you have feedback or a bug to report, do it here: https://github.com/Windows-XAML/Template10/issues.
To get started with validation, your models need to inherit Template10.Validation.ValidatableModelBase
. This will include an implementation of INotifyPropertyChanged
in case you need it in your logic. Should your poco classes (models) not support inheriting from a new base class, then this validation library will not work for you. The enhancements to your model(s) will include:
public ObservableCollection<string> Errors { get; }
public bool IsDirty { get; }
public bool IsValid { get; }
public ObservableDictionary<string, IProperty> Properties { get; }
public Action<IValidatableModel> Validator { get; set; }
public void MarkAsClean();
public void RaisePropertyChanged([CallerMemberName] string propertyName = null);
public void Revert();
public bool Validate();
protected T Read<T>([CallerMemberName] string propertyName = null);
protected void Write<T>(T value, [CallerMemberName] string propertyName = null, bool validateAfter = true);
In addition to inheriting from ValidatableModelBase
, both the setter and getters of your model properties will need to change to the following syntax in order to take part in the underlying change tracking mechanism.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString() => $"{FirstName} {LastName}";
}
public class User : Template10.Validation.ValidatableModelBase
{
public int Id { get; set; }
public string FirstName { get { return Read<string>(); } set { Write(value); } }
public string LastName { get { return Read<string>(); } set { Write(value); } }
public override string ToString() => $"{FirstName} {LastName}";
}
Once you have implemented these changes, you need to set the Validator
property of your model - typically when they are created or just before they are exposed in your view-model. (Note: It is not required that you use the MVVP design pattern to use this library - but if you don't you're probably building your app all wrong). You would set your validator something like this:
var user = new User
{
FirstName = "Jerry",
LastName = "Nixon",
Validator = i =>
{
var u = i as User;
if (string.IsNullOrEmpty(u.FirstName))
{
u.Properties[nameof(u.FirstName)].Errors.Add("First name is required");
}
if (string.IsNullOrEmpty(u.LastName))
{
u.Properties[nameof(u.LastName)].Errors.Add("Last name is required");
}
},
};
Notice in the code above how you are adding one or more errors to the Errors
property of the model's property; you are not adding to the Errors
property of the model. This allows errors on a field level. Property errors are automatically propagated to the model-level property Errors
. Any changes to that list will be overwritten.
Once the validation logic is injected into the model, you can call the model's Validate()
method to assess the current state of the model. You can repeatedly call Validate()
as the workflow in your app requires. If the validation logic has significant cost, calling Validate()
too frequently could have a performance cost to your app.
Above you saw how each property has an Errors
property. In addition, there is a Value
property and these others:
ObservableCollection <string> Errors { get; }
bool IsDirty { get; }
bool IsOriginalSet { get; }
bool IsValid { get; }
T Value { get; set; }
T OriginalValue { get; set; }
event EventHandler ValueChanged;
void MarkAsClean();
void Revert();
You will see that most of these properties are managed automatically by the base class. Calling MarkAsClean()
or Revert()
impacts IsDirty
and Value
respectively. That being said, you can also use them in tandem with the control wrapper that ships in the library. You would use it like this:
<validate:ControlWrapper PropertyName="FirstName">
<TextBox Width="{StaticResource FieldWidth}"
Header="First Name"
Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</validate:ControlWrapper>
By default the visual looks like this:
The visual definition of the wrapper is defined in the library (https://github.com/Windows-XAML/Template10.Validation/blob/master/Library/Themes/Generic.xaml) and you can override it simply by changing the Template
property of the wrapper. Here's how you might do it:
<validate:ControlWrapper PropertyName="FirstName">
<validate:ControlWrapper.Template>
<ControlTemplate TargetType="validate:ControlWrapper">
<StackPanel DataContext="{TemplateBinding Property}">
<TextBlock Text="IsValid" />
<TextBlock Text="{Binding IsValid}" />
<TextBlock Text="Errors" />
<TextBlock Text="{Binding Errors.Count}" />
<ContentPresenter Content="{TemplateBinding Content}" />
</StackPanel>
</ControlTemplate>
</validate:ControlWrapper.Template>
<TextBox Width="{StaticResource FieldWidth}"
Header="First Name"
Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</validate:ControlWrapper>
In the XAML above the ControlTemplate
is created in-line, but could easily be moved to a resource and re-used in multiple controls. It also demonstrates how every control could have a separate look, if desired. The XAML above would render a look something like this:
I realize that's a silly use of the control, but it demonstrates an easy starting place.
I hope you enjoy Template 10 Validation!