wpfvalidationmvvm

Validation ErrorTemplate shows on `ContentControl` instead of `TextBox`


Problem

I'm trying to generate visual feedback on the result of validating a TextBox input control with the following requirements:

The related validation code seems to be working: the function that performs validation is running, the error messages are accumulated, and OnErrorsChanged is being invoked and WPF is doing ... something... a red box appears around my entire UserControl.

After reading many related SO questions/answers and considering the fact that a red box is being drawn, my initial instinct was that this is related to adorner placement. However, I have tried placing AdornerDecorator at every layer with no effect. I also have checked at runtime if the textbox controls have an adorner layer accessible to them, and they do. It is being created from the ContentControl hosting my view (View1.xaml). This seems to explain why I'm seeing a red box around the entire ContentControl, but I'm confused as to why the TextBox's Validation.ErrorTemplate is not controlling the placement of the adorner.

enter image description here

Control layout

Code

View1.xaml

<UserControl x:Class="View1"
             ....namespaces....
             xmlns:Fluent="urn:fluent-ribbon"
             xmlns:vm="clr-namespace:MyNamespace.ViewModel"
             xmlns:view="clr-namespace:MyNamespace.View"
             Name="MyView1"
             DataContext="{StaticResource g_MainWindowViewModel}"
             Validation.ErrorTemplate="{x:Null}">
<DockPanel LastChildFill="True">
    <ContentControl Name="NewSessionFormContentControl" 
                    Content="{Binding m_SessionFormViewModel}" 
                    DockPanel.Dock="Left"/>
</DockPanel>

With a ControlTemplate taken from BionicCode's answer here:

<ControlTemplate x:Key="ValidationErrorTemplate">
    <DockPanel LastChildFill="True">
    <Border BorderBrush="Red" BorderThickness="1">
        <AdornedElementPlaceholder x:Name="AdornedElement"/>
    </Border>
    <Border Background="White" 
            BorderBrush="Red" 
            Padding="4"
            BorderThickness="1,0,1,1" 
            HorizontalAlignment="Left">
        <ItemsControl ItemsSource="{Binding}"
                        HorizontalAlignment="Left"
                        DockPanel.Dock="Right">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                <TextBlock Text="{Binding ErrorContent}" 
                            Foreground="Red"
                            DockPanel.Dock="Right"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Border>
    </DockPanel>
</ControlTemplate>

Here is a sample input control to be validated in this view:

<TextBox Name="Name1"
        Width="350" 
        Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
        Text="{Binding Name, Mode=OneWayToSource, ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=Explicit}"/>

