Populating a DataGrid with Dynamic Columns in a Silverlight Application using MVVM
When binding to a DataGrid, in most cases you have a fixed number of columns and a variable number of rows. This post covers the case when you have a variable number of rows AND a variable number of columns.
NOTE: This post is part of a series that starts with this prior post. The example in this post uses the application that is built as part of that series.
For more general information on populating a DataGrid in Silverlight, see these prior posts:
- Populating a DataGrid in a Silverlight Application
- Populating a DataGrid in a Silverlight Application using MVVM
The example is a Student Management application. Each student has an Id and name and a set of scores. When building the application, you don’t want to hard-code a predefined number of scores. Rather, you want to allow the users to define any number of scores. This makes it more challenging to then bind the data to a DataGrid.
Start by adding a DataGrid to a page. If you are working through the series of posts, you can use the DataGrid you added from the prior post in this series. (See the links above.)
Add a Models folder to your Silverlight project and add a Student class to that folder. Or if you are working through the series, modify the existing Student class.
In C#:
using System;
using System.Collections.Generic;
namespace InStepSM.SL.Models
{
public class Student
{
public String StudentName { get; set; }
public int StudentId { get; set; }
public List<decimal> ProjectScores { get; set; }
}
}
In VB:
Namespace Models
Public Class Student
Public Property StudentName As String
Public Property StudentId As Integer
Public Property ProjectScores As List(Of Decimal)
End Class
End Namespace
NOTE: The VB code Namespace includes only "Models" because VB automatically prepends the application namespace to any defined namespace in the application. The result is InStepSM.SL.Models in this case, just like the C# code.
This class includes a student’s name, Id, and any number of scores.
Since this example follows an MVVM approach, add a ViewModels folder to your Silverlight project and add a StudentViewModel class to that folder. Or if you are working through the series, modify the existing StudentViewModel class.
In C#:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using InStepSM.SL.Models;
namespace InStepSM.SL.ViewModels
{
public class StudentViewModel: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Student> _studentList;
public ObservableCollection<Student> StudentList
{
get
{
return _studentList;
}
set
{
if (_studentList != value)
{
_studentList = value;
OnPropertyChanged("StudentList");
}
}
}
private List<string> _titleList;
public List<string> TitleList
{
get
{
return _titleList;
}
set
{
if (_titleList != value)
{
_titleList = value;
OnPropertyChanged("TitleList");
}
}
}
public StudentViewModel()
{
PopulateStudents();
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public void PopulateStudents()
{
var itemList = new ObservableCollection<Student>()
{new Student(){StudentName="Frodo Baggins",
ProjectScores=new List<decimal>() {
89M,93M,88M}},
new Student(){StudentName="Rosie Cotton",
ProjectScores=new List<decimal>() {
97M,93M,94M}},
new Student(){StudentName="Samwise Gamgee",
ProjectScores=new List<decimal>() {
83M,90M,85M}},
new Student(){StudentName="Peregrin Took",
ProjectScores=new List<decimal>() {
69M,72M,75M}}};
StudentList = itemList;
var itemNameList = new List<string>()
{ "PreTest", "Chp 1", "Test" };
TitleList = itemNameList;
}
}
}
In VB:
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports InStepSM.SL.Models
Namespace ViewModels
Public Class StudentViewModel
Implements INotifyPropertyChanged
Public Event PropertyChanged(ByVal sender As Object,
ByVal e As System.ComponentModel.PropertyChangedEventArgs) _
Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Private _studentList As ObservableCollection(Of Student)
Public Property StudentList As ObservableCollection(Of Student)
Get
Return _studentList
End Get
Set(ByVal value As ObservableCollection(Of Student))
If _studentList IsNot value Then
_studentList = value
OnPropertyChanged("StudentList")
End If
End Set
End Property
Private _titleList As List(Of String)
Public Property TitleList As List(Of String)
Get
Return _titleList
End Get
Set(ByVal value As List(Of String))
If _titleList IsNot value Then
_titleList = value
OnPropertyChanged("TitleList")
End If
End Set
End Property
Public Sub New()
PopulateStudents()
End Sub
Protected Sub OnPropertyChanged(ByVal propertyName As String)
If Not String.IsNullOrEmpty(propertyName) Then
RaiseEvent PropertyChanged(Me,
New PropertyChangedEventArgs(propertyName))
End If
End Sub
Public Sub PopulateStudents()
Dim itemList = New ObservableCollection(Of Student)() From
{New Student() With {.StudentName = "Frodo Baggins",
.ProjectScores = New List(Of Decimal)(
New Decimal() {89D, 93D, 88D})},
New Student() With {.StudentName = "Rosie Cotton",
.ProjectScores = New List(Of Decimal)(
New Decimal() {97D, 93D, 94D})},
New Student() With {.StudentName = "Samwise Gamgee",
.ProjectScores = New List(Of Decimal)(
New Decimal() {83D, 90D, 85D})},
New Student() With {.StudentName = "Peregrin Took",
.ProjectScores = New List(Of Decimal)(
New Decimal() {69D, 72D, 75D})}}
StudentList = itemList
Dim itemNameList = New List(Of String)(
New String() {"PreTest", "Chp 1", "Test" })
TitleList = itemNameList
End Sub
End Class
End Namespace
This code defines two properties: one to hold the list of students, the other to hold the list of score titles. The list of score titles are used as the headers for the dynamic columns.
Hook the View to the View/Model in the xaml. Then modify the DataGrid xaml code to bind to both the list of score titles and the list of students.
<navigation:Page
x:Class="InStepSM.SL.Views.Overview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:vms ="clr-namespace:InStepSM.SL.ViewModels"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
d:DesignWidth="640" d:DesignHeight="480"
Title="Overview Page" >
<!– Reference the View/Model –>
<navigation:Page.DataContext>
<vms:StudentViewModel/>
</navigation:Page.DataContext>
<Grid x:Name="LayoutRoot">
<ScrollViewer x:Name="PageScrollViewer"
Style="{StaticResource PageScrollViewerStyle}">
<StackPanel x:Name="ContentStackPanel">
<TextBlock x:Name="HeaderText"
Style="{StaticResource HeaderTextStyle}"
Text="Student Overview"/>
<sdk:DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding StudentList}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn
Binding="{Binding StudentName}"
Header="Name"/>
<sdk:DataGridTemplateColumn Width="*">
<sdk:DataGridTemplateColumn.HeaderStyle>
<Style
TargetType="primitives:DataGridColumnHeader">
<Setter
Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter
Property="VerticalContentAlignment"
Value="Stretch" />
<Setter Property="Margin"
Value="0" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<ItemsControl
ItemsSource="{Binding DataContext.TitleList,
ElementName=LayoutRoot}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Orientation="Horizontal">
</StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="70" >
<TextBlock Text="{Binding}"
TextAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</sdk:DataGridTemplateColumn.HeaderStyle>
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ItemsControl
ItemsSource="{Binding ProjectScores}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="70">
<TextBlock Text="{Binding}"
TextAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</StackPanel>
</ScrollViewer>
</Grid>
</navigation:Page>
Yikes that is a lot of xaml!
The DataGrid element ItemSource property is bound to StudentList, the list of students defined in the View/Model. That is as it would be for a "normal" DataGrid (that is bound using MVVM).
The key technique here is that the DataGrid does not use automatically generated columns and instead explicitly defines the columns.
The first column is a DataGridTextColumn and binds to the student’s name.
The remaining columns use a DataGridTemplateColumn.
The HeaderStyle property of the DataGridTemplateColumn is bound to the list of score titles. The ContentTemplate property of the style defines the layout and content of each column title. The ItemsControl ItemsSource property defines the source of the data for the columns, which is the TitleList. Because the DataGrid is already bound to the StudentList, a simple binding here won’t work. The code must specify the original DataContext.TitleList from the root element.
The ItemsControl.ItemsPanel defines how the data is laid out. Since we want the titles spread across the top, the ItemsPanelTemplate uses a horizontal StatckPanel.
The ItemsControl.ItemTemplate defines how each title appears. The column width defined here must match the column width defined for the data in order for the header and the data to appear as a column.
The CellTemplate property of the DataGridTemplateColumn is bound to the list of scores. Since the scores are a part of the StudentList that is bound to the DataGrid, a simple binding works here. The ItemsSource is bound to the ProjectScores property.
Again, the ItemsControl.ItemsPanel defines how the scores are laid out and the ItemControl.ItemTemplate defines how each score appears.
[THANKS to Sally Xu for leading me in the right direction for setting up this binding.]
The final result:
Use this technique any time you need to bind data when the number of columns is not known.
Enjoy!
SEO services — January 27, 2011 @ 12:12 am
Really nice writeup, keep them coming, thanks for sharing!!..
Jonx — January 28, 2011 @ 1:07 pm
Good stuff… Can you make things editable (maybe not add new columns), sortable? Would be more then nice 🙂
BalamBalam — February 14, 2011 @ 8:12 am
Nice. Will this work in WPF?
Mikhail — April 17, 2011 @ 7:00 am
Thank you very much! Actually it works also with WPF with this small change:
and
Mikhail — April 17, 2011 @ 12:32 pm
or alternatively use this syntax:
Lawrence — April 28, 2011 @ 11:23 am
Hi Deborah,
Thanks for sharing the code. That was great. But I have another question : How can data be sorted on the dynamic columns, e.g the Test column. It seems the “Name” column can only be sorted now.
Regards,
Maurice — May 10, 2011 @ 12:43 pm
Great article!
Any thoughts on how to make the columns editable?
bookguru@126.com — June 1, 2011 @ 4:59 am
How to add comboxcolumn or hyperlinkcolumn according to the data type?
Thank u.
cincoutprabu — August 8, 2011 @ 2:39 am
For a read-only datagrid, this is too much of work…. You can simply create a dictionary with required number of columns and populate the datagrid in a single line. Check out this link:
http://codeding.com/?article=7
Tuli — October 27, 2011 @ 9:49 am
Tahnk you for the article.
I tried to use your example but with data coming from a Web Service via an async method.
After doing so, the TitleList binding stoped working and the grid’s title in the dynamic column was empty.
How can I support an async thread in this constlation ?