'Proper way to bind and validate non-string values in ReactiveUI?

We are confused about how we should bind and validate int and other non-string properties in ReactiveUI. When we do this, we get unhandled exceptions and cases where invalid values do not prevent the command from being run.

Example:

Consider a form with an integer field for a percentage that must be between 0 and 100. Part of the view model would be:

public class ExampleViewModel : ReactiveObject, IValidatableViewModel
{
    public ExampleViewModel()
    {
        this.ValidationRule(vm => vm.Percentage,
                            percentage => percentage >= 0 && percentage <= 100,
                            "Percentage must be between 0% and 100%");

        
        OkCommand = ReactiveCommand.CreateFromTask(OkAsync, ValidationContext.Valid);
    }

    [Reactive]
    public int Percentage { get; set; } = 10;

    public ReactiveCommand<Unit, Unit> OkCommand{ get; }

    public ValidationContext ValidationContext { get; } = new ValidationContext();
}

And the view would contain:

public ExampleView : ReactiveControl<ExampleViewModel>
{
    public ExampleView()
    {
        InitializeComponent();
        this.WhenActivated(d =>
        {
            this.Bind(ViewModel, vm => vm.Percentage, v => v.PercentageTextBox.Text).DisposeWith(d);
            this.BindValidation(ViewModel, vm => vm.Percentage, v => v.PercentageError.Content).DisposeWith(d)
            this.BindCommand(ViewModel, vm => vm.OkCommand, v => v.OkButton).DisposeWith(d);
        });
    }
}

Non-integer Input

If the user enters something like "3a4", then parsing throws a FormatException and ComponentModelTypeConverter.TryConvert catches that and doesn't assign anything to the Percentage property. So the validation rule doesn't run. The input is not marked invalid and the user is allowed to click "OK" even though the input is invalid.

Out of Range Input

If the user enters something like "62147483647" (larger than int.MaxValue), then parsing throws an OverflowException. The exception is not caught by ComponentModelTypeConverter.TryConvert and becomes an unhandled exception. We have wired up the unhandled exceptions event to report the error and exit the app. So from the user's perspective, that is a crash.

Not a Percentage Input

If the user enters something like "123", that is handled correctly. It is converted to int and the validation rule is run. The user is shown the error and prevented from clicking "OK".

Options

We aren't sure how we are meant to handle this. Some options (none of which seem good):

  1. Make all properties that textboxes are bound to strings and add a validation rule for whether it is parsable. Convert before applying to the model.
  2. Make custom value converters that only ever throw FormatException. That prevents unhandled exceptions but still doesn't validate that the value parses.
  3. Use WPF binding which supports INotifyDataErrorInfo and handles this correctly. But we like using strongly typed ReactiveUI binding. And it would be best to consistently use ReactiveUI bindings rather than mixing different types. This also doesn't work for anyone using another UI framework.

Updates

The unhandled exceptions when entering out of range values were fixed in ReactiveUI v14.3.10. However, the same issue exists with nullable ints (i.e. int?). I have filed bug #3021 for that issue and it is fixed in ReactiveUI v16.3.10.

The question remains, how are unparsable values supposed to be handled. They are not available to validation rules and do not show any validation error in the UI.



Solution 1:[1]

The solution is to set proper viewToVmConverter. You have to remember that you're trying to bind string value to int and it has to be handled properly.

So there should be also a validation on the "input" side like:

  • Allow to enter ONLY digits;
  • Narrow down range that can be entered (if input > | < than range set some extreme)

You should be able to set such conditions on control level or as i said before use viewToVmConverter and bind property processed value.

So you have to distinguish between input validation (binding / control level) and business validation (logic level).

The above applies always when you want to bind two types that has more conplicated conversion.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Posio