I built a UserControl
based on a DataGrid
in order to add filtering which I don't detail. It is quite complex, but works well (I'll have to figure out how to remove lots of code-behind later on).
I would like to extend some styles (each control which uses this UserControl
will have its own converters to change how the row are displayed with separators etc.) but I get the following error:
System.Windows.Markup.XamlParseException: ''Set property 'System.Windows.ResourceDictionary.DeferrableContent' threw an exception.
InvalidOperationException: Cannot re-initialize ResourceDictionary instance.
<DataGrid x:Class="CustomDataGrid">
<DataGrid.Resources>
<!-- Data Grid Row Header -->
<Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
<!-- I would like to use the specific ControlTemplate (e.g. add row separators) defined in the control which uses this UserControl -->
</Style>
</DataGrid.Resources>
<DataGrid.Style>
<!-- DataGrid -->
<Style TargetType="{x:Type DataGrid}">
<Setter Property="RowStyle" Value="{StaticResource DataGridRowStyle}" />
</Style>
</DataGrid.Style>
</DataGrid>
<controls:CustomDataGrid>
<controls:CustomDataGrid.Resources>
<!-- Data Grid Row Header -->
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridRow}">
<!-- Border, SelectiveScrollingGrid and DataGridCellsPresenter included in the ControlTemplate with specific value converters, etc. -->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</controls:CustomDataGrid.Resources>
</controls:CustomDataGrid>
You should convert your control to a custom control that defines a default Style
in Generic.xaml. This way, the client of your control can define a Style
without removing your defaults set by your internal Style
.
This way the client can provide the ControlTemplate
for the DaraGridRow
simply by defining a custom Style
. This will be an alternative to the DtaGridRowTemplate
property that you have introduced.
It's also more robust to register a dependency property changed callback with the CustomDataGRid.DataGridRowTemplate
property that delegates the new ControlTemplate
to the CustomDataGrid.Template
property. This link between the two properties is a crucial link. You don't want this link to be broken when the client defines a Style
that sets OverridesDefaultStyle
to true
. This will be the case if this link is established from a Style
(that's a perfect example why code-behind is really necessary - because you were saying in your question that you will have to remove all code-behind from your control).
The fixed and improved version of your control could look as follows:
CustomDataGrid.cs
public class CustomDataGrid : DataGrid
{
public static readonly DependencyProperty DataGridRowTemplateProperty = DependencyProperty.Register(
nameof(DataGridRowTemplate),
typeof(ControlTemplate),
typeof(CustomDataGrid),
new PropertyMetadata(default));
public ControlTemplate DataGridRowTemplate
{
get => GetValue(DataGridRowTemplateProperty) as ControlTemplate;
set => SetValue(DataGridRowTemplateProperty, value);
}
public static readonly RoutedCommand ColumnHeaderFilterCommand
= new RoutedCommand(nameof(ColumnHeaderFilterCommand), typeof(CustomDataGrid));
static CustomDataGrid()
{
// Register the default Style for this control
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomDataGrid), new FrameworkPropertyMetadata(typeof(CustomDataGrid)));
}
public CustomDataGrid()
{
var columnheaderFilterCommandBinding = new CommandBinding(
ColumnHeaderFilterCommand,
ExecutedColumnHeaderFilterCommand,
CanExecuteColumnHeaderFilterCommand);
this.CommandBindings.Add(columnheaderFilterCommandBinding);
}
private void CanExecuteColumnHeaderFilterCommand(object sender, CanExecuteRoutedEventArgs e)
{
object commandParameter = e.Paramter;
e.CanExecute = true;
}
private void ExecutedColumnHeaderFilterCommand(object sender, ExecutedRoutedEventArgs e)
{
object commandParameter = e.Paramter;
// TODO::Execute header click action
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
if (element is DataGridRow dataGridRow
&& this.DataGridRowTemplate is not null)
{
dataGridRow.Template = this.DataGridRowTemplate;
}
base.PrepareContainerForItemOverride(element, item);
}
}
Generic.xaml
Add a Themes folder to your project root. Then add a XAML ResourceDictionary
file to this folder and name the file Generic.xaml.
<ResourceDictionary>
<!--
The default Style for CustomDataGrid.
Unless an external Style sets OverridesDefaultStyle to TRUE,
external styles will be merged into the default Style.
This way you don't have to be afraid of external Styles.
-->
<Style TargetType="CustomDataGrid"
BasedOn="{StaticResource {x:Type DataGrid}}">
<!-- Set defaults here -->
<!-- For example, set the default template for the `DataGridRow` -->
<Setter Property="DataGridRowTemplate">
<Setter.Value>
<Controltemplate TargetType="DataGirdRow">
...
</ControlTemplate>
</Setter.Value>
</Setter>
<!-- Set up the ColumnHeader using routed commands -->
<Setter Property="ColumnHeaderStyle">
<Setter.Value>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Command"
Value="{x:Static CustomDataGrid.ColumnHeaderFilterCommand}" />
</Style>
</Setter.VAlue>
</Setter>
</Style>
</ResourceDictionary>
Now the client can either set the template by defining a custom Style, which will no longer break your internal default style, or by setting the DataGridRowTemplate
property.