Is it possible to use ReactiveUI bindings in WPF for validating user input with only INotifyDataErrorInfo?
Asked Answered
R

1

5

We're using ReactiveUI.WPF 11.0.1 in our .Net Core WPF application. We're looking into replacing all XAML-based bindings with ReactiveUI-based bindings. There is a ViewModel for the domain type that implements INotifyPropertyChanged and INotifyDataErrorInfo:

public class ItemViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private string Error => string.IsNullOrEmpty(Name) ? "Empty name" : string.Empty;
    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(Error))
            return Enumerable.Empty<string>();
        return new[] {Error};
    }

    public bool HasErrors => !string.IsNullOrEmpty(Error);
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }
}

There is a ViewModel for window:

public class MainWindowViewModel: ReactiveObject
{
    public ItemViewModel ItemA { get; } = new ItemViewModel();
    public ItemViewModel ItemB { get; } = new ItemViewModel();
}

And there is a MainWindow:

<reactiveUi:ReactiveWindow
    x:TypeArguments="local:MainWindowViewModel"
    x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp1"
    xmlns:reactiveUi="http://reactiveui.net"
    mc:Ignorable="d">
    <StackPanel>
        <TextBox Text="{Binding ItemA.Name}" />
        <TextBox x:Name="ItemBTextBox" />
    </StackPanel>
</reactiveUi:ReactiveWindow>
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
    public MainWindow()
    {
        InitializeComponent();
        ViewModel = new MainWindowViewModel();
        DataContext = ViewModel;
        this.WhenActivated(disposables =>
        {
            this.Bind(ViewModel, x => x.ItemB.Name, x => x.ItemBTextBox.Text);
        });
    }
}

The first TextBox shows the default WPF ErrorTemplate (red border) when its' Text property is empty. However, the second one (with ReactiveUI-based binding) doesn't. Is there a way to use ReactiveUI's bindings with WPF's ErrorTemplates automatically working without changing ItemViewModel class?

Rope answered 13/12, 2019 at 10:26 Comment(6)
Have you read the reactiveui documentation? If so, perhaps you should explain why you're not following the example and implementing as they do? reactiveui.net/docs/handbook/user-input-validationShoshone
@Andy, I've read it. We use FluentValidation, so all validation logic is defined in separate AbstractValidators (one validator per one domain interface). These validators are used both in immutable classes implementing domain interfaces (validator.ValidateAndThrow(this) in the constructor) and in ViewModels (delegate the implementation of INotifyDataErrorInfo to the _validationTemplate). The example from the documentation inherits ReactiveValidationObject<TViewModel>, and the validation logic is defined in the same class. We would like to have the view model and its' validation logic separatedRope
@Shoshone The documentation also states that FluentValidation is a great tool, but doesn't provide any examples of integrating it into ReactiveUI's validation. I've seen some NuGet packages integrating these two, I'm going to look into them.Rope
FWIW Fluentvalidation is something people seem to mostly love or loathe.Shoshone
You use the FromEventPattern to wrap the event into an Observable to turn it into a reactive UI property maybe? Here is a quick article on the idea Wrapping EventsVerbal
You can also use Pharmacist to generate observables for all events in project, then subscribe to the validation changing events.Platon
R
6

So, after some time I've tried to solve this issue again. ReactiveUI's bindings don't support INotifyDataErrorInfo validation as is. Therefore, I'd have to bind validation errors manually after binding the value. This can be simply done like that:

public MainWindow() {
    // some initialization code should be here.

    this.WhenActivated(cleanUp => {
        // binding ItemB's Name property to ItemBTextBox's Text property.
        this.Bind(ViewModel, x => x.ItemB.Name, x => x.ItemBTextBox.Text)
            .DisposeWith(cleanUp);
        // binding ItemB's Name property's validation errrors to ItemBTextBox.
        ViewModel.ItemB.WhenAnyPropertyChanged()
            .StartWith(ViewModel.ItemB)
            .Subscribe(itemB =>
            {
                if (!itemB.HasErrors)
                {
                    ClearValidationErrors(ItemBTextBox);
                    return;
                }

                var errorForName = newEmployee
                    .GetErrors(nameof(newEmployee.Name))
                    .Cast<string>()
                    .FirstOrDefault();
                if (string.IsNullOrEmpty(nameError))
                {
                    ClearValidationErrors(ItemBTextBox);
                    return;
                }
                SetValidationError(ItemBTextBox, errorForName);
            })
            .DisposeWith(cleanUp);
    });
}

However, the following question remains: how to make the WPF UI element (ItemBTextBox) display the error we set from code-behind? How the ClearValidationErrors() and SetValidationError() methods should be implemented? The only way to set validation error for UI element (so validation template would show it) I could find was the following code using WPF bindings:

Validation.ClearInvalid(ItemBTextBox.GetBindingExpression(TextBox.TextProperty));
Validation.MarkInvalid(
    ItemBTextBox.GetBindingExpression(TextBox.TextProperty),
    new ValidationError(new NotifyDataErrorValidationRule(), itemB, errorForName, null));

The problem is that the whole WPF validation mechanism is based on WPF bindings. ReactiveUI's bindings don't rely on those. The workaround would be to create a dummy WPF binding and use the code above to clear and set validation errors from code-behind.

