128

My WPF application generates sets of data which may have a different number of columns each time. Included in the output is a description of each column that will be used to apply formatting. A simplified version of the output might be something like:

class Data
{
    IList<ColumnDescription> ColumnDescriptions { get; set; }
    string[][] Rows { get; set; }
}

This class is set as the DataContext on a WPF DataGrid but I actually create the columns programmatically:

for (int i = 0; i < data.ColumnDescriptions.Count; i++)
{
    dataGrid.Columns.Add(new DataGridTextColumn
    {
        Header = data.ColumnDescriptions[i].Name,
        Binding = new Binding(string.Format("[{0}]", i))
    });
}

Is there any way to replace this code with data bindings in the XAML file instead?

fedab
  • 978
  • 11
  • 38
Generic Error
  • 4,437
  • 6
  • 28
  • 26

8 Answers8

129

Here's a workaround for Binding Columns in the DataGrid. Since the Columns property is ReadOnly, like everyone noticed, I made an Attached Property called BindableColumns which updates the Columns in the DataGrid everytime the collection changes through the CollectionChanged event.

If we have this Collection of DataGridColumn's

public ObservableCollection<DataGridColumn> ColumnCollection
{
    get;
    private set;
}

Then we can bind BindableColumns to the ColumnCollection like this

<DataGrid Name="dataGrid"
          local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
          AutoGenerateColumns="False"
          ...>

The Attached Property BindableColumns

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;
        ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (columns == null)
        {
            return;
        }
        foreach (DataGridColumn column in columns)
        {
            dataGrid.Columns.Add(column);
        }
        columns.CollectionChanged += (sender, e2) =>
        {
            NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
            if (ne.Action == NotifyCollectionChangedAction.Reset)
            {
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Add)
            {
                foreach (DataGridColumn column in ne.NewItems)
                {
                    dataGrid.Columns.Add(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Move)
            {
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
            }
            else if (ne.Action == NotifyCollectionChangedAction.Remove)
            {
                foreach (DataGridColumn column in ne.OldItems)
                {
                    dataGrid.Columns.Remove(column);
                }
            }
            else if (ne.Action == NotifyCollectionChangedAction.Replace)
            {
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
            }
        };
    }
    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }
    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • I agree, I like this solution best. – Jaime Nov 27 '12 at 19:45
  • 1
    nice solution for MVVM pattern – WPFKK Apr 18 '13 at 14:17
  • 2
    A perfect solution! Probably you need to do a few other things in BindableColumnsPropertyChanged: 1. Check dataGrid for null before accessing it and throw an exception with good explanation about binding only to DataGrid. 2. Check e.OldValue for null and unsubscribe from CollectionChanged event to prevent memory leaks. Just for your convince. – Mike Eshva Oct 23 '13 at 13:01
  • This is what I was looking for. Best answer for adding dynamic columns to datagrid – Nikhil Chavan Dec 28 '13 at 06:14
  • 3
    You register an event handler with the `CollectionChanged` event of the columns collection, however you never unregister it. That way, the `DataGrid` will be kept alive for as long as the view-model exists, even if the control template that contained the `DataGrid` in the first place has been replaced meanwhile. Is there any guaranteed way of unregistering that event handler again when the `DataGrid` is not required any longer? – O. R. Mapper Dec 29 '13 at 00:06
  • 1
    @O. R. Mapper: Theoretically there is but it does not work: WeakEventManager, NotifyCollectionChangedEventArgs>.AddHandler(columns, "CollectionChanged", (s, ne) => { switch.... }); – too Jul 22 '14 at 10:09
  • 7
    It is not bes solution. The main reason is that you are using UI classes in ViewModel. Also it will not work when you try to create some page switching. When switching back to page with such datagrid you will got an expection in line `dataGrid.Columns.Add(column)` DataGridColumn with Header 'X' already exists in the Columns collection of a DataGrid. DataGrids cannot share columns and cannot contain duplicate column instances. – Ruslan F. Oct 06 '15 at 15:12
  • 2
    @RuslanF. To handle switching exchange the `foreach (DataGridColumn column in columns) { dataGrid.Columns.Add(column); }` part with `foreach (var column in columns) { var dataGridOwnerProperty = column.GetType().GetProperty("DataGridOwner", BindingFlags.Instance | BindingFlags.NonPublic); if( dataGridOwnerProperty != null) dataGridOwnerProperty.SetValue(column, null); dataGrid.Columns.Add(column); }` Sry can't get linebreak to work – Rune Andersen Oct 31 '17 at 09:11
  • I am also trying to bind to a property of the "selected" column from a cell's context menu. How am I able to do this with this approach? – Matthew S Jul 12 '18 at 19:48
  • The code doesn't handle inserts properly. all columns you insert into the databound collection are inserted as the last column. To fix it change the handling of `NotifyCollectionChangedAction.Add` to `else if (ne.Action == NotifyCollectionChangedAction.Add) { var index = ne.NewStartingIndex; foreach (DataGridColumn column in ne.NewItems) { dataGrid.Columns.Insert(index++, column); } }` – stambikk Oct 09 '19 at 19:37
  • To get a complete example not having the multiple header issue, see also this answer: https://stackoverflow.com/a/57478201/3142379 The issue happened for me when using a UserControl containing a DataGrid with BindableColumns in multiple other UserControls. – Jessica Feb 09 '21 at 07:09
