5

Microsoft MVVM Toolkit

 3 years ago
source link: https://xamlbrewer.wordpress.com/2021/06/07/data-validation-with-the-microsoft-mvvm-toolkit/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

In this article we will walk through a handful of scenarios and techniques for input validation with the ObservableValidator class of Microsoft MVVM Toolkit. We will cover

  • canonical property validation,
  • comparing two (or more) properties,
  • comparing the new value of a property to its previous one,
  • delaying the validation, and
  • preventing to assign an invalid value to a property.

We created a sample app in UWP, it looks like this:

Input Validation in UWP

The System.Component.DataAnnotations is almost as old as the .NET Framework itself. It contains attribute classes to decorate ViewModels and Models with metadata for validation purposes – among other. These attributes describe property validation rules. When the rules are broken, the instance exposes its validation errors via its INotifyDataError members. This validation technique is intensively used in several ASP.NET frameworks and also made its way to Silverlight and WPF. Until very recently, data validation in UWP did not get much attention from Microsoft. Most development teams embedded third-party solutions such as Prism, Template10, or Calcium into their UWP apps, or rolled their own custom solution.

With Microsoft MVVM toolkit there now is a Microsoft provided alternative, and it’s even ready for Reunion. Now that we’re talking about the future: WinUI 3 controls will come with templates that react upon the INotifyDataErrorInfo status of the (View)Model that they are bound to. There is currently a bug that inhibits a deeper dive into this. Nevertheless, the WinUI3 Controls Gallery app already contains a sample page. It’s currently broken, but it reveals a glimpse of the near future of input validation in WinUI:

That’s not a spectacular screenshot, so for reference here’s an example of similar control templates in WPF:

customerrortemplateWPF

While the UI parts of UWP Data Validation are not yet ready for prime time, the supporting Microsoft MVVM Toolkit is fully operational. Welcome to ObservableValidator.

ObservableValidator

The great official documentation teaches us that

‘The ObservableValidator is a base class implementing the INotifyDataErrorInfo interface, providing support for validating properties exposed to other application modules. It also inherits from ObservableObject, so it implements INotifyPropertyChanged and INotifyPropertyChanging as well. It can be used as a starting point for all kinds of objects that need to support both property change notifications and property validation.’.

Microsoft MVVM Toolkit is fully developed in the open, the source code for ObservableValidator is right here. Models and ViewModels that require validation just need to inherit from it, like this:

public class Suspect : ObservableValidator
{
private string _name;
private string _socialSecurityNumber;
// ... there's more
}

The instance will expose its error status through its INotifyDataErrorInfo members. The Views in our sample app have bindings to ErrorsChanged and HasErrors. Check our previous blog post to see how this was done. Were’ reusing its ‘error popup’ approach. Here’s its XAML definition:

<SymbolIcon Symbol="ReportHacked"
Foreground="Red"
Visibility="{x:Bind ViewModel.Suspect.HasErrors, Mode=OneWay}">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ViewModel.Suspect.Errors, Mode=OneWay}"
Foreground="Red" />
</ToolTipService.ToolTip>
</SymbolIcon>

Using existing Validation Attributes

The System.Component.DataAnnotations namespace hosts a huge list of attributes available for the validation of individual properties: required, minimum and maximum length, range, Enum, Regex … These cover most of the usual suspects. There are not too much XAML examples on the market, so when you search for sample code, you’ll probably end up in ASP.NET MVC projects. Don’t worry about that: MVC Models are very similar to MVVM ViewModels.

To implement validation in a class, it suffices to decorate its properties with one or more of these validation attributes and call one of its SetProperty() overloads with true as the third parameter in the property setter. Here’s how our sample app evaluates whether Keyser Söze has a required Name of minimum length and has his SocialSecurityNumber checked against a regular expression:

