Silverlight DataGrid Column Binding

I may be relatively new to Silverlight and MVVM but I know enough that I should be able to bind to a ObservableCollection<DataGridColumn>. What's up with that?

Columns Binding

I tried forcing the binding in code and it tells be that Columns does not have a setter. What if my columns need to change at runtime? In my case they do! I have a feeling that columns will be bindable in the future so I want a solution that will be easy to swap out into an MVVM solution when this functionality becomes available. I just created a solution that worked for me, hopefully it works for you.

Starting with my view model I want to add an ObservableCollection to hold my columns I want the view to bind to.

private ObservableCollection<DataGridColumn> _columns;
public ObservableCollection<DataGridColumn> Columns
{
    get { return _columns; }
    set
    {
        _columns = value;
        NotifyPropertyChanged("Columns");
    }
}

This is common MVVM code, having a class that implements INotifyPropertyChanged. It is also important to note that this is the same way it would be implemented if binding in xaml actually worked for DataGridColumns. Now I can modify this local list and ideally the Column list would be bound in xaml to the DataGrid.Columns property.

public MyViewModel()
{
    ...
    Columns = new ObservableCollection<DataGridColumns>();
}

public ChangeDataGridColumns()
{
    Columns.Clear();
    Columns.Add(new DataGridTextColumn
    {
        Binding = new Binding("Column1"),
        Header = "Column1Header",
        Visibility = Visibility.Visible
    });
    ...
}

This code is simply creating a new column collection then eventually some action will call ChangeDataGridColumns which will remove all other columns and create a new column or columns. I am also demonstrating a binding in code. Elsewhere in my view model I am binding the DataGrid.ItemsSource property to an ObservableCollection. The binding is the property that says show the column Widget.Column1 as the data for this column.

The interesting thing here is that now I can bind to stuff that even Blend doesn't allow you to bind to, like Header and Visibility. In my case, this is valuable because my users can specify what they want the Header text to be for each column and I determine its visibility at runtime based on certain criteria.

Now I need to write some view code. MVVM purists might want to stop here.

Since the Columns list on a DataGrid is an ObservableCollection we have a changed event we can access. I wrote some basic code behind to get to a loaded event and wire up an event handler for the Columns CollectionChanged event.

public MainPage()
{
    InitializeComponent();
    Loaded += MainPageLoaded;
}

private void MainPageLoaded(object sender, RoutedEventArgs e)
{
    ...
    myViewModel.Columns.CollectionChanged += ColumnsCollectionChanged;
}

This is really doing the same thing binding does, only we're going to see the code. The idea is that in the ColumnsCollectionChanged handler we are going to recognize changes to the view model's Column collection and make the same changes on the view's DataGrid control. I just recently wrote this code so I might not have worked out all possible scenarios yet.

private void ColumnsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Reset)
    {
        myDataGrid.Columns.Clear();
    }
    else if (e.Action == NotifyCollectionChangedAction.Add)
    {
        foreach (DataGridColumn column in e.NewItems)
            myDataGrid.Columns.Add(column);
    }
    else if (e.Action == NotifyCollectionChangedAction.Remove)
    {
        foreach (DataGridColumn column in e.OldItems)
            myDataGrid.Columns.Remove(column);
    }
}

It may not be perfect MVVM code but it can't be. Once the ObservableCollection property can be bound all we need to do is take out this crappy code behind and replace it with:

<sdk:DataGrid
    x:Name="myDataGrid"
    ItemsSource="Binding Widgets"
    Columns="Binding Columns"
    AutoGenerateColumns="False" />

Until then, the code behind solution will work.