Wednesday, May 8, 2013

How to Bind to a DataGrid when its columns are not known at compile time (ItemsSource is bidimensional)

Making a Bind in a WPF DataGrid is not the the most difficult task. In fact, there are dozens of posts explaining how to do it (http://wpftutorial.net/DataGrid.html, http://www.codeproject.com/Articles/30905/WPF-DataGrid-Practical-Examples). However, they all have one detail in common: they assume that the columns of the DataGrid are known at compile time, ie., each column is a reference to a property of class that represents a row in the DataGrid.

An example: the class Customer has the properties Id, Name and Age. The DataGrid displays a list of customers. Therefore, it is necessary to create a column for each property. To do so, simply create in xaml, three instances of the class DataGridTextColumn and make a bind to each of the 3 properties, like following code:

       <DataGrid.Columns>

             <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}"/>

             <DataGridTextColumn Header="Nome" Binding="{Binding Path=Name}"/>

             <DataGridTextColumn Header="Idade" Binding="{Binding Path=Age}"/>

       </DataGrid.Columns>

Although this approach solves many of the problems, it is not for scenarios whose columns are known only at runtime. Consider, for example, the case where the user selects a date range and the DataGrid should, thereafter, show a column for each day in this range. In this case, how to generate the bindings of the columns? And worse: how to fill column headers?

The purpose of this post is, therefore, to show an approach to deal with scenarios in which the columns of the DataGrid are not known at compile time. At the end of the post, a small project for download will exemplify the use of the technique presented here. To understand it, it is essential that the reader has a minimal knowledge about bindings and how to use DataGrid in WPF. If not, I strongly recommend you read the posts mentioned in the first paragraph. We will follow here the solution proposed in http://stackoverflow.com/questions/320089/how-do-i-bind-a-wpf-datagrid-to-a-variable-number-of-columns/8890435

In order to solve the problem posed in question it should be noted, firstly, that you cannot define the columns in xaml, simply because we do not know the columns in compile time. Therefore, we will do the bindings programatically through a C# method. This method should receive a list of column names and, from them, create instances of the DataGridTextColumn class and set its Header and Binding programmatically, as shown in the code below:

       public void SetGridValues(IList<string> columnHeaders, IList<Row> lines)

       {

            _datagrid.Columns.Clear();

            for (var index = 0; index < columnHeaders.Count; index++)

            {

                var newColumn = new DataGridTextColumn

                                    {

                                        Header = columnHeaders[index],

                                        Binding = new Binding(string.Format("Cells[{0}].Value", index)),

                                    };

                _datagrid.Columns.Add(newColumn);

            }

 

            Lines = lines;

       }

What this method does is very simple: for each column, the method creates an instance of DataGridTextColumn sets the Header property using the corresponding name and fill the Binding property using an instance of the Binding class. The Binding class, in turn, receives in the constructor a single parameter: the Path of the property to be displayed in the Grid. It is worth remembering here that the DataGrid stores its data in the format "list of lines", that is, each element from the list ItemsSource is a line of the DataGrid. In the method SetGridValues (above), the parameter lines is exactly the list of rows in the DataGrid. Therefore, the Path parameter of the binding is the path relative to the instance of the Row class for that line.

To better understand this, we must examine in detail the class Row:

       public class Row

       {

             public IList<Cell> Cells { get; set; }

       }

The class definition Row is actually quite simple: a list of cells represents each column. The cell represented by the Cell class is even simpler:

       public class Cell : INotifyPropertyChanged

       {

             private string _value;

 

             public Cell(string value)

             {

                    _value = value;

             }

 

             public string Value

             {

                    get { return _value; }

                    set

                   {

                           _value = value;

                           OnPropertyChanged("Value");

                    }

             }

 

        #region Implementation of INotifyPropertyChanged

 

             public event PropertyChangedEventHandler PropertyChanged;

 

             private void OnPropertyChanged(string name)

             {

                    if (PropertyChanged != null)

                    PropertyChanged(this, new PropertyChangedEventArgs(name));

             }

 

        #endregion

       }

Examining the Row and Cell classes, it becomes clear the reason for the chosen value for the Path property in the Binding constructor. A clearer explanation I leave to the reader's curiosity to execute the code in debug mode in Visual Studio:

string.Format("Cells[{0}].Value", index)

 To finish, is worth one last comment: the method SetGridValues is only necessary to assemble the skeleton of the DataGrid, ie, to define the DataGrid columns. Once assembled the skeleton, updating a cell is as easy as changing its Value property, as follows:

       Model.Lines[row].Cells[column].Value = Model.NewValue;

In this case, the setter of the Value property already notifies the change and, as there is a bind to the corresponding cell, the DataGrid control makes the refresh automatically, freeing us from this burden. Take a look in our download project and see this code working.

Dear developers, I hope you enjoyed. Any questions and comments, please post. The code to see the DataGrid in action is in the link below:

Click to download

Please, like us on facebook and have a look at our fan page!!

No comments:

Post a Comment