[Required(
ErrorMessage = "Name is Required")]
[MinLength(
2,
ErrorMessage = "Name should be longer than one character")]
public string Name
{
get => _name;
set => SetProperty(ref _name, value, true);
}
[RegularExpression(
@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$",
ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
get => _socialSecurityNumber;
set => SetProperty(ref _socialSecurityNumber, value, true);
}

The validation is triggered whenever the property gets a new value. All we need in the View is a two-way binding to the property:

<TextBox Text="{x:Bind ViewModel.Suspect.Name, Mode=TwoWay}"
PlaceholderText="Name" />
<TextBox Text="{x:Bind ViewModel.Suspect.SocialSecurityNumber, Mode=TwoWay}"
PlaceholderText="Social Security Number" />

Here’s how our sample app reacts to an evil social security number:

The regular expression correctly refuses a number that starts with 666.

Rolling your own Validation Attributes

It’s easy to roll your own reusable validation attribute: inherit from ValidationAttribute and provide your own implementation of the IsValid() method. The method gets the ValidationContext injected as a parameter, exposing the whole instance that is being validated, not only the decorated property. This allows you to write validation rules over more than one property, like:

  • property a should be less than property b, or
  • if property a has a value, then property b becomes required.

Allow us to mention that there already *is* a validation attribute that compares two properties -the CompareAttribute– but it only checks for equality. It’s used in the confirmation fields for email addresses and passwords (and probably nowhere else)

Our sample app contains a validation rule to compare two dates. It is used in the common ‘if the EndDate is filled out it then should come after StartDate’ scenario.

Let’s first show how the GreaterThan attribute is applied in the ViewModel class:

[GreaterThan (
nameof(StartDate),
"End date should come after start date.")]
public DateTime? EndDate
{
get => _endDate;
set {
SetProperty(ref _endDate, value, true);
}
}

Here’s the implementation of the validation attribute itself. It can be reused across applications:

public sealed class GreaterThanAttribute : ValidationAttribute
{
private string _errorMessage;
public GreaterThanAttribute(string propertyName, string errorMessage)
{
PropertyName = propertyName;
_errorMessage = errorMessage;
}
public string PropertyName { get; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
return ValidationResult.Success;
}
var instance = validationContext.ObjectInstance;
var otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);
if (((IComparable)value).CompareTo(otherValue) > 0)
{
return ValidationResult.Success;
}
return new ValidationResult(_errorMessage);
}
}

Since we’ve created a dependency between the values of Start- and EndDate in the ViewModel, we need to make sure that the validation is triggered by both properties’ changes. That’s why we make a call to ValidateProperty() in the setter of StartDate:

public DateTime? StartDate
{
get => _startDate;
set { SetProperty(ref _startDate, value, true);
ValidateProperty(EndDate, nameof(EndDate));
}
}

In the View, we bound both properties to a Date in a CalendarDatePicker:

<CalendarDatePicker Date="{x:Bind ViewModel.NoDelorean.GetStartDate(), BindBack=ViewModel.NoDelorean.SetStartDate, Mode=TwoWay}"
PlaceholderText="Start Date" />
<CalendarDatePicker Date="{x:Bind ViewModel.NoDelorean.GetEndDate(), BindBack=ViewModel.NoDelorean.SetEndDate, Mode=TwoWay}"
PlaceholderText="End Date" />

[We used {x:Bind} with function bindings to plug in a transformation between the DateTime of the properties and the DateTimeOffset values of the controls.]

Here’s how an invalid instance looks like in the sample app:

Also, please note that this rule does not apply to all ViewModels. Sometimes it should be possible to go back in time:

Using a CustomValidation method

Not every validation rule should be cast in a universally reusable validation attribute class. For a local rule, you can get away with writing a static validation method and applying it to a property via a CustomValidation attribute. Our sample app uses this technique to compare a new value of a property to its old value. The ViewModel represents a CountDown class – the validation rule ensures that we don’t ‘count up’.

The values to compare must have their own fields:

private int _value = 10;
private int _previousValue;

We created a static method that takes the property’s type (int in our Counter case) and a ValidationContext, and returns the result of the validation as a ValidationResult:

public static ValidationResult ValidateValue(
int value,
ValidationContext context)
{
var instance = (Countdown)context.ObjectInstance;
var isValid = value < instance._previousValue;
if (isValid)
{
return ValidationResult.Success;
}
return new ValidationResult("We're not supposed to count up.");
}

Again the validation context gives us access to the instance being validated, so it allows us to compare the new counter value to the old. Here’s how the rule is applied to the Value property:

[CustomValidation(typeof(Countdown), nameof(ValidateValue))]
public int Value
{
get => _value;
set
{
_previousValue = _value;
SetProperty(ref _value, value, true);
}
}

And this is how violating the counter rule looks like in the sample app:

Delaying Validation

Up until now, we always triggered the validation on the assignment of the property. This is not needed or wanted in every scenario or for every property. The third parameter in the SetProperty() call was always set to true in the previous examples. In our next sample, the validation will be triggered by a button click, so we start with a false in the property setter:

