Search code examples
c#wpfdatagridcontroltemplate

WPF Datagrid, how to access validation errors from a ControlTemplate


In a WPF DataGrid, I want to show a validation result in a small box inside the cell.
I managed to do so for a single column by binding to the Validation.Errors data structure (see the code below).
This is what I got and it's pretty close to the desired outcome; now I want to implement it for all the columns.

Small validation message as a box in the cell

The problem

In order to make the solution reusable over multiple columns I tried to move it into a ControlTemplate. I couldn't find a way to establish the binding of Validation.Errors again from inside the control template (See the code below). As a result, the red label is always empty.

Empty error box: doesn't work

The working, single-column solution

The working solution is based on following code:

<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
    <DataGrid.Columns>

        <DataGridTemplateColumn Header="Name">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Grid>
                        <Label x:Name="x" Content="{Binding Name}"/>
                        <Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
                        Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>
                    </Grid>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>

    </DataGrid.Columns>
</DataGrid>

No need to read it all: here are the relevant parts

It works by binding the Label "x" to the Name property of my example datacontext.

<Label x:Name="x" Content="{Binding Name}"/>

Then, the error label in turn binds to the former Label (via its name) and gets the Validation.Errors information (graphics formats removed here for clearness).

<Label Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>

This proves that the result is achieveable, but this solution cannot be reused over multiple columns without repeating it over and over again.

Wrapping attempt

In order to have a reusable template, i tried to wrap all my cell contorls (label x and label with x's errors) into a ControlTemplate; it will be used by a Label component that is what i'll actually have on the grid.
The wrapping code is this (bewlow there is the complete code):

<Label Content="{Binding Name}">
    <Label.Template>
       <ControlTemplate TargetType="Label">
          //my controls
       </ControlTemplate>
    </Label.Template>
</Label>

About "my contols"

I had to change the line:

<Label x:Name="x" Content="{Binding Name}"/>

to this:

<Label x:Name="x" Content="{TemplateBinding Content}"/>

But the Label dedicated to the errors doesnt work anymore (graphics configuration removed):

Empty error box: doesn't work

I can guess that it doesn't work because only the content property is trasfered form the templated label to the inner label x; the content and not the entire 'state' of the property including the validation errors collection. But how can I access those errors then?

Code

<Window.DataContext>
    <local:ViewModel/>
</Window.DataContext>

<DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
    <DataGrid.Columns>

        <DataGridTemplateColumn Header="Name">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>

                    <Label Content="{Binding Name}">

                        <Label.Template>
                            <ControlTemplate TargetType="Label">
                                <Grid>
                                    <Label x:Name="x" Content="{TemplateBinding Content}"/>
                                    <Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
                                Content="{Binding ElementName='x', Path='(Validation.Errors)[0].ErrorContent'}"/>
                                </Grid>
                            </ControlTemplate>
                        </Label.Template>

                    </Label>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>

    </DataGrid.Columns>
</DataGrid>

DataContext

public class ViewModel
{
    public ObservableCollection<Person> People { get; } = new ObservableCollection<Person>() { new Person { Name = "Alan" } };
}

public class Person: INotifyDataErrorInfo
{
    public string Name { get; set; }

    public bool HasErrors => true;

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        yield return "Some error";
    }
}

Solution

  • Instead of Binding to Validation.Errors of Label 'x', you can refer to Validation.Errors of the TemplatedParent, i.e. Main Label. I was able to extract the ControlTemplate to window resource, and use this resource as Label Template, so we can reuse this template.

    <Window.Resources>
        <ControlTemplate TargetType="Label" x:Key="Lbl">
            <Grid>
                <Label x:Name="x" Content="{TemplateBinding Content}"/>
                <Label Padding="2" HorizontalAlignment="Right" VerticalAlignment="Top" Height="15" Width="44" FontSize="8" Foreground="White" Background="Red"
                       Content="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"/>
            </Grid>
        </ControlTemplate>
    </Window.Resources>
    
    <DataGrid ItemsSource="{Binding People}" AutoGenerateColumns="False" CanUserAddRows="False">
        <DataGrid.Columns>
    
            <DataGridTemplateColumn Header="Name">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
    
                        <Label Content="{Binding Name}"
                               Template="{StaticResource Lbl}">
    
                        </Label>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
    
        </DataGrid.Columns>
    </DataGrid>