K. *_*cov 5 validation wpf fluentvalidation reactiveui
我们在 .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));
}
}
Run Code Online (Sandbox Code Playgroud)
有一个用于窗口的 ViewModel:
public class MainWindowViewModel: ReactiveObject
{
public ItemViewModel ItemA { get; } = new ItemViewModel();
public ItemViewModel ItemB { get; } = new ItemViewModel();
}
Run Code Online (Sandbox Code Playgroud)
还有一个主窗口:
<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>
Run Code Online (Sandbox Code Playgroud)
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);
});
}
}
Run Code Online (Sandbox Code Playgroud)
第一个 TextBox 在其 Text 属性为空时显示默认的 WPF ErrorTemplate(红色边框)。但是,第二个(基于 ReactiveUI 的绑定)没有。有没有办法在不更改 ItemViewModel 类的情况下使用 ReactiveUI 的绑定与 WPF 的 ErrorTemplates 自动工作?
所以,过了一段时间我再次尝试解决这个问题。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);
});
}
Run Code Online (Sandbox Code Playgroud)
但是,仍然存在以下问题:如何使 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));
Run Code Online (Sandbox Code Playgroud)
问题在于整个 WPF 验证机制基于 WPF 绑定。ReactiveUI 的绑定不依赖于这些。解决方法是创建一个虚拟 WPF 绑定,并使用上面的代码从代码隐藏中清除和设置验证错误。
ItemBTextBox.SetBinding(TextBox.TextProperty, new Binding("Non_existent_property.")
{ Mode = BindingMode.OneTime }); // invoke this in MainWindow constructor.
Run Code Online (Sandbox Code Playgroud)
这种方法有效,但本质上相当丑陋(我们必须使用虚拟 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 });
}
}
Run Code Online (Sandbox Code Playgroud)
并像这样使用它:
ValidationHelper.ClearValidationErrors(ItemBTextBox);
ValidationHelper.AddValidationError(new ValidationError(new NotifyDataErrorValidationRule(), itemB, errorForName, null),
ItemBTextBox);
Run Code Online (Sandbox Code Playgroud)
它远非完美,但它确实有效。并且您不需要使用任何虚拟 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);
});
}
Run Code Online (Sandbox Code Playgroud)
在视图的 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);
Run Code Online (Sandbox Code Playgroud)