Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

How to Auto-generate Multiple Custom Columns in a WPF DataGrid for Same Type Properties

1 Apr 2020CPOL2 min read 13.8K   127  
A generic workaround for auto-generating multiple custom columns in a WPF DataGrid in case where the data class has multiple properties of the same type
Auto-generating multiple custom columns in a WPF DataGrid in case where the data class has multiple properties of the same type doesn’t work like one would have expected because the custom control’s data context is not the property one intends to bind to, but the containing instance. So, the custom control or its view model doesn't know to which of the same-type properties to bind.

Introduction

There is an issue when auto-generating multiple custom columns in a WPF DataGrid in case where the data class has multiple properties of the same type (like Item here below).

C#
public class Item
{
   public Enum Prop0 { get; set; } = Enum.CustomEnum1;
   public Enum Prop1 { get; set; } = Enum.CustomEnum2;
}

The reason why it doesn’t work like one would have expected is that the custom control’s data context is not the property one intends to bind to (Prop0 or Prop1) but their containing instance (an instance of Item in our case). So, if you auto-generate custom columns, you cannot tell the custom control or its view model which of the two properties to bind to.

Workaround

A workaround I'd suggest, consists in making a facade for the data class, instances of which you intend(ed) to bind to in the data grid. The facade class would basically replicate the initial data class with the exception that the properties of the same type, which require custom date cells, would have individual types in the containing facade:

C#
public class ItemFacade
{
    private readonly Item item;
    public ItemFacade(Item item) => this.item = item;

    public EnumContainer0 Prop0 
    {
        get => new EnumContainer0 { Enum = this.item.Prop0 };
        set => this.item.Prop0 = value.Enum;
    }
    public EnumContainer1 Prop1
    {
        get => new EnumContainer1 { Enum = this.item.Prop1 };
        set => this.item.Prop1 = value.Enum;
    }
}

These individual specific types can either be sub-classes of the original property's type in question, if it is possible, or be sub-classes of a facade type thereof:

C#
public class EnumContainer
{
    public Enum Enum { get; set; }
}
public class EnumContainer0 : EnumContainer { }
public class EnumContainer1 : EnumContainer { }

You can even consider implicit cast in that case :).

The related XAML code will then look as below:

XML
Window x:Class="TestWpfDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestWpfDataGrid"
        xmlns:i="clr-namespace:System.Windows.Interactivity;
                 assembly=System.Windows.Interactivity" 
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DataGrid ItemsSource="{Binding Items}">
            <i:Interaction.Behaviors>
                <local:ColumnHeaderBehaviour/>
            </i:Interaction.Behaviors>
                <DataGrid.Resources>
                    <DataTemplate DataType="{x:Type local:EnumContainer0}">
                    <StackPanel>
                        <TextBlock Text="Custom "/>
                        <TextBlock Text="{Binding Prop0.Enum}"/>
                    </StackPanel>
                </DataTemplate>
                    <DataTemplate DataType="{x:Type local:EnumContainer1}">
                    <StackPanel>
                        <TextBlock Text="Custom "/> 
                        <TextBlock Text="{Binding Prop1.Enum}"/>
                    </StackPanel> 
                    </DataTemplate>
                </DataGrid.Resources>
        </DataGrid>
    </Grid>
</Window>

Note that you must bind to individual properties in the data templates, which is now possible as the individual properties have their own types and thus can have individual data templates.

The attached behavior in local:ColumnHeaderBehaviour can be as follows:

C#
public class ColumnHeaderBehaviour : Behavior<DataGrid>
    {
        protected override void OnAttached()
        {
            AssociatedObject.AutoGeneratingColumn += OnGeneratingColumn;
        }

        protected override void OnDetaching()
        {
            AssociatedObject.AutoGeneratingColumn -= OnGeneratingColumn;
        }

        private static void OnGeneratingColumn(object sender,
                                               DataGridAutoGeneratingColumnEventArgs eventArgs)
        {
            if (eventArgs.PropertyDescriptor is PropertyDescriptor descriptor)
            {
                var control = (DataGrid)sender;
                var resourceDictionary = control.Resources;
                var dataTemplate = resourceDictionary.Values
                    .OfType<DataTemplate>()
                    .Where(el => (Type)el.DataType == descriptor.PropertyType)
                    .FirstOrDefault();

                if (dataTemplate != null)
                {
                    var column = new DataGridTemplateColumn()
                    {
                        CellTemplate = dataTemplate,
                    };
                    eventArgs.Column = column;
                }
                eventArgs.Column.Header = descriptor.DisplayName ?? descriptor.Name;
            }
            else
            {
                eventArgs.Cancel = true;
            }
        }
    }

Note that the above behavior code works if the data templates are specified within the related data grid.

This facade class is fully testable. Typing it down and the tests thereof would certainly cost me less time than I've spent when searching for a solution.

Conclusion

A simple facade for the data object, just to have its properties typed differently, won't be sufficient. It's all about custom data cells, isn't it? So, there will be a control in the custom data cell, that will display and probably change the related property. So, it should also be aware if the property changes. In this way, we come to an idea that:

  • the data object facade should be a view model, and
  • the properties' facades should be view models too.

Moreover, it would be more secure to always use a view model as a data grid context.

History

  • 1st April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)