Solution

  • Your code has several flaws and it does not honor certain details of the binding engine's validation behavior. I also have some suggestions to make that should improve your overall code.

    1 Missing Validation Error Rendering for the TextBox

    As I had already mentioned in the comments you must set the Binding.Mode to BindingMode.TwoWay if you want the binding target to show validation errors.

    Explanation

    If only want plain property validation, then BindingMode.OneWayToSource is sufficient. The binding engine sets attatched properties like the Validation.Errors property on the target element. It does this in the wake of updating the target property. The target is only updated if the Binding.Mode is OneWay, OneTime or TwoWay. Because you want to trigger the data validation (which requires to update the binding source), the only feasible mode is TwoWay.

    Fix

    SessionFromView.xaml
    Set the Binding.Mode of the TextBox binding to BindingMode.TwoWay

    <TextBox Name="SessionName"
             Validation.ErrorTemplate="{DynamicResource ValidationErrorTemplate}"
             Text="{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" />
    

    2 Bug in ViewModelBase.ClearErrors

    You missed raising the INotifyDataErrorInfo.ErrorsChanged event for the case that the parameter propertyName is null or empty.

    Explanation

    The binding engine subscribes to the INotifyDataErrorInfo.ErrorsChanged event to update the binding (based on the configured Binding.Mode). If you call ViewModelbase.ClearErrors without providing a property name, as you are doing, then clearing the errors will not affect the UI as the binding engine is never triggered to re-evaluate the binding that is associated with the event. As a result, the error template is not removed once the errors have been cleared.

    Fix

    ViewModelBase.cs

    protected bool ClearErrors(string propertyName = "")
    {
      if (string.IsNullOrEmpty(propertyName))
      {
        Errors.Clear();
    
        // FIX::Raise ErrrorsChanged event
        OnErrorsChanged(propertyName);
    
        return true;
      }
    
      if (Errors.Remove(propertyName))
      {
        OnErrorsChanged(propertyName);
    
        return true;
      }
    
      return false;
    }
    

    3 Validation Error Border Around Parent of TextBox

    You have three possible options to fix this:

    1. Explicitly set Binding.ValidatesOnNotifyDataErrors to false (recommended).
    2. set Validation.ErrorTemplate to null (not recommended). Although I wouldn't recommend this solution (because it will still keep the binding busy with handling validation errors, which introduces overhead and performance costs), this solution is more convenient as you can use global implicit styles to set Validation.ErrorTemplate to {x:Null}.
    3. Change the INotifyDataErrorInfo.GetErrors implementation (not recommended). This will eleimintae the possibility to get all errors of the binding source. WPF is using this feature heavily (which in your case causes the red border around the UserControl (or the parent hosting ContentControl to be precise).

    Explanation

    The default value for Binding.ValidatesOnNotifyDataErrors is true. In addition, the Validation.ErrorTemplate attached property returns a default template that simply draws a red border around the target element.

    If a control binds to the data source object and not to a property on this object, the binding engine passes an empty string instead of a specific property name to the INotifyDataErrorInfo.GetErrors method. If this method is implemented to return all current errors of the instance, like your ViewModelBase does (and which is the recommended implementation),
    then the target has errors to display:

    <!-- 
      The Content property binding targets the whole data source object 
      and not a property of the data source. As a result, the binding engine can't query the errors 
      for a particular property and instead tries to get all errors of the source object.
      And because ValidatesOnNotifyDataErrors is TRUE by default, the binding on the Content property 
      will be validated.
    
      The following two bindings are equivalaent
    -->
    
    <ContentControl Name="NewSessionFormContentControl"
                    Content="{Binding m_SessionFormViewModel}" />
    
    
    <ContentControl Name="NewSessionFormContentControl"
                    Content="{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=True}" />
    
    // ViewModelBase: 
    // This is were all errors of the source object are returned 
    // in case the binding engine passes in an empty string
    public System.Collections.IEnumerable GetErrors(string propertyName)
      => string.IsNullOrWhiteSpace(propertyName)
        ? Errors.SelectMany(entry => entry.Value) // Return all errors
        : Errors.TryGetValue(propertyName, out IList<object> errors)
          ? (IEnumerable<object>)errors
          : new List<object>();
    

    Fix #1 (recommended)

    SessionView.xaml

    <!-- Disable validation for the particular binding and avoid related overhead -->
    <ContentControl Name="NewSessionFormContentControl"
                    Content="{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=False}" />
    

    Fix #2

    SessionView.xaml

    <!-- 
      Set Validation.ErrorTemplate to NULL. 
      The binding will still perform all the validation related operations
      but won't show the visual error feedback .
    -->
    <ContentControl Name="NewSessionFormContentControl"
                    Validation.ErrorTemplate="{x:Null}"
                    Content="{Binding m_SessionFormViewModel}" />
    

    Fix #3

    ViewModelBase.xaml

    // Don't support NULL and empty string queries
    public System.Collections.IEnumerable GetErrors(string propertyName)
      => string.IsNullOrWhiteSpace(propertyName)
        ? new List<object>() // Return no errors
        : Errors.TryGetValue(propertyName, out IList<object> errors)
          ? (IEnumerable<object>)errors
          : new List<object>();