1

I'd like to display a table of items in WPF. In general that's not that hard, except that:

  • I know, which columns I want to display only at runtime (user in a way defines them), and
  • I'd like to avoid using codebehind: the XAML, where I want to display the table is inside a DataTemplate, which is inside another DataTemplate etc. Introducing CodeBehind there will be problematic (though I will do that if I have no other options).

I though of using WPF's DataGrid, which is smart enough to extract column names in runtime (depending on properties of collection items), but I know which columns I want to display only at runtime - I would have to create object with specific properties at runtime, what is also problematic (if possible).

Also, I don't really need to use specifically the DataGrid, as this will be simple table of strings for preview only - it may be as well displayed, say, inside Grid with ItemsControl - I just need to provide a view with columns and rows.

So the questions are:

  • How to customize displayed columns in DataGrid only from ViewModel (DataContext) and XAML?, or
  • How to display tabular for preview data when columns are known at runtime only?
Spook
  • 25,318
  • 18
  • 90
  • 167
  • So what exactly will you be binding your itemsource to? Different object types? A csv? – Marisa Jan 29 '18 at 20:15
  • Simply, a list of list of strings. – Spook Jan 29 '18 at 21:01
  • Possible duplicate of [How do I bind a WPF DataGrid to a variable number of columns?](https://stackoverflow.com/questions/320089/how-do-i-bind-a-wpf-datagrid-to-a-variable-number-of-columns). My preferred approach however is to [use DataTable](https://stackoverflow.com/a/44206066/1506454) – ASh Jan 30 '18 at 07:13

1 Answers1

1

Since it seems like there is interest for this problem and I found solution myself, here it goes (attached properties rulez!)

For clarity, I created model classes to wrap list of list of strings:

public class TableDataRow
{
    public TableDataRow(List<string> cells)
    {
        Cells = cells;
    }

    public List<string> Cells { get; }
}

public class TableData
{
    public TableData(List<string> columnHeaders, List<TableDataRow> rows)
    {
        for (int i = 0; i < rows.Count; i++)
            if (rows[i].Cells.Count != columnHeaders.Count)
                throw new ArgumentException(nameof(rows));

        ColumnHeaders = columnHeaders;
        Rows = rows;
    }

    public List<string> ColumnHeaders { get; }
    public List<TableDataRow> Rows { get; }
}

Now we define the attached property:

public static class DataGridHelper
{
    private static void TableDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dataGrid = d as DataGrid;
        var tableData = e.NewValue as TableData;
        if (dataGrid != null && tableData != null)
        {
            dataGrid.Columns.Clear();
            for (int i = 0; i < tableData.ColumnHeaders.Count; i++)
            {
                DataGridColumn column = new DataGridTextColumn
                {
                    Binding = new Binding($"Cells[{i}]"),
                    Header = tableData.ColumnHeaders[i]
                };
                dataGrid.Columns.Add(column);
            }

            dataGrid.ItemsSource = tableData.Rows;
        }
    }

    public static TableData GetTableData(DependencyObject obj)
    {
        return (TableData)obj.GetValue(TableDataProperty);
    }

    public static void SetTableData(DependencyObject obj, TableData value)
    {
        obj.SetValue(TableDataProperty, value);
    }

    // Using a DependencyProperty as the backing store for TableData.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TableDataProperty =
        DependencyProperty.RegisterAttached("TableData", 
            typeof(TableData), 
            typeof(DataGridHelper), 
            new PropertyMetadata(null, TableDataChanged));
}

The usage is trivial:

(...)
xmlns:h="clr-namespace:<namespace-of-DataGridHelper>"
(...)    

<DataGrid AutoGenerateColumns="False" h:DataGridHelper.TableData="{Binding ResultData}" />

Obviously DataContext must publish TableData via ResultData. Don't forget about AutoGenerateColumns, otherwise you'll receive additional column "Cells".

Spook
  • 25,318
  • 18
  • 90
  • 167