ItemBTextBox.SetBinding(TextBox.TextProperty, new Binding("Non_existent_property.") 
    { Mode = BindingMode.OneTime }); // invoke this in MainWindow constructor.

This approach works, but it is quite ugly by its nature (we have to use dummy WPF bindings to make it work, these dummy bindings obviously throw binding errors, etc.). If someone knows a way to use WPF's ValidationTemplates to show validation errors (which could be set from code-behind) for UI elements without WPF bindings, please let me know.

UPD: So I've figured out other way to manipulate WPF's Validation.Errors property. It relies on the reflection and the fact that the Validation class has inner static methods AddValidationError() and RemoveValidationError(). So I can declare new static class:

public static class ValidationHelper
{
    private static readonly MethodInfo AddValidationErrorMethod =
        typeof(Validation).GetMethod("AddValidationError", BindingFlags.NonPublic | BindingFlags.Static);

    private static readonly MethodInfo RemoveValidationErrorMethod =
        typeof(Validation).GetMethod("RemoveValidationError", BindingFlags.NonPublic | BindingFlags.Static);

    public static void AddValidationError(
        ValidationError validationError,
        DependencyObject targetElement)
    {
        AddValidationErrorMethod
            .Invoke(null, new object[] {validationError, targetElement, true});
    }

    public static void ClearValidationErrors(DependencyObject targetElement)
    {
        foreach (var error in Validation.GetErrors(targetElement).ToArray())
            RemoveValidationErrorMethod
                .Invoke(null, new object[] { error, targetElement, true });
    }
}

and use it like this:

ValidationHelper.ClearValidationErrors(ItemBTextBox);
ValidationHelper.AddValidationError(new ValidationError(new NotifyDataErrorValidationRule(), itemB, errorForName, null),
                            ItemBTextBox);

It's far from perfect, but it works. And you don't need to use any dummy WPF bindings.

UPD2: this may be kind of less relevant to the initial question, but I'm also going to add my naive extension method for binding INotifyDataErrorInfo errors to WPF controls' ValidationTemplate to the answer in case anyone with the same problem needs a reference.

// just a helper method to extract property name from the expression.
private static string GetPropertyName<T, TProperty>(this Expression<Func<T, TProperty>> property)
    where T : class
{
    if (!(property.Body is MemberExpression member))
        throw new ArgumentException("A method is provided instead of a property.");
    if (!(member.Member is PropertyInfo propertyInfo))
        throw new ArgumentException("A field is provided instead of a property");
    return propertyInfo.Name;
}

public static IDisposable BindValidationError
    <TView, TViewModel, TValidatableObject, TProperty>(
        this TView view,
        TViewModel viewModel,
        Expression<Func<TViewModel, TValidatableObject>> objectToValidateName,
        Expression<Func<TValidatableObject, TProperty>> propertyToValidate,
        Func<TView, DependencyObject> uiElementDelegate)
    where TViewModel : class
    where TView : IViewFor<TViewModel>
    where TValidatableObject : class, INotifyDataErrorInfo
{
    string lastError = null;
    var propertyToValidateName = propertyToValidate.GetPropertyName();
    return viewModel.WhenAnyValue(objectToValidateName)
        .StartWith(objectToValidateName.Compile().Invoke(viewModel))
        .Do(objectToValidate =>
        {
            var uiElement = uiElementDelegate.Invoke(view);
            if (objectToValidate == null)
            {
                ValidationHelper.ClearValidationErrors(uiElement);
                return;
            }

            ValidateProperty(
                objectToValidate,
                propertyToValidateName,
                uiElement,
                ref lastError);
        })
        .Select(objectToValidate => objectToValidate != null
            ? Observable.FromEventPattern<DataErrorsChangedEventArgs>(objectToValidate,
                nameof(objectToValidate.ErrorsChanged))
            : Observable.Empty<EventPattern<DataErrorsChangedEventArgs>>())
        .Switch()
        .Subscribe(eventArgs =>
        {
            if (eventArgs.EventArgs.PropertyName != propertyToValidateName)
                return;
            var objectToValidate = (INotifyDataErrorInfo) eventArgs.Sender;
            var uiElement = uiElementDelegate.Invoke(view);
            ValidateProperty(
                objectToValidate,
                propertyToValidateName,
                uiElement,
                ref lastError);
        });
}

Use it in the view's WhenActivated:

this.Bind(
    ViewModel,
    viewModel => viewModel.ItemB.Name,
    view => view.ItemBTextBox.Text)
    .DisposeWith(cleanUp);
this.BindValidationError(
    ViewModel,
    viewModel => viewModel.ItemB,
    itemB => itemB.Name,
    view => view.NewEmployeeNameTextBox)
    .DisposeWith(cleanUp);
Rope answered 11/5, 2020 at 9:20 Comment(1)
Thank you so much for sharing! I was having the same problem. My ViewModel implements IValidatableViewModel but I just wanted to use the existing infrastructure of adorned invalid controls. Using BindValidation did not lead anywhere. This just saved my day!Bair

© 2022 - 2024 — McMap. All rights reserved.