19

I've continued my research and have not found any reasonable way to do this. The Columns property on the DataGrid isn't something I can bind against, in fact it's read only.

Bryan suggested something might be done with AutoGenerateColumns so I had a look. It uses simple .Net reflection to look at the properties of the objects in ItemsSource and generates a column for each one. Perhaps I could generate a type on the fly with a property for each column but this is getting way off track.

Since this problem is so easily sovled in code I will stick with a simple extension method I call whenever the data context is updated with new columns:

public static void GenerateColumns(this DataGrid dataGrid, IEnumerable<ColumnSchema> columns)
{
    dataGrid.Columns.Clear();

    int index = 0;
    foreach (var column in columns)
    {
        dataGrid.Columns.Add(new DataGridTextColumn
        {
            Header = column.Name,
            Binding = new Binding(string.Format("[{0}]", index++))
        });
    }
}

// E.g. myGrid.GenerateColumns(schema);
TheCloudlessSky
  • 18,608
  • 15
  • 75
  • 116
Generic Error
  • 4,437
  • 6
  • 28
  • 26
  • 1
    The highest voted and accepted solution is not the best one! Two years later the answer would be: http://msmvps.com/blogs/deborahk/archive/2011/01/23/populating-a-datagrid-with-dynamic-columns-in-a-silverlight-application-using-mvvm.aspx – Mikhail Poda Apr 18 '11 at 18:08
  • 4
    No, It would not. Not the provided link anyway, because the result of that solution is completely different! – 321X Aug 10 '11 at 19:59
  • 2
    Seems like Mealek's solution is much more universal, and is useful in situations where direct use of C# code is problematic, e.g. in ControlTemplates. – EFraim Dec 07 '11 at 18:59
  • 3
    here is the link: http://blogs.msmvps.com/deborahk/populating-a-datagrid-with-dynamic-columns-in-a-silverlight-application-using-mvvm/ – Mikhail Poda Sep 16 '15 at 13:20
9

I have found a blog article by Deborah Kurata with a nice trick how to show variable number of columns in a DataGrid:

Populating a DataGrid with Dynamic Columns in a Silverlight Application using MVVM

Basically, she creates a DataGridTemplateColumn and puts ItemsControl inside that displays multiple columns.

akjoshi
  • 15,374
  • 13
  • 103
  • 121
Lukas Cenovsky
  • 5,476
  • 2
  • 31
  • 39
6

I managed to make it possible to dynamically add a column using just a line of code like this:

