所以,过了一段时间我再次尝试解决这个问题。
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);