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 FilesMicrosoft SDKsSilverlightv3.0LibrariesClientSystem.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

Brian Lagunas

View all posts

17 comments

  • I think you can do this with the DataGridTextColumn out of the box. Set the binding to the nested indexer property and make sure IsReadOnly is set to false, because there is a bug in the DataGrid and the nested indexer property cannot be resolved as settable.

    • You are correct, I could accomplish the same thing by writing this:

      dataGrid.Columns.Add(new DataGridTextColumn()
      {
      IsReadOnly = false,
      Binding = new System.Windows.Data.Binding(string.Format(“Periods[{0}].Hours”, i)) { Mode = System.Windows.Data.BindingMode.TwoWay }
      });

      But, this works only only for the DataGrid. If I used any other grid such as the Infragistics XamWebGrid or the DevExpress AgDataGrid, that method would not work. So I wrote this blog post with that in mind, and suggested a method that can work with any grid.

  • Ok, this codes are corrects but after do this, i need disable or not disable one cell if my data is not null or is null,

    ¿Is this posible?

    I played with is read only and IsEnabled of Texbox of datatemplate, and not work, because IsReadOnly disabled all cells of column and textbox is datatemplate for all cell columns do same thing…

    🙁

    You have one idea?

    Thanks,

    Pablo

    • You could try using a ValueConverter on your edit data template. This will allow you to conditionally set the IsReadOnly property on your TextBox.

  • Hi, I tried to implement this with WCF RIA Services that returns an entity with an EntityCollection as one of its properties (the entity is associated with another in a one to many relationship).
    The indexer doesn’t seem to work in this case.
    If I bind to the .Count, I get the number of items in the collection, so the data is there.

    Any suggestions?

    • That is because you cannot apply [] indexing to an EntityCollection. You could try using ElementAt(index).

      • Thanks for your reply. I tried the following, but it’s not valid syntax.
        I tried both [] and () for the index. () throws an error, while [] doesn’t work.
        What am I doing wrong?

        Thanks again.

        dg.Columns.Add(New DataGridTextColumn With {
        .Binding= New Binding(“Periods.ElementAt[0].Hours”)})

  • First of all, thank you. This solved a problem for me that was driving me nuts. Do have a follow question for you. How do I get it to sort on those columns?

    • Unfortunately this is a limitation of template columns. At the time of this post there is no sort available on template columns on the data grid.

Follow Me

Follow me on Twitter, subscribe to my YouTube channel, and watch me stream live on Twitch.