Custom Validation Attributes and Multi-Language Resource Loading for Validation with INotifyDataErrorInfo for XAML Applications (UWP, WinUI, Uno)

I think this will be the last in the series of posts covering validation using the INotifyDataErrorInfo interface, which can be used to expose validation error messages directly in your XAML. In this post we’re going to look at adding custom validation using both the CustomValidationAttribute and by creating our own ValidationAttribute. We’ll also look at how we can further customise error messages to be language specific.

If you haven’t already been following along, here are the links to the previous posts in this series:

Building a XAML UserControl for WinUI, UWP, WPF or Xamarin.Forms (.NET MAUI)

Adding Validation to a XAML Control Using INotifyDataErrorInfo and the CommunityToolkit for WinUI, WPF, UWP and Uno

Customising Error Messages with INotifyDataErrorInfo Validation for XAML Applications (UWP, WinUI, Uno)

Localization of Error Messages

We’re actually going to do this in reverse order, starting with how to support multiple languages for the error messages for validation. In the previous post we covered how we can look up the error message for a validation attribute by specifying the ErrorMessageResourceType and the ErrorMessageResourceName. However, the limitation with our implementation was that it just returned a static string, specified in English.

What we need to do is to be able to return the error message in the current language for the application. Luckily, the ResourceLoader class enables us to retrieve resources that have been defined in language specific resource files.

As a quick summary for how localized resources work in XAML, you can specify language specific resources using a .resw file for each language/locale you want to support. For example, in the following image the application has resources for en-US (US English) and fr-CA (Canadian French), based on the folder structure:

If you double-click on the .resw file in Solution Explorer it will open in the Visual Studio resource editor, allowing you to edit the Name, Value and Comment for each resource.

Linking resources to attributes of a XAML element can be done based on convention. For example if you have a TextBlock called “FirstNamePrompt”, if you create a resources with Name of “FirstNamePrompt.Text” the Value will be used at runtime to set the Text property of the TextBlock. This isn’t limited to string only resources, so you can specify properties such as Height and Width that may need to vary for different langauges.

Behind all this wonderful XAML / Resource magic is the ResourceLoader class, which determines which resources to load based on language/locale and other runtime metrics (scale is often used when determining which image quality to load).

Let’s return to how we want to use the ResourceLoader for localizing our error messages. So far, our view model has been constructed to be platform agnostic. By this I mean it hasn’t been tied to WinUI, UWP, Uno or Xamarin.Forms (.NET Maui). If we start referencing the ResourceLoader directly, we’ll be breaking this architecture, resulting in a strong link between our view model and the way WinUI loads resources. For a WinUI only application, you might think that this is acceptable but I would challenge that breaking this architecture will limit the ability to test your view model independently of the view technology that it’s going to be used with.

So, the question becomes, how do we abstract away the ResourceLoader in a way that it can still be used when running in a WinUI application. Luckily, the Microsoft dotnet extensions already have an answer, as it exposes the IStringLocalizer interface. We’re going create a new error resolver that makes use of the IStringLocalizer interface.

public static class ErrorResourceResolver
{
    public static IStringLocalizer Localizer { get; set; }
    public static string MinLengthError => LoadErrorByName();

    public static string RequiredError => LoadErrorByName();

    public static string LoadErrorByName([CallerMemberName] string methodName = null)
    {
        return Localizer.GetString(methodName).Value;
    }
}

Now all we need is an implementation for IStringLocalizer and to set the static Localizer property on the ErrorResourceResolver class. Here’s the implementation

public class ResourceLocalizer : IStringLocalizer
{
    private ResourceLoader resourceLoader;
    public LocalizedString this[string name]
        => new LocalizedString(name, (resourceLoader ?? (resourceLoader = new Microsoft.ApplicationModel.Resources.ResourceLoader())).GetString(name));

    public LocalizedString this[string name, params object[] arguments] => this[name];

    public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
    {
        throw new NotImplementedException();
    }
}

And then all we need to do is set the Localizer property with a new instance of the ResourceLocalizer class – I suggest doing this either in the App.xaml.cs of the code behind of the main window of the application to ensure the Localizer property is set before it might be needed.

ErrorResourceResolver.Localizer = new ResourceLocalizer();

The final outcome is that if you switch the language used by Windows (go to Settings > Time & language > Language & region and set the Windows display language), the application will load the language specific resources. For example in the following image, the French resources have been loaded.

