WPF DataGrid with a one click ComboBox showing enumeration values sorted by enumeration names

Viewed 225

Requirements

I would like to add a column with comboboxes to a WPF DataGrid meeting the following requirements:

  1. The value displayed in the ComboBox should be the name of the enumeration constant
  2. The entries in the ComboBox should be sorted by the name of the enumeration constant
  3. The property type in the underlying object should be enum, not string
  4. The number of clicks should be reduced. When I use DataGridComboBoxColumn, I need about 4 clicks to change a value.
  5. I actually like code behind solutions, although XAML based solutions are fine too.
  6. It should run under .NET 5 WPF

Sample Application

The application uses the code provided in the DataGridComboBoxColumn. It works, but has 2 problems:

  1. the Details DropDown lists the entries alphabetically. In my real application I have many more entries and it is very difficult to find the right one when they are not sorted.

  2. It takes 4 mouse clicks to change a ComboBox value.

Code

XAML:

<Window x:Class="SortedComboBoxDataGrid.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:SortedComboBoxDataGrid"
        xmlns:core="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">
  <Window.Resources>
    <CollectionViewSource x:Key="SamplesViewSource"  CollectionViewType="ListCollectionView"/>
    
    <ObjectDataProvider x:Key="myEnum" MethodName="GetValues" ObjectType="{x:Type core:Enum}">
      <ObjectDataProvider.MethodParameters>
        <x:Type Type="local:DetailEnum"/>
      </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
  </Window.Resources>
  
  <DataGrid x:Name="MainDataGrid" DataContext="{StaticResource SamplesViewSource}" ItemsSource="{Binding}"
                AutoGenerateColumns="False">
    <DataGrid.Columns>
      <DataGridTextColumn Binding="{Binding Path=SomeText}" Header="SomeText"/>
      <DataGridComboBoxColumn Header="Detail" SelectedItemBinding="{Binding Detail}" 
                                ItemsSource="{Binding Source={StaticResource myEnum}}"/>
    </DataGrid.Columns>
  </DataGrid>
</Window>

Enum DetailEnum:

namespace SortedComboBoxDataGrid {
  public enum DetailEnum {
    No,
    Some,
    Many,
    All
  }

Sample class:

  public class Sample {
    public string SomeText { get; set; }
    public DetailEnum Detail { get; set; }

    public Sample(string someText, DetailEnum detail) {
      SomeText = someText;
      Detail = detail;
    }
  }
}

Window code behind:

using System.Collections.Generic;
using System.Windows;

namespace SortedComboBoxDataGrid {
  public partial class MainWindow: Window {
    public MainWindow() {
      InitializeComponent();

      var samples = new List<Sample>() { 
        new Sample("first", DetailEnum.All),
        new Sample("second", DetailEnum.Many),
        new Sample("any", DetailEnum.Some),
        new Sample("last", DetailEnum.No),
      };

      var samplesViewSource = ((System.Windows.Data.CollectionViewSource)(FindResource("SamplesViewSource")));
      samplesViewSource.Source = samples;
    }
  }
}

What I have tried already

I tried Displaying sorted enum values in a ComboBox. This sorts enums nicely, but to do so it converts the enum values into strings, then sorts those strings. If the user clicks on a different entry, the grid returns a string and not an enumeration value.

I tried various solutions I found on stackoverflow to reduce the number of tricks, but could not get one working properly with sorted (!) enums.

I wonder if it would be better to use for the ComboBox a list of class instances having the enum value and the enum name as its properties instead of a List ?

  public class DetailEnumClass {
    public DetailEnum EnumValue { get; set; }
    public string EnumName { get; set; }
  }

Please read this before marking this question as a duplicate

I am aware there are already many answers on stackoverflow regarding one or the other problem I mentioned here. However, I am not able to come up with a working solution which covers all requirements. So please only mark this question as a dupplicate if you have found an answer which provides full code for the complete problem. Thanks.

2 Answers

You could use a DataGridTemplateColumn with a ComboBox that binds to a sorted IEnumerable<DetailEnum> that you create yourself in the code-behind:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        Resources.Add("enums",
            Enum.GetValues(typeof(DetailEnum)).Cast<DetailEnum>().OrderBy(x => x.ToString()));
        InitializeComponent();
        ...
    }
}

XAML:

