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!
JS — November 13, 2011 @ 7:55 am
How can I make the dynamic columns sortable? How can I adjust the widths of the dynamic columns? It seems that all the dynamic columns are one big column, how can I show vertical gridlines?
Help urgently needed.
Ihab — June 28, 2012 @ 4:59 am
Many thanks. very well done solution
Saved my week
RMB — September 14, 2012 @ 4:02 pm
I’ve been using your solution and it works very well. Thanks!
However, I need to be able to edit the cell data. I’ve added a CellEditingTemplate to your solution and I can now change the data on the grid. But the change never makes it back to the underlying collection–the collection property Set is called, but the new value is always the original data. Thus, after exiting edit mode the cell’s value reverts back.
Any suggestions?
Aziz — February 13, 2013 @ 9:39 am
Hello.
Can we have an example project with WPF, please!?
Nic — November 1, 2013 @ 7:49 pm
Great example,
I m using WPF and .NET4. The value sfrom th elist ar enot showing for me in the datagrid. Only the column headers are displayed.
Anyone, any clues to have this work in WPF?
Andrew — January 8, 2014 @ 7:20 pm
Very nice article. However there seems to be one slight problem (at least in WPF). If you have not, 3 different columns but lots (hundreds?), the horizontal scroll bar does not seem to appear. I tried setting a MaxWidth for the Grid and still no luck.
I’m guessing it has to do with the fact that we don’t have a fixed number of columns and WPF doesn’t realize that it should add a horizontal scroll.
Any ideas?
Andrew — January 10, 2014 @ 12:56 pm
Removing the Width=”*” solved it for me!
See also: http://stackoverflow.com/questions/21034631/datagrid-with-variable-number-of-columns-keeping-the-horizontal-scrollbar
Bro — August 11, 2016 @ 7:29 am
great. That’s it what I was looking for.