[Required(
ErrorMessage = "Name is Required")]
[MinLength(
2,
ErrorMessage = "Name should be longer than one character")]
public string Name
{
get => _name;
set => SetProperty(ref _name, value, false);
}
[RegularExpression(
@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$",
ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
get => _socialSecurityNumber;
set => SetProperty(ref _socialSecurityNumber, value, false);
}

For the sake of simplicity we validate all properties together – there’s a call for this: ValidateAllProperties(). The call is hooked to the button with an instance of MVVM Toolkit’s RelayCommand:

public ICommand ValidateCommand =>
new RelayCommand(() => ValidateAllProperties());

Here’s the binding:

<Button Content="Validate"
Command="{x:Bind ViewModel.SuspectWithDelayedValidation.ValidateCommand, Mode=OneWay}" />

And this is how the result looks like in the app:

There is a Try

In the previous examples we started the validation after the assignment of a property. We were continuously breaking an ancient object oriented programming principle:

it should not be possible to bring a properly encapsulated object into an invalid state via the public interface.

In some scenarios or parts of your app (e.g. in the Models) you should indeed prevent the assignment of invalid values to properties. Fortunately Microsoft MVVM Toolkit comes with TrySetProperty() … there *is* a Try.

TrySetProperty inspects the new value that you (try to) assign, and only succeeds when that value is a valid one. On the upside you’ll never have instances in an invalid state, on the downside you need to provide you own Errors store – instances will never have ‘official’ errors in their INotifyDataErrorInfo members.

In our sample app we decorated the ViewModel with alternative members – a list of ValidationResult instances to store the errors per property and a Boolean that returns whether that list has members:

public class NotYoda : ObservableValidator
{
private List<ValidationResult> _errors = new List<ValidationResult>();
public string Errors => string.Join(Environment.NewLine, from ValidationResult e in _errors select e.ErrorMessage);
// Since HasErrors is not virtual:
public bool ErrorsHaveI => Errors.Length > 0;
// More, there is...
}

In the property setters we first clean up the previous messages for the property, then call the TrySetProperty, use its output parameter to update our custom error list, and then notify the custom error property changes:

[Required(
ErrorMessage = "Name is Required")]
[MinLength(
2,
ErrorMessage = "Name should be longer than one character")]
public string Name
{
get => _name;
set
{
_errors.RemoveAll(v => v.MemberNames.Contains(nameof(Name)));
TrySetProperty(ref _name, value, out IReadOnlyCollection<ValidationResult> errors);
_errors.AddRange(errors);
OnPropertyChanged(nameof(Errors));
OnPropertyChanged(nameof(ErrorsHaveI));
}
}
[RegularExpression(
@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$",
ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
get => _socialSecurityNumber;
set
{
_errors.RemoveAll(v => v.MemberNames.Contains(nameof(SocialSecurityNumber)));
TrySetProperty(ref _socialSecurityNumber, value, out IReadOnlyCollection<ValidationResult> errors);
_errors.AddRange(errors);
OnPropertyChanged(nameof(Errors));
OnPropertyChanged(nameof(ErrorsHaveI));
}
}

In the View, the bindings for the properties are not different from the other samples:

<TextBox Text="{x:Bind ViewModel.NotYoda.SocialSecurityNumber, Mode=TwoWay}"
PlaceholderText="Social Security Number" />
<TextBlock Text="{x:Bind ViewModel.NotYoda.SocialSecurityNumber, Mode=TwoWay}" />

The error icon is of course bound to the custom error properties:

<SymbolIcon Symbol="ReportHacked"
Foreground="Red"
Visibility="{x:Bind ViewModel.NotYoda.ErrorsHaveI, Mode=OneWay}"
HorizontalAlignment="Right">
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ViewModel.NotYoda.Errors, Mode=OneWay}"
Foreground="Red" />
</ToolTipService.ToolTip>
</SymbolIcon>

Our sample app displays the current (valid) value next to the input boxes:

The shape of things to come

If you compare the different ViewModels in our sample app, you’ll definitely observe the copy/paste patterns around most of the calls to SetProperty() and OnPropertyChanged(). In a future version, MVVM Toolkit will be enhanced with property attributes that will generate the source code for these – as illustrated in this tweet from the author:

We’ll keep you informed on this. In mean time, our sample app lives here on GitHub.

Enjoy!

Advertisements
Report this ad

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK