wpfuser-controlsresourcedictionarytargettype

WPF how to extend UserControl Resource Style


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>

Solution

  • 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.