Search code examples
c#wpfxamldata-binding

Set error template of a specific child of a UserControl


I am trying to understand how to add an error template to a child of a UserControl, and I saw other similar questions but I still could not wrap my head around this.

My goal is to create a UserControl that has a TextBox and a Label, with an optional readonly TextBox. I already implemented a lot of features and everything is working fine, except that the whole UserControl gets a red border in case of validation errors.

I tried adding Validation.ValidationAdornerSite to UserControl's root element, but it didn't work.

Also, the idea is that the validation should be done inside Model classes, so I don't want to put any validation mechanism inside the UserControl.

In this case, I don't really understand how the binding system works. It makes sense that the whole UserControl is highlighted on errors, since it is a UserControl's property that is bound to the Model's property, but I expected to be able to "redirect" the error to a child with Validation.ValidationAdornerSite. What am I missing or misunderstanding? For example, how can I highlight the TextBox "txt1"?

Thanks in advance!

This is the smallest example I could come up with:

MyControl.xaml

<UserControl x:Class="MyNamespace.MyControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel x:Name="ctRoot">
        <Label x:Name="lbTitle" Content="MyLabel"/>
        <TextBox x:Name="txt1" Text="{Binding Text}"/>
        <TextBox x:Name="txt2"/>
    </StackPanel>
</UserControl>

MyControl.xaml.cs

public partial class MyControl : UserControl
{
    public MyControl()
    {
        InitializeComponent();
        ctRoot.DataContext = this;
    }

    public static readonly DependencyProperty TextProperty = DependencyProperty
        .Register("Text", typeof(string), typeof(MyControl), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
}

Model.cs

public class Model : INotifyDataErrorInfo, INotifyPropertyChanged
{
    private string m_prop;
    public string MyProp
    {
        get => m_prop;

        set
        {
            m_errors.Clear();
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MyProp)));

            if (value.Length == 0)
            {
                m_errors.Add("Cannot be empty");
                ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MyProp)));
            }

            m_prop = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyProp)));
        }
    }

    private List<string> m_errors = new List<string>();
    public bool HasErrors => m_errors.Count > 0;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    public event PropertyChangedEventHandler PropertyChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        return m_errors;
    }
}

MyWindow.xaml

<Window x:Class="MyNamespace.MyWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MyNamespace"
        Title="MyWindow" Width="200" Height="200">
    <StackPanel>
        <local:MyControl Text="{Binding MyProp}" Margin="10"/>
    </StackPanel>
</Window>

MyWindow.xaml.cs

public partial class MyWindow : Window
{
    public Model model = new Model();

    public MyWindow()
    {
        InitializeComponent();
        DataContext = model;
    }
}

Solution

  • You can use Validation.ValidationAdornerSiteFor to change which element appears to indicate that an error occurred.

    <UserControl x:Class="WpfApp1.MyControl"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 Name="myControl">
        <StackPanel x:Name="ctRoot">
            <Label x:Name="lbTitle" Content="MyLabel"/>
            <TextBox x:Name="txt1" Text="{Binding Text, ElementName=myControl}"
                     Validation.ValidationAdornerSiteFor="{Binding ElementName=myControl}"/>
            <TextBox x:Name="txt2"/>
        </StackPanel>
    </UserControl>