Silverlight DataGrid – Populate Dynamic Columns from a Child Collection

March 8th, 2010

Lets face it, any one building LOB (line of business) applications know that their users love Microsoft Excel.  If the users had their way, they would do everything in Excel.  Due to this known fact, when building Silverlight LOB applications, there is often a need to flatten out an object for editing in a grid.  For example; you may have an object that has an n-level number of properties or attributes that aren’t known until runtime, but you want to edit the object in a single row in a grid. You don’t want to add a bunch of properties on your object like Prop1, Prop2, Prop3, etc.., just so you can bind it to your grid.  You want to dynamically add columns to your grid and bind those columns to the correct object in the child collection at run time.

Well, this is much easier than you may think and I will show you how to accomplish this with just a few simple helper methods, and you can use any grid of your choice.  For this example, I will be using the DataGrid that comes with the Silverlight Toolkit. Make sure you download and install it, because I am not including the System.Windows.Controls.Data assembly required for the DataGrid.

It will be located at c:\Program Files\Microsoft SDKs\Silverlight\v3.0\Libraries\Client\System.Windows.Controls.Data.dll

In my scenario I am building a staffing application and I have a “StaffMember” object that has a collection of “Period” objects as a child property.  My objects look something like this:

public class StaffMember
{
    public string Name { get; set; }
    public string Department { get; set; }
    public ObservableCollection<Period> Periods { get; set; }
}
 
public class Period
{
    public string Title { get; set; }
    public int Hours { get; set; }
}

Pretty simple!  now, lets create our DataGrid that will show our data for editing.

<UserControl x:Class="SilverlightApplication1.MainPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:grid="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <Grid x:Name="LayoutRoot">
        <grid:DataGrid x:Name="dataGrid"/>
    </Grid>
</UserControl>

Okay, now I don’t know how many Periods my StaffMember will have until I get the data back from the database at run time.  For demonstration purposes, I just created a method on my StaffMember class that would create my objects by iterating through a loop.

public static List<StaffMember> GetData()
{
    List<StaffMember> dataList = new List<StaffMember>();
    for (int i = 0; i < 3; i++)
    {
        StaffMember member = new StaffMember { Name = String.Format("Name#{0}", i), Department = String.Format("Department#{0}", i) };
        ObservableCollection<Period> periods = new ObservableCollection<Period>();
        for (int j = 0; j < 5; j++)
            periods.Add(new Period() { Title = String.Format("Period#{0}-{1}", i, j), Hours = j });
        member.Periods = periods;
        dataList.Add(member);
    }
    return dataList;
}

Now, we need to set the datasource on the DataGrid.  Since we are creating the columns at runtime make sure you set AutoGenerateColumns to false;

List<StaffMember> dataList = StaffMember.GetData();
dataGrid.AutoGenerateColumns = false;
dataGrid.ItemsSource = dataList;
dataGrid.Columns.Clear();

Next, lets take care of creating the easy columns first.  I created a method that its’ sole purpose is to give me new DataGridTextColumns.

private static DataGridTextColumn CreateTextColumn(string fieldName, string title)
{
    DataGridTextColumn column = new DataGridTextColumn();
    column.Header = title;
    column.Binding = new System.Windows.Data.Binding(fieldName);
    return column;
}

Using this method we can create our first two columns.

dataGrid.Columns.Add(CreateTextColumn("Name", "Staff Name"));
dataGrid.Columns.Add(CreateTextColumn("Department", "Company Department"));

Your DataGrid should now look something like this.

text columns added to grid

Now we need to create our columns based off the Periods collection.  To do this we will utilize a DataGridTemplateColumn.  The first thing we need to do is create a method that will dynamically create a DataTemplate that the DataGridTemplateColumn will use as the CellTemplate.

private string CreateColumnTemplate(int index, string propertyName)
{
    StringBuilder CellTemp = new StringBuilder();
    CellTemp.Append("<DataTemplate ");
    CellTemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
    CellTemp.Append("2006/xaml/presentation' ");
    CellTemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>");
    CellTemp.Append(String.Format("<TextBlock Text='{{Binding Periods[{0}].{1}}}'/>", index, propertyName));
    CellTemp.Append("</DataTemplate>");
    return CellTemp.ToString();
}

What I am doing here is using a StringBuilder to create a DataTemplate, represented by XAML.  Pay special attention to the TextBlock’s binding.  I am using String.Format to create my binding string base off the index the of the object in the collection and the name of the property on the child object I want to bind to.  Now, lets create our template that will be used for editing.

private string CreateColumnEditTemplate(int index, string propertyName)
{
    StringBuilder CellTemp = new StringBuilder();
    CellTemp.Append("<DataTemplate ");
    CellTemp.Append("xmlns='http://schemas.microsoft.com/winfx/");
    CellTemp.Append("2006/xaml/presentation' ");
    CellTemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>");
    CellTemp.Append(String.Format("<TextBox Text='{{Binding Periods[{0}].{1}, Mode=TwoWay}}'/>", index, propertyName));
    CellTemp.Append("</DataTemplate>");
    return CellTemp.ToString();
}

This method is very similar to the previous one we wrote, but notice the subtle difference; I am using a TextBox instead of a TextBlock, and the Mode is set to TwoWay.  This will allow us to edit the values in the DataGrid.  Now we need a method that will actually create the TemplateColumns.

private DataGridTemplateColumn CreateTemplateColumn(int i, string propName)
{
    DataGridTemplateColumn column = new DataGridTemplateColumn();
    column.Header = String.Format("Period#{0}.{1}", i, propName);
    column.CellTemplate = (DataTemplate)XamlReader.Load(CreateColumnTemplate(i, propName)); //display template
    column.CellEditingTemplate = (DataTemplate)XamlReader.Load(CreateColumnEditTemplate(i, propName)); //edit template
    return column;
}

Notice that we are setting the CellTemplate and CellEditTemplate by using the XamlReader to load our StringBuilder result and cast it as a legitimate DataTemplate the column can use.  Now that we have the method that will create our TemplateColumns, lets go ahead and build our dynamic columns to the n-level.  We do this by looping though the number of columns that need to be created and using our CreateTemplateColumn method to add the new columns to the DataGrid.

int periodCount = dataList[0].Periods.Count;
for (int i = 0; i < periodCount; i++)
{
   dataGrid.Columns.Add(CreateTemplateColumn(i, "Hours"));
}

Now of course, in the real world you would not want to use the first index of the child collection to figure out how many columns to build.  I would recommend some kind of definition object that will define what columns and how many columns to build.

completed DataGrid 

That is it.  You have now successfully satisfied your customer’s addiction to Excel.  Well, at least a little bit.

Download Source

  • Danesan

    Summa

  • soren

    Thanks man!!!! Just what I was looking for!!!
    Love the StaffMember example: Stripped for unnecessary code!!!

  • http://www.phanmemquocbao.com Lqtuanvl

    Thanks very much.