Search code examples
c#data-bindingwinui-3

Binding data with unknown columns to a DataGrid in WinUI 3


I have my data stored in a Dictionary<string, List<string>>. I want to bind it to a datagrid in WinUI 3. The number of elements in the List<string> is variable for each key and unknown at compile time.

I apologise for being lengthy, but I want to provide as much context as possible.

Since the number of columns in my data is unknown, I excluded the recommended approach of creating a custom class. I also had to exclude the use of ExpandoObject class. And, if I'm not mistaken, a DataTable cannot be directly bound to a datagrid in WinUI 3.

I know I need an ObservableCollection as my ItemSource so I'm using a class ObservableDictionary. Not recommended by many based on what I could read.

The keys in my dictionary represent the first column of my datagrid (the header being "Tag"). Each element in the List<string>, properly converted (see method ConvertDictionaryToTagData) will become headers and elements of my datagrid. Below is an example of the original data (actual data comes from an external file):

public ObservableDictionary<string, List<string>> Source = new();
...
Source.Add("AAA", new List<string> { "Prop1", "Prop2" });
Source.Add("BBB", new List<string> { "Prop1", "Prop4" });
Source.Add("CCC", new List<string> { "Prop3", "Prop1", "Prop5", "Prop2" });
Source.Add("DDD", new List<string> { "Prop6" });
Source.Add("EEE", new List<string> { "Prop1", "Prop7" });

The result I'm expecting in my datagrid is:

Tag Prop1 Prop2 Prop3 Prop4 Prop5 Prop6 Prop7
AAA true true false false false false false
BBB true false false true false false false
CCC true true true false true false false
DDD false false false false false true false
EEE true false false false false false true

These are my attempts at a solution. I created a class TagData and a ConvertDictionaryToTagData method for converting the data:

public class TagData
{
    public string Tag {  get; set; }
    public bool[] Values { get; set; }
}
private ObservableCollection<TagData> ConvertDictionaryToTagData(ObservableDictionary<string, List<string>> data)
{
    var allHeaders = data.SelectMany(x => x.Value).Distinct().Order().ToList();

    var tagDataList = new ObservableCollection<TagData>();

    foreach (var item in data)
    {
        var tag = new TagData
        {
            Tag = item.Key,
            Values = new bool[allHeaders.Count] headers
        };

        for (int i = 0; i < allHeaders.Count; i++)
        {
            tag.Values[i] = item.Value.Contains(allHeaders[i]);
        }

        tagDataList.Add(tag);
    }

    return tagDataList;
}

I'm preparing my data:

ObservableCollection<TagData> Data = ConvertDictionaryToTagData(Source);

My datagrid is as follows:

<controls:DataGrid x:Name="dataGrid"
                   AutoGenerateColumns="True"
                   GridLinesVisibility="Horizontal"
                   ItemsSource="{x:Bind ViewModel.Data, Mode=OneWay}">

But all I get is the headers Tag and Values with no data.

I modified the datagrid to:

<controls:DataGrid x:Name="dataGrid"
                   AutoGenerateColumns="False"
                   GridLinesVisibility="Horizontal"
                   ItemsSource="{x:Bind ViewModel.Data, Mode=OneWay}">
 <controls:DataGridTextColumn Binding="{Binding [Tag]}"
                              Header="Tag" />

with no luck. I also used the AutoGeneratingColumn event:

<controls:DataGrid x:Name="dataGrid"
                   AutoGenerateColumns="True"
                   AutoGeneratingColumn="DataGrid_AutoGeneratingColumn"
                   GridLinesVisibility="Horizontal"
                   ItemsSource="{x:Bind ViewModel.Data, Mode=OneWay}">
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    var dataGrid = sender as DataGrid;

    e.Cancel = true;
    if (dataGrid != null)
    {
        DataGridTextColumn tag = new()
        {
            Header = "Tag",
            Binding = new Binding()
            {
                Source = ViewModel.Data,
                Path = new PropertyPath("Tag"),
                Mode = BindingMode.OneWay
            }
        };
        dataGrid.Columns.Add(tag);

        var allHeaders = ViewModel.Source.SelectMany(x => x.Value).Distinct().Order().ToList();
        foreach (string str in allHeaders)
        {
            DataGridCheckBoxColumn column = new()
            {
                Header = str,
                Binding = new Binding()
                {
                    Path = new PropertyPath(string.Format("Values")),
                    Mode = BindingMode.OneWay
                }
            };
            dataGrid.Columns.Add(column);
        }
    }
}

This time I get the right headers twice (created once for Tag and one more time for Values). But, again, no data. And I had to use the e.Cancel = true; at the beginning of the event to avoid the creation of additional headers "Tag"and "Values".

I'm stuck because I don't know how to create the content of my columns or, better, how to bind those columns to the source data. I'm indeed unfamiliar with the binding part:

DataGridTextColumn tag = new()
        {
            Header = "Tag",
            Binding = new Binding()
            {
                Source = ViewModel.Data,
                Path = new PropertyPath("Tag"),
                Mode = BindingMode.OneWay
            }
        };

and its correct syntax.

I'd appreciate any suggestions I can get on how to solve this problem I'm facing. I'm flexible to changing the type I use for storing the data if that can help.


Solution

  • I did some tests. It seems my approach was correct from the beginning (use of TagData class). I just had to re-build my solution.

    You can indeed assign your ItemSource in both XAML and code behind. In case of different assignments (XAML and OnNavigatedTo() method), the one in XAML seems to prevail.

    AutoGenerateColumns is set to False.