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.
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.
That is it. You have now successfully satisfied your customer?s addiction to Excel. Well, at least a little bit.
You could use StringBuilder AppendFormat(“”) instead of Append(String.Format(“”))
Nice post, loved the “StaffMember” example name.
Just what I was looking for, thank you.
Is there a way to sort the dynamic columns. I’ve thus far tried adding the SortMemberPath as the header name, and setting CanUserSort to true. However when clicking any of the dynamic column headers to sort, this seems to only reset the grid to its original state (If you’d previously sorted one of the non-dynamic columns).
@Robert
Unfortunately this is a limitation of using dynamic columns.
Hi,
Is there a way i can have dynamic cell types for the period columns for the sample you have shown above.. Like one row checkbox and other row combo box…?
I followed your example, with the difference that I’m bring data from db through WCF RIA. For some reason, when I bring my list to the client side, it drops the observableCollection. Any ideas why it’s doing that?
Thanks a bunch, this was precisely what I needed. Saved me lots of hours going through MSDN.
Hi, Thanks for this blog it really helped a lot, however I am facing a problem here. Even though I am setting the Header names in my code I dont see them at run time. I’ve also made the Header visibility to true for all grid headers.
Any Ideas on whats wrong?
Please let me know if you need any further details.
Thanks
Supreet
Thanks very much – was very useful.
Summa
Thanks man!!!! Just what I was looking for!!!
Love the StaffMember example: Stripped for unnecessary code!!!
Thanks very much.