MVVM Community Toolkit: Using ObservableValidator with non-string properties

2.7k Views Asked by At

I have a WPF application that uses the ObservableValidator to handle validation of properties using data annotations. This works great for string properties. For example:

public class LoginViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [StringLength(64, MinimumLength = 8)]
    private string username = String.Empty;

    [RelayCommand]
    private void LogIn()
    {
        ValidateAllProperties();

        if(HasErrors)
        {
            return;
        }
        
        // Log in the user
    }

    // ...
}

When I bind the Username property to a text box using <TextBox Text="{Binding Username, ValidatesOnNotifyDataErrors=True}" />, I automatically get validation messages in my view that appear around the textbox!

However, I don't know how I'm supposed to handle cases where I need to validate non-string properties. For example:

public class User : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [Range(0, 200)]
    private int age = 10;
}

If I used the same approach of <TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True}" /> and the user enters a value that's not an integer like "12aaa", the default value converter on the binding throws an exception saying that "12aaa" cannot be converted into an integer. These value conversion exceptions can't be detected from my view model since the binding engine never updates the property value.

Thus, calling ValidateAllProperties() sets HasError to false, even though the user entered invalid data!

I see a few ways to handle these impossible-to-detect errors:

  1. Prevent the user from ever entering invalid data. This seems feasible at first, but becomes harder with more complicated types (e.g., a user input for a TimeSpan).
  2. Add string fields for each of the non-string properties. Even if you do this, I don't know how to propagate validation errors from the typed properties to the non-typed counterparts so they show up around the appropriate text box.
  3. Probably some other options I haven't thought of.

Are there any recommended ways of handling conversion errors for non-string properties with the MVVM Toolkit's ObservableValidator? Thanks in advance for your help!

3

There are 3 best solutions below

2
AudioBubble On BEST ANSWER

This is a runtime exception and not thrown by the library you use. The exception is thrown before the value is assigned to the source property (when the binding engine tries to convert from target type string to the source type int). The exception is not related to data validation.

TextBox.Text is of type string. User.Age is of type int. The exception is thrown because the binding tries to assign a string value from the TextBox to an int property. There is no implicit cast from string to int. In case the source property type differs from the target property type (and there is no implicit type conversion) Binding markup extension will use a default converter so that e.g. an int value can be send/assigned to a string binding target property. But this only works if there exists a default conversion.

For example, "123" is purely numeric and can be converted to int using a default converter. But "123abc" is alphanumeric and the standard conversion to int fails and the binding engine throws the exception.

You can:

  1. Make User.Age of type string
  2. Add a User.AgeText property of type string to bind to the TextBox and convert it to int in your data model for example using int.TryParse from the property setter
  3. Implement an IValueConverter to convert the binding target value from string to int e.g. with the help of int.TryParse.
  4. Extend TextBox and implement an e.g. NumericTextBox where the input is converted from string to int or the input is validated to ensure a valid numeric value (so that the default converter can handle it). The validation of the value itself (e.g. if the numeric value is within a particular range) is still implemented in the data source (view model class)
1
Lukasz Szczygielek On
1
Sheriff On

How about using CustomValidation?

public partial class LoginViewModel : ObservableValidator
{
    public LoginViewModel()
    {
        ValidateAllProperties();
    }

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [StringLength(64, MinimumLength = 8)]
    private string? username = String.Empty;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [Range(0, 200)]
    [CustomValidation(typeof(LoginViewModel), nameof(ValidateAge))]
    private string? age = "10";

    public static ValidationResult ValidateAge(string age, ValidationContext context)
    {
        if (int.TryParse(age, out int number))
        {
            return ValidationResult.Success;
        }

        return new("Validation failed/show error message");
    }

    [RelayCommand]
    private void LogIn()
    {
        ValidateAllProperties();

        if (HasErrors)
        {
            return;
        }

        // Log in the user
    }
}