1

I'd like to implement MVVM Toolkit's validation method using reusable controls. My problem is that the warning highlight appears on the whole control, like this:

enter image description here

If I don't use reusable controls, it works correctly:

enter image description here

The reusable control looks like this:

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

And the view and view model I use it:

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;

        ...
    }

As you can see, I use MVVM Toolkit's source generator for this property and their validation method. ViewModelBase inherits from ObservableValidator which implements INotifyDataErrorInfo. Validation is working correctly, meaning whenever I type 2 characters, the error highlight disappears and reappears when I enter less than 2 characters.

For the 2nd example, where the validation highlight is showing correctly, I created a property the same way I did for first name and I simply bound the text box's text property to the UserName property.

Is it possible to make validation work with reusable controls or a completely different approach is needed in this case?

1 Answer 1

5

Since the validating Binding is the one that is set from the view model to the UserControl, the binding engine will set the attached property Validation.HasError to true for the UserControl (the binding target). Hence the error template is adorning the UserControl and not a particular internal element.

You must configure the UserControl to instruct the binding engine to adorn a different element instead. You can use the attached Validation.ValidationAdornerSiteFor property:

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

I just like to point out that it is technically correct to apply the error template on the complete UserControl.
To change this behavior, you must explicitly validate the internal bindings (see example below).

Since Validation.ValidationAdornerSiteFor only allows to set a single alternative element, you would have to manually delegate the validation error, in case the UserControl has multiple validated inputs.

The following example shows how to route the external binding validation error to the corresponding internal input element:

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}" />
7
  • Thank you for the detailed answer! It works, except for one little detail. When I delete the content of the TextBox, it should also raise an error as it's marked as required. When I debugged it, the validationError.ErrorContent is set correctly, saying "The FirstName field is required.", however, the error is not shown. How can I make it so that the validation error tooltip is shown as for the other validation errors?
    – drazse
    Commented May 23, 2022 at 0:18
  • Which solution have you implemented, the validation error delegation? And does hasError returns true? Is MarkInvalid called? I think the Required attribute will raise an exception. Have you tried to set ValidatesOnException to true for the internal TextBox binding? I guess this will fix it.
    – BionicCode
    Commented May 23, 2022 at 0:33
  • Unfortunately, it didn't help it. I used your second example as the first one didn't show the tooltip with the error message, only the highlight on the text box. And yes, MarkInvalid is called, the error message is there, but it's not showing. The exception is null for Required, but it's also null in the case of the other ones. I don't get why it works for those and why it doesn't for this one.
    – drazse
    Commented May 23, 2022 at 1:24
  • 1
    Sorry, but I can't reproduce your issue. You either missed a detail of your implementation or the issue is related to the library you are using. Implementing data validation is really very simple. You can implement the third example of this answer: How to add validation to view model properties or how to implement INotifyDataErrorInfo. It will give you attribute based validation. I assume this will fix your issues.
    – BionicCode
    Commented May 23, 2022 at 8:29
  • 1
    I thought you were only styling or theming the controls, but you are overriding their functionality too. I understood that you have added a custom clear text button. Since the functionality is hidden inside the library, I can't help you. This is a classic example why you should avoid 3rd party libraries and only use them if absolutely necessary. It's very easy to implement such a button. Maybe the library contains a less invasive style that only themes the button without changing its behavior, so that you can add the clear feature yourself (to give you full control about the behavior).
    – BionicCode
    Commented May 24, 2022 at 9:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.