MyItemsCollection.AddPropertyDescriptor(
    new DynamicPropertyDescriptor<User, int>("Age", x => x.Age));

Regarding to the question, this is not a XAML-based solution (since as mentioned there is no reasonable way to do it), neither it is a solution which would operate directly with DataGrid.Columns. It actually operates with DataGrid bound ItemsSource, which implements ITypedList and as such provides custom methods for PropertyDescriptor retrieval. In one place in code you can define "data rows" and "data columns" for your grid.

If you would have:

IList<string> ColumnNames { get; set; }
//dict.key is column name, dict.value is value
Dictionary<string, string> Rows { get; set; }

you could use for example:

var descriptors= new List<PropertyDescriptor>();
//retrieve column name from preprepared list or retrieve from one of the items in dictionary
foreach(var columnName in ColumnNames)
    descriptors.Add(new DynamicPropertyDescriptor<Dictionary, string>(ColumnName, x => x[columnName]))
MyItemsCollection = new DynamicDataGridSource(Rows, descriptors) 

and your grid using binding to MyItemsCollection would be populated with corresponding columns. Those columns can be modified (new added or existing removed) at runtime dynamically and grid will automatically refresh it's columns collection.

DynamicPropertyDescriptor mentioned above is just an upgrade to regular PropertyDescriptor and provides strongly-typed columns definition with some additional options. DynamicDataGridSource would otherwise work just fine event with basic PropertyDescriptor.

doblak
  • 3,036
  • 1
  • 27
  • 22
3

Made a version of the accepted answer that handles unsubscription.

public class DataGridColumnsBehavior
{
    public static readonly DependencyProperty BindableColumnsProperty =
        DependencyProperty.RegisterAttached("BindableColumns",
                                            typeof(ObservableCollection<DataGridColumn>),
                                            typeof(DataGridColumnsBehavior),
                                            new UIPropertyMetadata(null, BindableColumnsPropertyChanged));

    /// <summary>Collection to store collection change handlers - to be able to unsubscribe later.</summary>
    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> _handlers;

    static DataGridColumnsBehavior()
    {
        _handlers = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();
    }

    private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = source as DataGrid;

        ObservableCollection<DataGridColumn> oldColumns = e.OldValue as ObservableCollection<DataGridColumn>;
        if (oldColumns != null)
        {
            // Remove all columns.
            dataGrid.Columns.Clear();

            // Unsubscribe from old collection.
            NotifyCollectionChangedEventHandler h;
            if (_handlers.TryGetValue(dataGrid, out h))
            {
                oldColumns.CollectionChanged -= h;
                _handlers.Remove(dataGrid);
            }
        }

        ObservableCollection<DataGridColumn> newColumns = e.NewValue as ObservableCollection<DataGridColumn>;
        dataGrid.Columns.Clear();
        if (newColumns != null)
        {
            // Add columns from this source.
            foreach (DataGridColumn column in newColumns)
                dataGrid.Columns.Add(column);

            // Subscribe to future changes.
            NotifyCollectionChangedEventHandler h = (_, ne) => OnCollectionChanged(ne, dataGrid);
            _handlers[dataGrid] = h;
            newColumns.CollectionChanged += h;
        }
    }

    static void OnCollectionChanged(NotifyCollectionChangedEventArgs ne, DataGrid dataGrid)
    {
        switch (ne.Action)
        {
            case NotifyCollectionChangedAction.Reset:
                dataGrid.Columns.Clear();
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Add:
                foreach (DataGridColumn column in ne.NewItems)
                    dataGrid.Columns.Add(column);
                break;
            case NotifyCollectionChangedAction.Move:
                dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (DataGridColumn column in ne.OldItems)
                    dataGrid.Columns.Remove(column);
                break;
            case NotifyCollectionChangedAction.Replace:
                dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
                break;
        }
    }

    public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
    {
        element.SetValue(BindableColumnsProperty, value);
    }

    public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
    {
        return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
    }
}
Mikhail Orlov
  • 2,789
  • 2
  • 28
  • 38
