在可重用控件上使用 INotifyDataErrorInfo

Using INotifyDataErrorInfo on reusable control

我想使用可重用控件实现 MVVM Toolkit 的验证方法。我的问题是警告高亮显示在整个控件上,像这样:

如果我不使用可重复使用的控件,它可以正常工作:

可重用控件如下所示:

ValidationTextBox.xaml

<StackPanel>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="275" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=HeaderText}" />

            <TextBox
                Grid.Row="1"
                Text="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=TextBoxContent}" />
        </Grid>
</StackPanel>

ValidationTextBox.xaml.cs

public partial class ValidationTextBox : UserControl
    {
        
        public static readonly DependencyProperty HeaderTextProperty = 
            DependencyProperty.Register(nameof(HeaderText), typeof(string), typeof(ValidationTextBox), new PropertyMetadata(default(string)));

        public string HeaderText
        {
            get => (string)GetValue(HeaderTextProperty);
            set => SetValue(HeaderTextProperty, value);
        }

        public static readonly DependencyProperty TextBoxContentProperty =
            DependencyProperty.Register(nameof(TextBoxContent), typeof(string), typeof(ValidationTextBox), new FrameworkPropertyMetadata(default(string)));

        public string TextBoxContent
        {
            get { return (string)GetValue(TextBoxContentProperty); }
            set { SetValue(TextBoxContentProperty, value); }
        }

        public ValidationTextBox()
        {
            InitializeComponent();
        }
}

以及我使用的视图和视图模型:

RegisterView.xaml

...
<controls:ValidationTextBox
                            Grid.Row="1"
                            Grid.Column="2"
                            MaxWidth="300"
                            Margin="10,10,0,0"
                            HeaderText="First name"
                            TextBoxContent="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
...

RegisterViewModel.cs

public partial class RegisterViewModel : ViewModelBase
    {
        ...

        [ObservableProperty]
        [Required]
        [MinLength(2)]
        private string? _firstName;

        ...
    }

如您所见,我为此 属性 使用了 MVVM Toolkit 的源代码生成器及其验证方法。 ViewModelBase 继承自 ObservableValidator,它实现了 INotifyDataErrorInfo。 验证工作正常,这意味着每当我输入 2 个字符时,错误突出显示消失并在我输入少于 2 个字符时重新出现。

对于正确显示验证突出显示的第二个示例,我创建了一个 属性 与我为名字所做的相同的方式,我只是将文本框的文本 属性 绑定到 UserName 属性.

是否可以使用可重复使用的控件进行验证,或者在这种情况下需要完全不同的方法?

由于验证 Binding 是从视图模型设置到 UserControl,绑定引擎会将附加的 属性 Validation.HasError 设置为 true对于 UserControl(绑定目标)。因此,错误模板装饰 UserControl 而不是特定的内部元素。

您必须配置 UserControl 以指示绑定引擎装饰不同的元素。您可以使用附件 Validation.ValidationAdornerSiteFor 属性:

<UserControl>
  <StackPanel>
    <TextBlock />
    <TextBox Validation.ValidationAdornerSiteFor="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}" />
  </StackPanel>
</UserControl>

我只想指出,在完整 UserControl 上应用错误模板在技术上是正确的。
要更改此行为,您必须显式验证内部绑定(参见下面的示例)。

由于 Validation.ValidationAdornerSiteFor 只允许设置一个替代元素,您必须手动委托验证错误,以防 UserControl 有多个验证输入。

以下示例显示如何将外部绑定验证错误路由到相应的内部输入元素:

MyUserControl.xaml.cs

partial class MyUserControl : UserControl
{
  // This ValidationRule is only used to explicitly create a ValidationError object.
  // It will never be invoked.
  private class DummyValidationRule : ValidationRule
  {
    public override ValidationResult Validate(object value, CultureInfo cultureInfo) => throw new NotSupportedException();
  }

  public string TextData
  {
    get => (string)GetValue(TextDataProperty);
    set => SetValue(TextDataProperty, value);
  }

  public static readonly DependencyProperty TextDataProperty = DependencyProperty.Register(
    "TextData", 
    typeof(string), 
    typeof(MyUserControl), 
    new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,  OnTextDataChanged));

  private static void OnTextDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var userControl = (d as MyUserControl);
    BindingExpression? bindingExpression = userControl.GetBindingExpression(TextDataProperty);
    if (bindingExpression is null)
    {
      return;
    }

    userControl.OnTextDataChanged(bindingExpression.HasError, Validation.GetErrors(userControl).FirstOrDefault());
  }

  private void OnTextDataChanged(bool hasError, ValidationError validationError)
  {
    BindingExpression bindingExpression = this.InternalTextBox.GetBindingExpression(TextBox.TextProperty);
    if (hasError)
    {
      validationError = new ValidationError(new DummyValidationRule(), bindingExpression, validationError.ErrorContent, validationError?.Exception);
      Validation.MarkInvalid(bindingExpression, validationError);
    }
    else
    {
      Validation.ClearInvalid(bindingExpression);
    }
  }
}

MyUserControl.xaml

<UserControl Validation.ErrorTemplate="{x:Null}">
  <StackPanel>
    <TextBlock />
    <TextBox x:Name="InternalTextBox"
             Text="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=TextData, ValidatesOnNotifyDataErrors=True}" />
  </StackPanel>
</UserControl>
<MyUserControl TextData="{Binding TextValue, ValidatesOnNotifyDataErrors=True}" />