【问题标题】:Is it possible to use ReactiveUI bindings in WPF for validating user input with only INotifyDataErrorInfo?是否可以在 WPF 中使用 ReactiveUI 绑定仅通过 INotifyDataErrorInfo 验证用户输入?
【发布时间】:2019-12-13 10:26:40
【问题描述】:

我们在 .Net Core WPF 应用程序中使用 ReactiveUI.WPF 11.0.1。我们正在考虑用基于 ReactiveUI 的绑定替换所有基于 XAML 的绑定。 实现 INotifyPropertyChanged 和 INotifyDataErrorInfo 的域类型有一个 ViewModel:

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));
    }
}

窗口有一个ViewModel:

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

还有一个主窗口:

<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);
        });
    }
}

当第一个 TextBox 的 Text 属性为空时,它会显示默认的 WPF ErrorTemplate(红色边框)。但是,第二个(使用基于 ReactiveUI 的绑定)没有。有没有办法使用 ReactiveUI 的绑定与 WPF 的 ErrorTemplates 自动工作而不更改 ItemViewModel 类?

【问题讨论】:

  • 您阅读过响应式用户界面文档吗?如果是这样,也许您应该解释为什么您不遵循示例并像他们那样实施? reactiveui.net/docs/handbook/user-input-validation
  • @Andy,我读过。我们使用 FluentValidation,因此所有验证逻辑都在单独的 AbstractValidators 中定义(每个域接口一个验证器)。这些验证器用于实现域接口的不可变类(构造函数中的validator.ValidateAndThrow(this))和ViewModels(将INotifyDataErrorInfo 的实现委托给_validationTemplate)。文档中的示例继承了 ReactiveValidationObject,验证逻辑定义在同一个类中。我们希望将视图模型及其验证逻辑分开
  • @Andy 该文档还指出 FluentValidation 是一个很棒的工具,但没有提供任何将其集成到 ReactiveUI 验证中的示例。我已经看到一些集成这两个的 NuGet 包,我将研究它们。
  • FWIW Fluentvalidation 似乎是人们最喜欢或最讨厌的东西。
  • 您使用 FromEventPattern 将事件包装到 Observable 中以将其转换为反应式 UI 属性可能吗?这是一篇关于这个想法的快速文章Wrapping Events

标签: wpf validation fluentvalidation reactiveui


【解决方案1】:

所以,一段时间后,我再次尝试解决此问题。 ReactiveUI 的绑定不支持 INotifyDataErrorInfo 验证。因此,我必须在绑定值后手动绑定验证错误。这可以像这样简单地完成:

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);
    });
}

但是,仍然存在以下问题:如何使 WPF UI 元素(ItemBTextBox)显示我们在代码隐藏中设置的错误? ClearValidationErrors() 和 SetValidationError() 方法应该如何实现?我可以找到为 UI 元素设置验证错误的唯一方法(因此验证模板会显示它)是使用 WPF 绑定的以下代码:

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

问题在于整个 WPF 验证机制是基于 WPF 绑定的。 ReactiveUI 的绑定不依赖于这些。解决方法是创建一个虚拟 WPF 绑定并使用上面的代码从代码隐藏中清除和设置验证错误。

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

这种方法有效,但它的本质非常丑陋(我们必须使用虚拟 WPF 绑定才能使其工作,这些虚拟绑定显然会引发绑定错误等)。如果有人知道如何使用 WPF 的 ValidationTemplates 为没有 WPF 绑定的 UI 元素显示验证错误(可以从代码隐藏中设置),请告诉我。

UPD: 所以我想出了其他方法来操纵 WPF 的 Validation.Errors 属性。它依赖于反射以及 Validation 类具有内部静态方法 AddValidationError() 和 RemoveValidationError() 的事实。所以我可以声明新的静态类:

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 });
    }
}

并像这样使用它:

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

它远非完美,但它确实有效。而且您不需要使用任何虚拟 WPF 绑定。

UPD2: 这可能与最初的问题不太相关,但我还将添加我的天真扩展方法,用于将 INotifyDataErrorInfo 错误绑定到 WPF 控件的 ValidationTemplate 以防万一任何有相同问题的人都需要参考。

// 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);
        });
}

在视图的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);

【讨论】:

  • 非常感谢您的分享!我遇到了同样的问题。我的 ViewModel 实现了 IValidatableViewModel 但我只想使用现有的装饰无效控件的基础结构。使用 BindValidation 并没有导致任何结果。这只是节省了我的一天!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-30
  • 1970-01-01
  • 2018-04-08
  • 1970-01-01
  • 2023-01-12
  • 2015-06-07
相关资源
最近更新 更多