Deborah's Developer MindScape






         Tips and Techniques for Web and .NET developers.

January 23, 2011

Populating a DataGrid with Dynamic Columns in a Silverlight Application using MVVM

Filed under: C#,Silverlight,VB.NET @ 7:52 pm

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:

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:

image

Use this technique any time you need to bind data when the number of columns is not known.

Enjoy!

18 Comments

  1.   SEO services — January 27, 2011 @ 12:12 am    Reply

    Really nice writeup, keep them coming, thanks for sharing!!..

  2.   Jonx — January 28, 2011 @ 1:07 pm    Reply

    Good stuff… Can you make things editable (maybe not add new columns), sortable? Would be more then nice 🙂

  3.   BalamBalam — February 14, 2011 @ 8:12 am    Reply

    Nice. Will this work in WPF?

  4.   Mikhail — April 17, 2011 @ 7:00 am    Reply

    Thank you very much! Actually it works also with WPF with this small change:



    and

  5.   Mikhail — April 17, 2011 @ 12:32 pm    Reply

    or alternatively use this syntax:

  6.   Lawrence — April 28, 2011 @ 11:23 am    Reply

    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,

  7.   Maurice — May 10, 2011 @ 12:43 pm    Reply

    Great article!

    Any thoughts on how to make the columns editable?

  8.   bookguru@126.com — June 1, 2011 @ 4:59 am    Reply

    How to add comboxcolumn or hyperlinkcolumn according to the data type?

    Thank u.

  9.   cincoutprabu — August 8, 2011 @ 2:39 am    Reply

    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

  10.   Tuli — October 27, 2011 @ 9:49 am    Reply

    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 ?

RSS feed for comments on this post. TrackBack URI

Leave a comment

© 2019 Deborah's Developer MindScape   Provided by WPMU DEV -The WordPress Experts   Hosted by Microsoft MVPs