For debugging this example I prepended the resource value with “french” so that I know the correct resource has been loaded. Of course, for a production application you’d want to have a native speaker review the resources for each language you want to support.

Custom Validation

Sometimes the built in validation attributes don’t cut it and you need to provide your own validation logic. Two ways to do this are to use the CustomValidationAttribute or to create your own validation attribute that inherits from ValidationAttribute. We’ll take a look at both.

CustomValidationAttribute

Using the CustomValidationAttribute is very similar to using any of the other built in validation attributes. The only difference is that you need to specify the method that you want to be called in order to perform the validation. In the following code example the Last property has a CustomValidation attribute that uses the static StartsWithUppercaseValidation method on the MainViewModel class.

[CustomValidation(typeof(MainViewModel), nameof(StartsWithUppercaseValidation))]
public string Last { get => lastName; set => SetProperty(ref lastName, value, true); }

public static ValidationResult StartsWithUppercaseValidation(object value)
{
    var text = value + "";
    var isValid = string.IsNullOrWhiteSpace(text) ? false : char.IsUpper(text.First());
    return isValid ? ValidationResult.Success : new ValidationResult(null);
}

The StartsWithUppercaseValidation method needs to be a static method, which limits the ability to access further information about the entity being validated. The only input is the value of the property being validated. If you do need full access to the entity being validated, you can change the method signature of the validation method to include a second parameter of type ValidationContext which has a reference to the entity being validated.

It’s worth noting that the return value for the validation method needs to be a ValidationResult. If there are no validation errors, simply return the predefined ValidationResult.Success. If there are validation errors, you need to return an instance of the ValidationResult class, which accepts an error message as a parameter for the constructor. Specifying a null error message will default to using the internal logic to determine the error message, using the DefaultErrorMessage, ErrorMessage, ErrorMessageResourceType and ErrorMessageResourceName properties if they’ve been specified.

Implementing a ValidationAttribute

Sometimes you need more control than just specifying the validation method. In this scenario you can implement your own validation attribute by simply inheriting from the ValidationAttribute base class. Our example here ensures there are only letters or digits in the property (please don’t use this for validating names in an actual app as there are plenty of names out there that wouldn’t validate correctly with this attribute)

public class NoSpecialCharactersAttribute : ValidationAttribute
{
    public NoSpecialCharactersAttribute() : base("Character {1} is invalid for {0}")
    {
    }
    private int InvalidCharacter(string value)
    {
        var first = value
            .Select((c, idx) => new { Character = c, Index = idx + 1 })
            .Where(c => !char.IsLetterOrDigit(c.Character))
            .Select(c => c.Index)
            .FirstOrDefault();
        return first;
    }
    private int FirstInvalidCharacter { get; set; }
    public override bool IsValid(object value)
    {
        return (FirstInvalidCharacter = InvalidCharacter(value + "")) <= 0;
    }
    public override string FormatErrorMessage(string name)
    {
        return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, FirstInvalidCharacter);
    }
}

What’s interesting about this validation attribute is that the default error message, supplied as the parameter to the base constructor includes two placeholders. The first ({0}) is for the property name being validated; the second ({1}) is for the position of the first invalid character. The position of the first invalid character is captured when the IsValid method is invoked, and is subsequently used in the call to FormatErrorMessage.

After all the different validation options, here is our Last property. As you can see, we’ve kept the definition of our property simple, yet it has 5 validation rules applied, each using a slightly different technique to either validate the property, or for how the error message is loaded.

[Display(Name = nameof(DisplayResolver.LastName), ResourceType = typeof(DisplayResolver))]
[Required(ErrorMessageResourceType = typeof(ErrorResolver),
    ErrorMessageResourceName = nameof(ErrorResolver.CustomRequired))]
[MinLength(2,
    ErrorMessageResourceType = typeof(ErrorResourceResolver),
    ErrorMessageResourceName = nameof(ErrorResourceResolver.MinLengthError))]
[MaxLength(100)]
[NoSpecialCharacters]
[CustomValidation(typeof(MainViewModel), nameof(StartsWithUppercaseValidation))]
public string Last { get => lastName; set => SetProperty(ref lastName, value, true); }

I hope you’ve got something out of this series on validation. If there’s anything I’ve missed, or you have a better way to do something, please leave a comment!

Leave a comment