<DataGrid x:Name="MainDataGrid" AutoGenerateColumns="False">
    <DataGrid.Resources>
        <DataTemplate x:Key="dt">
            <ComboBox ItemsSource="{StaticResource enums}"
                      SelectedItem="{Binding Detail, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Path=SomeText}" Header="SomeText"/>
        <DataGridTemplateColumn Header="Detail" CellTemplate="{StaticResource dt}" CellEditingTemplate="{StaticResource dt}" />
    </DataGrid.Columns>
</DataGrid>

When I used the answer I accepted, I run into an interesting problem. The following code works fine:

<Window.Resources>
  <CollectionViewSource x:Key="SamplesViewSource" 
    CollectionViewType="ListCollectionView"/>
</Window.Resources>

But I had to merge in some other resources. The first step was to change the code above like this:

<Window.Resources>
  <ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        
    </ResourceDictionary.MergedDictionaries>
    <CollectionViewSource x:Key="SamplesViewSource"  
      CollectionViewType="ListCollectionView"/>
  </ResourceDictionary>
</Window.Resources>

After which I could not run the application anymore. I got the error message "Cannot find resource named 'enums'. Resource names are case sensitive."

It took me 2 days to figure out what is goin on here. Normally, one does not need to add <ResourceDictionary> to <Window.Resources>, XAML will do that for you behind the scene. To be able to use MergedDictionaries, you have to do it yourself. Which changes how XAML behave. Maybe I should add here, I do hate XAML since over 10 years because of this kind of problems.

I guess with <ResourceDictionary> XAML removes the autocreated ResourceDictionary with a new one and the content of the old one is lost, i.e. enums, which was added to Windows.Resources before calling InitializeComponent().

I thought no big deal and tried to add enums after InitializeComponent(), because enums is only needed once I fill data into the DataGrid.DataContext with the following line:

samplesViewSource.Source = samples;

But I still got the same error message. My guess is that the ComboBox's DataTemplate defined in DataGrid.Resources gets executed when the DataGrid gets created. At this time, enums is not added yet.

I could get rid of the error by using a dynamic instead static resource:

<ComboBox ItemsSource="{DynamicResource enums}"

At this point I was really annoyed with all the problems XAML gives and decided not to use Resources for ComboBox.ItemsSource but the DataContext.

Which, of course, gave another problem. I couldn't find it in the documentation, but with the debugger I found out that ComboBox.DataContext is a Sample and not the DataContext inherited from the DataGrid, which makes sense. Each row needs access to a different item in DataGrid.ItemsSource.

After another day, I figured out how to access the DataGrid.DataContext from the Combobox DataTemplate:

<DataTemplate x:Key="dt">
  <ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, 
    AncestorType={x:Type DataGrid}}, 
    Path=DataContext.DetailEnums}" 
    SelectedItem="{Binding Detail, UpdateSourceTrigger=PropertyChanged}" 
</DataTemplate>

Here is the complete code for you how to use ComboBoxes in a DataGrid without using Resources to hold the data, if you are like me and try to minimise the problems XAML creates:

<Window x:Class="SortedComboBoxDataGrid.Window2"
        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"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight">

  <DataGrid x:Name="MainDataGrid" ItemsSource="{Binding SamplesViewSource}" AutoGenerateColumns="False">
    <DataGrid.Resources>
      <DataTemplate x:Key="dt">
        <ComboBox ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, 
          Path=DataContext.DetailEnums}" 
                  SelectedItem="{Binding Detail, UpdateSourceTrigger=PropertyChanged}"/>
      </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
      <DataGridTextColumn Binding="{Binding Path=SomeText}" Header="SomeText"/>
      <DataGridTemplateColumn Header="Detail" CellTemplate="{StaticResource dt}" CellEditingTemplate="{StaticResource dt}" />
    </DataGrid.Columns>
  </DataGrid>
</Window>

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Data;


namespace SortedComboBoxDataGrid {
  public partial class Window2: Window {
    public record MainDataGridDataContext(ListCollectionView SamplesViewSource, DetailEnum[] DetailEnums);

    public Window2() {
      InitializeComponent();

      var samples = new List<Sample>() {
        new Sample("first", DetailEnum.All),
        new Sample("second", DetailEnum.Many),
        new Sample("any", DetailEnum.Some),
        new Sample("last", DetailEnum.No),
      };
      var samplesViewSource = new ListCollectionView(samples);

      var detailEnums = Enum.GetValues(typeof(DetailEnum)).Cast<DetailEnum>().OrderBy(x => x.ToString()).ToArray(); ;
      MainDataGrid.DataContext = new MainDataGridDataContext(samplesViewSource, detailEnums);
    }
  }
}
Related