2

You can create a usercontrol with the grid definition and define 'child' controls with varied column definitions in xaml. The parent needs a dependency property for columns and a method for loading the columns:

Parent:


public ObservableCollection<DataGridColumn> gridColumns
{
  get
  {
    return (ObservableCollection<DataGridColumn>)GetValue(ColumnsProperty);
  }
  set
  {
    SetValue(ColumnsProperty, value);
  }
}
public static readonly DependencyProperty ColumnsProperty =
  DependencyProperty.Register("gridColumns",
  typeof(ObservableCollection<DataGridColumn>),
  typeof(parentControl),
  new PropertyMetadata(new ObservableCollection<DataGridColumn>()));

public void LoadGrid()
{
  if (gridColumns.Count > 0)
    myGrid.Columns.Clear();

  foreach (DataGridColumn c in gridColumns)
  {
    myGrid.Columns.Add(c);
  }
}

Child Xaml:


<local:parentControl x:Name="deGrid">           
  <local:parentControl.gridColumns>
    <toolkit:DataGridTextColumn Width="Auto" Header="1" Binding="{Binding Path=.}" />
    <toolkit:DataGridTextColumn Width="Auto" Header="2" Binding="{Binding Path=.}" />
  </local:parentControl.gridColumns>  
</local:parentControl>

And finally, the tricky part is finding where to call 'LoadGrid'.
I am struggling with this but got things to work by calling after InitalizeComponent in my window constructor (childGrid is x:name in window.xaml):

childGrid.deGrid.LoadGrid();

Related blog entry

Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
Andy
  • 21
  • 2
1

You might be able to do this with AutoGenerateColumns and a DataTemplate. I'm not positive if it would work without a lot of work, you would have to play around with it. Honestly if you have a working solution already I wouldn't make the change just yet unless there's a big reason. The DataGrid control is getting very good but it still needs some work (and I have a lot of learning left to do) to be able to do dynamic tasks like this easily.

Bryan Anderson
  • 15,969
  • 8
  • 68
  • 83
  • My reason is that coming from ASP.Net I'm new to what can be done with decent data binding and I'm not sure where it's limits are. I'll have a play with AutoGenerateColumns, thanks. – Generic Error Nov 26 '08 at 20:37
0

There is a sample of the way I do programmatically:

public partial class UserControlWithComboBoxColumnDataGrid : UserControl
{
    private Dictionary<int, string> _Dictionary;
    private ObservableCollection<MyItem> _MyItems;
    public UserControlWithComboBoxColumnDataGrid() {
      _Dictionary = new Dictionary<int, string>();
      _Dictionary.Add(1,"A");
      _Dictionary.Add(2,"B");
      _MyItems = new ObservableCollection<MyItem>();
      dataGridMyItems.AutoGeneratingColumn += DataGridMyItems_AutoGeneratingColumn;
      dataGridMyItems.ItemsSource = _MyItems;

    }
private void DataGridMyItems_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
        {
            var desc = e.PropertyDescriptor as PropertyDescriptor;
            var att = desc.Attributes[typeof(ColumnNameAttribute)] as ColumnNameAttribute;
            if (att != null)
            {
                if (att.Name == "My Combobox Item") {
                    var comboBoxColumn =  new DataGridComboBoxColumn {
                        DisplayMemberPath = "Value",
                        SelectedValuePath = "Key",
                        ItemsSource = _ApprovalTypes,
                        SelectedValueBinding =  new Binding( "Bazinga"),   
                    };
                    e.Column = comboBoxColumn;
                }

            }
        }

}
public class MyItem {
    public string Name{get;set;}
    [ColumnName("My Combobox Item")]
    public int Bazinga {get;set;}
}

  public class ColumnNameAttribute : Attribute
    {
        public string Name { get; set; }
        public ColumnNameAttribute(string name) { Name = name; }
}
David Soler
  • 192
  • 1
  • 10