Welcome to MSDN Blogs Sign in | Join | Help

Scott Morrison

The other Microsoft Scott
Sorting Data in the Silverlight DataGrid

This post is one in a series of posts outlining the new functionality delivered in Silverlight 2 Beta 2.  It is important to note that the following will not work in Silverlight 2 Beta 1.

Multi-column Sorting

One of the most requested features in Beta 1 was a way to sort the data contained in the DataGrid.  In fact it was so desired that clever developers such as Matt Berseth simply created their own implementation.  Fortunately, in Beta 2 you don't have to put much work into getting this functionality out of the box. 

To get sorting functionality in your DataGrid you will need to perform the following steps:

  1. Make sure that the collection that you use as the DataGrid's ItemsSource implements IList.  Common implementations are List<T> and ObservableCollection<T>. 
  2. That's it, you don't need to do anything else

Why this odd requirement?  Under the covers, when a DataGrid is data bound to a collection that implements IList, it creates an internal ListCollectionView to provide the sorting functionality.  Since CollectionViews can handle multiple levels of sorting, the DataGrid also possesses this functionality and exposes it by allowing your users to click on multiple headers while holding the Ctrl key. (And yes, if you are thinking it, Shift is going to be the key in the next drop) 

Here is the DataGrid showing off multi-column sorting:

MultiColumnSort

Customizing Sorting

Alternatively if you want more control over how the sorting is performed, you can implement the newly ported ICollectionView interface.  The DataGrid knows that when it is bound to an ICollectionView to delegate all sorting to it, and to listen to its SortDescriptions collection and visually display the sorting status of the CollectionView.  Additionally by using an external CollectionView, you can have multiple controls share the same sort order.

SortMemberPath

There are a few special cases in sorting where you want to take some control, but don't want to have to write an ICollectionView.  One of these cases is controlling what property on your data item each column sorts by.  The default is the property that the column is bound to through the DisplayMemberBinding property.  However, if you look at the Template Column there is no DisplayMemberBinding property, meaning that the DataGrid has no idea of how to sort the data if you were to click on that column's header.  To give the DataGrid that information, you can use a property found on all column types called SortMemberPath.  This property allows you to specify a path to the property on the data item that you want that particular column to sort by.  Now if you create a template column that uses a DatePicker to allow your user to select a date instead of the standard TextBox, you can still provide sorting for that column by setting the SortMemberPath.

C#

datePickerColumn.SortMemberPath = "Birthday";

VB

DatePickerColumn.SortMemberPath = "Birthday"

Notice that since SortMemberPath is on DataGridColumn, you can use this beyond just template columns and can actually have a column bound to one property, but sort based on another property.  This is a much less common scenario, but becomes useful when working with types that normally don't support sorting such as Colors or Images.

Sorting UI Cues

In the picture above you can see that each sorted column has an arrow in its header indicating if it is sorted ascending or descending.  It also has a subtle animation when switching between the two.  If you want to customize how this looks or behaves, override the default DataGridColumnHeader template using the DataGrid.ColumnHeaderStyle property.  The template contains two storyboards, one named "SortedAscending State" and the other named "SortedDescending State".  These two storyboards are ran by the DataGrid when a column is sorted ascending or descending.  Finally there is a third storyboard called "Unsorted State" that is ran when the data is no longer being sorted by that column and can be used to clean up any UI that you used to display sort status.

Auto-sizing the Silverlight DataGrid

This post is one in a series of posts outlining the new functionality delivered in Silverlight 2 Beta 2.  It is important to note that the following will not work in Silverlight 2 Beta 1.

Auto-sizing the DataGrid

Unlike in Beta 1, the height and width of the DataGrid is now auto-sized by default.  This means that if you put it in a Canvas or a StackPanel and don't specify a size, you will no longer see just the ColumnHeaders, but rather will get a DataGrid that is sized to its contents. 

Warning: This can be dangerous however if you are working with a large (1000+ item) collection as the ItemsSource.  Since the DataGrid will grow to the size of its contents, and will create objects for each cell that it can show based on its height, it will literally create thousands of objects to display all of the data.  As you can guess this has app performance implications so you should avoid the combination of an auto-sized DataGrid and a large amount of data to display.  This isn't an issue of course if you specify a size for the DataGrid, or put it in a container such as a Grid that provides it with a size.

Since the default size for the DataGrid is auto you won't have write any code to get auto-sizing behavior, but if you do give the DataGrid a height or width, and then switch it back to auto-size at runtime, simply set that value to double.NaN which is WPF and Silverlight's way of having a FrameworkElement auto-size itself.

C#

dataGrid1.Height = double.NaN;

VB

DataGrid1.Height = Double.NaN

Auto-sizing Columns and Rows

In addition to the DataGrid auto-sizing itself, Columns and Rows are also auto-sized by default.  Like the DataGrid, rows are auto-sized in the same manner by setting the value to double.NaN while columns on the other hand are done slightly differetly.  If you have ever worked with the Grid control, you might have noticed that the Width property of its ColumnDefinitions is not of type double, but rather a struct called GridLength.  The reason for this is that unlike a FrameworkElement's Width or Height properties which only have the possible values of Auto or a numeric value, a ColumnDefinition's Width can be set to a numeric value, Auto, or Star.  DataGridColumn.Width follows a similar pattern where instead of being of type double, it is of type DataGridLength.

The possible values for DataGridLenth are:

  • Auto: This auto-sizes the column or row to grow to the size of the largest header or cell.  It is effectively a combination of the other two auto-size modes.  You might notice as you scroll when a new larger cell is encountered that this value increases.  It will however not decrease once that item is scrolled out of view.
  • SizeToHeader: This auto-sizes the column or row the grow to the size of the header.  This will ignore the contents of the cells when determining its size and will not change unless the size of the header changes.
  • SizeToCells: This auto-sizes the column or row to grow to the size of the largest visible cell.  It will ignore the header cell in this calculation.  You might notice as you scroll when a new larger cell is encountered that this value increases.  It will however not decrease once that item is scrolled out of view.
  • Numeric: Unlike the others this is not an enum value, but rather simply a numeric value such as 100.  This mode will behave the same way that Beta 1 did, however setting it in code behind is slightly different.

Setting these in XAML is straight forward other than that unfortunately just like GridLength, there is no Intellisense for the enum values since a number is a valid entry as well.

XAML

<my:DataGrid x:Name="dataGrid1" AutoGenerateColumns="False">
    <my:DataGrid.Columns>
        <my:DataGridTextColumn DisplayMemberBinding="{Binding}" 
            Width="Auto" Header="Auto"/>
        <my:DataGridTextColumn DisplayMemberBinding="{Binding}" 
            Width="SizeToHeader" Header="Size to Header"/>
        <my:DataGridTextColumn DisplayMemberBinding="{Binding}" 
            Width="SizeToCells" Header="Size to Cells"/>
        <my:DataGridTextColumn DisplayMemberBinding="{Binding}" 
            Width="50" Header="Numeric"/>
    </my:DataGrid.Columns>
</my:DataGrid>

If you were to run the code above and give it a data source it would look something like this:

AutoSizedColumns

As you can see, once you know the possible enum values, setting it in XAML is fairly simple, and the Beta 1 code of Width="100" just works.  When you are working in code behind however things get slightly more tricky.

If you wanted to write code to do the same as above:

C#

DataGrid dataGrid1 = new DataGrid();
dataGrid1.AutoGenerateColumns = false;

DataGridTextColumn col1 = new DataGridTextColumn();
col1.DisplayMemberBinding = new Binding();
col1.Width = DataGridLength.Auto;
col1.Header = "Auto";
dataGrid1.Columns.Add(col1);
DataGridTextColumn col2 = new DataGridTextColumn();
col2.DisplayMemberBinding = new Binding();
col2.Width = DataGridLength.SizeToHeader;
col2.Header = "Size to Header";
dataGrid1.Columns.Add(col2);
DataGridTextColumn col3 = new DataGridTextColumn();
col3.DisplayMemberBinding = new Binding();
col3.Width = DataGridLength.SizeToCells;
col3.Header = "Size to Cells";
dataGrid1.Columns.Add(col3);
DataGridTextColumn col4 = new DataGridTextColumn();
col4.DisplayMemberBinding = new Binding();
col4.Width = new DataGridLength(50);
col4.Header = "Numeric";
dataGrid1.Columns.Add(col4);

dataGrid1.ItemsSource = "a b c d e f g h i j".Split();
LayoutRoot.Children.Add(dataGrid1);

VB

Dim dataGrid1 As New DataGrid
dataGrid1.AutoGenerateColumns = False

Dim col1 As New DataGridTextColumn
col1.DisplayMemberBinding = New Binding
col1.Width = DataGridLength.Auto
col1.Header = "Auto"
dataGrid1.Columns.Add(col1)
Dim col2 As New DataGridTextColumn
col2.DisplayMemberBinding = New Binding
col2.Width = DataGridLength.SizeToHeader
col2.Header = "Size to Header"
dataGrid1.Columns.Add(col2)
Dim col3 = New DataGridTextColumn
col3.DisplayMemberBinding = New Binding
col3.Width = DataGridLength.SizeToCells
col3.Header = "Size to Cells"
dataGrid1.Columns.Add(col3)
Dim col4 = New DataGridTextColumn
col4.DisplayMemberBinding = New Binding
col4.Width = New DataGridLength(50)
col4.Header = "Numeric"
dataGrid1.Columns.Add(col4)

dataGrid1.ItemsSource = "a b c d e f g h i j".Split()
LayoutRoot.Children.Add(dataGrid1)

The interesting line above is where the Numeric column's width is set to 50.  There are two ways to do this.  If you are working with doubles, you can create a new GridLength that takes a double in its constructor.  This is the most common and the easiest way to do it.  If however you are working with strings such as a value in a TextBox, you can use the ConvertFrom method on DataGridLengthConverter which takes an object and returns a GridLength.

What's New in the Silverlight DataGrid in Beta 2

Silverlight 2 Beta 2 was released this weekend, see what ScottGu has to say about it here.  We've all been pretty busy since releasing Beta 1 at MIX, and there are a lot of great new features across the product as a result.  The question that you might be asking is what has changed in the DataGrid?  Check out this list of new features and their associated posts to find out.

New Features Overview

  • Auto-sizing: Unlike Beta 1 when you had to specify widths and heights yourself, everything in the DataGrid now can be auto-sized.  That includes the DataGrid itself, Columns, Rows, Headers, and Row Details.
  • Sorting: Now any time you bind the DataGrid to a collection that implements IList such as a List<T> you now get sorting for free.  Even better sorting isn't limited to a single column, but end users can sort by multiple columns by holding Ctrl and clicking on additional column headers.
  • Column Reordering: In Beta 1 the order of columns could be set programmatically, but now in Beta 2 we have extended this capability to the end user, where column reordering can be performed at runtime just by dragging a column's header.
  • Frozen Columns: This is a semi-advanced feature, but once you know about it you'll be surprised at how useful it can be.  A frozen column does not scroll horizontally but rather is "frozen" in place.  This is the same as the Freeze Column feature in Excel.
  • Programmatic Scrolling: Now you can scroll columns and items into view to make sure that an item you want to highlight to your user is visible on screen.
  • Performance Enhancements: One of the big points of feedback we got in Beta 1 was vertical scrolling speed needed to be faster.  We've made some improvements in this space to speed things up.
  • API Improvements: These are changes that we made to existing API's to try to make using the DataGrid easier.  Hopefully they are all for the better, and we would love to hear your feedback if you think that something could be easier.
New Posts Coming Soon...

You might have noticed that posts have dropped off over the last month or so.  The reason being that we have been pretty heads down working on some great new stuff for the DataGrid.  I can't go into specifics just yet, but stay tuned...

If however you are starved for DataGrid info, check out these posts over on the Swiss MSDN blog who have been doing some great things with the DataGrid in Silverlight Beta 1:

Using WCF and LINQ to Connect a DataGrid to a SQL Database

CRUD operations using Silverlight 2 Beta 1 Part 1 / Part 2

Adding Paging to the Silverlight DataGrid

Know of any other really good Silverlight DataGrid posts?  I would love to hear about them in the comments.

Defining Silverlight DataGrid Columns at Runtime

Now that you know the basics of the Silverlight DataGrid and how to specify the Columns in XAML, you might want to customize your DataGrid's columns at runtime.

This process is pretty straight forward, so instead of doing the usual end-to-end walk through, I'm going to provide you with a Rosetta Stone between the static XAML form and the dynamic C#/VB form for the scenario in the last post.

Defining a DataGrid

For any of these columns to be useful you are going to first need a DataGrid to add them to.  The following creates a DataGrid, adds it as a child of the root layout Grid, and sets its ItemsSource to a collection called "source".

C#

DataGrid targetDataGrid = new DataGrid();
targetDataGrid.ItemsSource = source;
LayoutRoot.Children.Add(targetDataGrid);

VB

Dim targetDataGrid As New DataGrid
targetDataGrid.ItemsSource = Source
LayoutRoot.Children.Add(targetDataGrid)

 

Defining a DataGrid Text Column

The following creates a DataGridTextBoxColumn bound to the FirstName property with a header of "First Name", and adds it to the columns collection of the DataGrid named "targetDataGrid".

Static
 <my:DataGrid x:Name="targetDataGrid">
     <my:DataGrid.Columns>
         <my:DataGridTextBoxColumn Header="First Name" 
             DisplayMemberBinding="{Binding FirstName}" />
     </my:DataGrid.Columns>
 </my:DataGrid>
Dynamic

C#

using System.Windows.Data;
...
DataGridTextBoxColumn textBoxColumn = new DataGridTextBoxColumn();
textBoxColumn.Header = "First Name";
textBoxColumn.DisplayMemberBinding = new Binding("FirstName");
targetDataGrid.Columns.Add(textBoxColumn);

VB

Imports System.Windows.Data
...
Dim TextBoxColumn As New DataGridTextBoxColumn
TextBoxColumn.Header = "First Name"
TextBoxColumn.DisplayMemberBinding = New Binding("FirstName")
TargetDataGrid.Columns.Add(TextBoxColumn)

 

Defining a DataGrid CheckBox Column

The following creates a DataGridCheckColumn bound to the Available property with a header of "Available", and adds it to the columns collection of the DataGrid named "targetDataGrid".

Static
 <my:DataGrid x:Name="targetDataGrid">
     <my:DataGrid.Columns>
         <my:DataGridCheckBoxColumn Header="Available " 
             DisplayMemberBinding="{Binding Available}" />
     </my:DataGrid.Columns>
 </my:DataGrid>
Dynamic

C#

using System.Windows.Data;
...
DataGridCheckBoxColumn checkBoxColumn = new DataGridCheckBoxColumn();
checkBoxColumn.Header = "Available";
checkBoxColumn.DisplayMemberBinding = new Binding("Available");
targetDataGrid.Columns.Add(checkBoxColumn);

VB

Imports System.Windows.Data
...
Dim CheckBoxColumn As New DataGridCheckBoxColumn
CheckBoxColumn.Header = "Available"
CheckBoxColumn.DisplayMemberBinding = New Binding("Available")
targetDataGrid.Columns.Add(CheckBoxColumn)

 

Defining a DataGrid Template Column

The following creates a DataGridTemplateColumn bound to the Birthday property with a header of "Birthday", and adds it to the columns collection of the DataGrid named "targetDataGrid".

Static
<UserControl.Resources>
    <local:DateTimeConverter x:Key="DateConverter" />
</UserControl.Resources>
...
<my:DataGrid x:Name="targetDataGrid" AutoGenerateColumns="False" >
    <my:DataGrid.Columns>
        <my:DataGridTemplateColumn Header="Birthday">
            <my:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock 
                        Text="{Binding Birthday, 
                        Converter={StaticResource DateConverter}}" 
                        FontFamily="Trebuchet MS" FontSize="11" 
                        Margin="5,4,5,4"/>
                </DataTemplate>
            </my:DataGridTemplateColumn.CellTemplate>
            <my:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <DatePicker 
                        SelectedDate="{Binding Birthday, Mode=TwoWay}" />
                </DataTemplate>
            </my:DataGridTemplateColumn.CellEditingTemplate>
        </my:DataGridTemplateColumn>
    </my:DataGrid.Columns>
</my:DataGrid>
Dynamic

There are two ways to dynamically create a template column for a DataGrid.  One is to load in the CellTemplate and CellEditingTemplates as DataTemplates from resources, and the other is to construct the DataTemplates on the fly using XamlReader.

1. Resources Method

This method creates the CellTemplate DataTemplate and the CellEditingTemplate DataTemplate in XAML and stores them as named resources.  Then when the column is created the DataTemplates are used.

Use the below XAML to create the DataTemplates as resources to support the code for this method.

<UserControl.Resources>
    <local:DateTimeConverter x:Key="DateConverter" />

    <DataTemplate x:Key="myCellTemplate">
        <TextBlock 
            Text="{Binding Birthday, 
            Converter={StaticResource DateConverter}}" 
            FontFamily="Trebuchet MS" FontSize="11" 
            Margin="5,4,5,4"/>
    </DataTemplate>

    <DataTemplate x:Key="myCellEditingTemplate">
        <DatePicker 
            SelectedDate="{Binding Birthday, Mode=TwoWay}" />
    </DataTemplate>
</UserControl.Resources>

C#

using System.Windows.Data;
...
DataGridTemplateColumn templateColumn = new DataGridTemplateColumn();
templateColumn.Header = "Birthday";
templateColumn.CellTemplate = (DataTemplate)Resources["myCellTemplate"];
templateColumn.CellEditingTemplate = 
                       (DataTemplate)Resources["myCellEditingTemplate"];
targetDataGrid.Columns.Add(templateColumn);

VB

Imports System.Windows.Data
...
Dim TemplateColumn As New DataGridTemplateColumn
TemplateColumn.Header = "Birthday"
TemplateColumn.CellTemplate = Resources("myCellTemplate")
TemplateColumn.CellEditingTemplate = Resources("myCellEditingTemplate")
targetDataGrid.Columns.Add(TemplateColumn)

2. XamlReader Method

This method creates the DataTemplate inline using the XamlReader class.  This class takes a string and parses it to try to build a visual tree.  In this case we are creating DataTemplates.  This method is especially useful if the DataTemplate itself has to be dynamic.  One example being if you wanted to modify what the element in the template was data bound to.

Warning: This method is considerably more difficult than the resources method.  I recommend only using this if you need to dynamically create the DataTemplate.

Some things to watch out for:

  1. You will need to declare any XAML namespace that is used in the data template
  2. Any custom XAML namespace needs to specify both the clr-namespace and the assembly
  3. You cannot have white space between the xmlns: and the name of your namespace
  4. External resources cannot be referenced, they need to be declared inline
  5. The entire template is a single line, so if you get a XAML Parse exception, it will always say line 1, however the character is fairly accurate if you were to concatenate all of your lines.
  6. When using the StringBuilder approach shown below, make sure that you have the correct white space at the end of a line so that it is correctly separated when concatenated with the next line.

Now that the warnings are out of the way, here is how you do the equivalent of the code above:

C#

using System.Windows.Data;
using System.Windows.Markup;
using System.Text;
...
DataGridTemplateColumn templateColumn = new DataGridTemplateColumn();
templateColumn.Header = "Birthday";

StringBuilder CellTemp = new StringBuilder();
CellTemp.Append("<DataTemplate ");
CellTemp.Append("xmlns='http://schemas.microsoft.com/client/2007' ");
CellTemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ");

//Be sure to replace "YourNamespace" and "YourAssembly" with your app's 
//actual namespace and assembly here
CellTemp.Append("xmlns:local = 'clr-namespace:YourNamespace");
CellTemp.Append(";assembly=YourAssembly'>");

CellTemp.Append("<Grid>");
CellTemp.Append("<Grid.Resources>");
CellTemp.Append("<local:DateTimeConverter x:Key='DateConverter' />");
CellTemp.Append("</Grid.Resources>");
CellTemp.Append("<TextBlock ");
CellTemp.Append("Text = '{Binding Birthday, ");
CellTemp.Append("Converter={StaticResource DateConverter}}' ");
CellTemp.Append("FontFamily='Trebuchet MS' FontSize='11' ");
CellTemp.Append("Margin='5,4,5,4'/>");
CellTemp.Append("</Grid>");
CellTemp.Append("</DataTemplate>");

StringBuilder CellETemp = new StringBuilder();
CellETemp.Append("<DataTemplate ");
CellETemp.Append("xmlns='http://schemas.microsoft.com/client/2007' ");
CellETemp.Append("xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>");
CellETemp.Append("<DatePicker ");
CellETemp.Append("SelectedDate='{Binding Birthday, Mode=TwoWay}' />");
CellETemp.Append("</DataTemplate>");

templateColumn.CellTemplate = 
    (DataTemplate)XamlReader.Load(CellTemp.ToString());
templateColumn.CellEditingTemplate = 
    (DataTemplate)XamlReader.Load(CellETemp.ToString());
targetDataGrid.Columns.Add(templateColumn);

VB

Imports System.Windows.Data
Imports System.Text
Imports System.Windows.Markup
...
Dim TemplateColumn As New DataGridTemplateColumn
TemplateColumn.Header = "Birthday"

Dim CellTemp As New StringBuilder
CellTemp.Append("<DataTemplate ")
CellTemp.Append("xmlns = 'http://schemas.microsoft.com/client/2007' ")
CellTemp.Append("xmlns:x = 'http://schemas.microsoft.com/winfx/2006/xaml' ")

'Be sure to replace "YourNamespace" and "YourAssembly" with your app's
'actual namespace and assembly here
CellTemp.Append("xmlns:local = 'clr-namespace:YourNamespace")
CellTemp.Append(";assembly=YourAssembly'>")

CellTemp.Append("<Grid>")
CellTemp.Append("<Grid.Resources>")
CellTemp.Append("<local:DateTimeConverter x:Key='DateConverter' />")
CellTemp.Append("</Grid.Resources>")
CellTemp.Append("<TextBlock ")
CellTemp.Append("Text = '{Binding Birthday, ")
CellTemp.Append("Converter={StaticResource DateConverter}}' ")
CellTemp.Append("FontFamily='Trebuchet MS' FontSize='11' ")
CellTemp.Append("Margin='5,4,5,4'/>")
CellTemp.Append("</Grid>")
CellTemp.Append("</DataTemplate>")

Dim CellETemp As New StringBuilder
CellETemp.Append("<DataTemplate ")
CellETemp.Append("xmlns = 'http://schemas.microsoft.com/client/2007' ")
CellETemp.Append("xmlns:x = 'http://schemas.microsoft.com/winfx/2006/xaml'>")
CellETemp.Append("<DatePicker ")
CellETemp.Append("SelectedDate='{Binding Birthday, Mode=TwoWay}' />")
CellETemp.Append("</DataTemplate>")

TemplateColumn.CellTemplate = XamlReader.Load(CellTemp.ToString())
TemplateColumn.CellEditingTemplate = XamlReader.Load(CellETemp.ToString())
targetDataGrid.Columns.Add(TemplateColumn)

 

Up next time - a reason to dynamically define columns at runtime: responding to the AutoGeneratingColumn event.

Defining Columns for a Silverlight DataGrid

If you read my last post, you might have noticed how easy it is to get a Silverlight DataGrid up and running with the AutoGenerateColumns feature.  Something else you might have noticed is that if you don't like the default column choices it is sort of hard to change them.  Not to fear however since there are two ways to get more control.  One way is to use the AutoGeneratingColumn event (we'll talk about this 5 topics or so down the line) but the easiest way, and the topic of our discussion today, is by using the Columns collection.

The Columns Collection and You

The Columns Collection is your ticket to controlling the DataGrid's column order, appearance, and even what controls they use to represent data in their cells.  It enables you to have a concrete instance for each column that you define unlike auto generation that simply fills the column collection for you.

Before we dive into using the Columns collection though, it is useful to know what you can put in it.

DataGrid Column Types
  • DataGridTextBoxColumn - When you think of a standard column in a DataGrid, you probably picture something that looks a lot like a DataGridTextBoxColumn.  This column is the default and uses a TextBlock to display its data, and a TextBox to allow editing of its data.
  • DataGridCheckBoxColumn - The DataGridCheckBoxColumn is the first out of box custom column type.  It provides a read-only CheckBox for displaying a boolean or nullable boolean value, and a normal CheckBox to allow editing of that value.  This column derives from DataGridBoundColumnBase and serves as a sample for building your own column types.  We'll walk through the process of building your own column types in a future post.
  • DataGridTemplateColumn - If the DataGrid was a movie, the DataGridTemplateColumn would be the star.  It is the most versatile column but, like most prima donnas, takes the most work to get up and going.  Once it is going though there is pretty much no limit to what it can do.
Using the Columns Collection

First we need to get our project back to where we ended last time.  Go ahead and follow the steps 1 and 2 here.

If you followed steps 1 & 2 from the previous post, your DataGrid should look something like this:

<my:DataGrid x:Name="dg" AutoGenerateColumns="True"></my:DataGrid>

(If yours looks different because you did step 3, don't worry since it won't affect this walk through. The one exception is that you need to change IsReadOnly back to False.)

Also, in the code behind file for Page.xaml you should be setting the source to a List<Data> where Data is the custom business object you created.

Now that we are all on the same page lets start playing with the Column collection.

Since auto generation is no longer needed, go ahead and either remove AutoGenerateColumns or set it to False.

<my:DataGrid x:Name="dg" AutoGenerateColumns="False"></my:DataGrid>

Now add the Columns collection, and add a TextBox column in it bound to the "FirstName" property.

<my:DataGrid x:Name="dg" AutoGenerateColumns="False">
    <my:DataGrid.Columns>
        <my:DataGridTextBoxColumn Header="First Name" 
                DisplayMemberBinding="{Binding FirstName}" />
    </my:DataGrid.Columns>
</my:DataGrid>

The code above is doing a few things:

  1. It creates the Columns collection using the <my:DataGrid.Columns> tag.
  2. Inside that collection it creates a new DataGridTextBoxColumn
  3. The content that you see in the column header is set to "First Name" using the Header property
  4. The column is data bound to the FirstName property using the DisplayMemberBinding property.  Notice that since the DataContext of the DataGrid is its ItemsSource collection a Source does not need to be specified. 

Side Note: Some of you might be wondering why we use DisplayMemberBinding here instead of a string like DisplayMemberPath.  The thinking behind this decision was that a binding gives you a lot more control of what ends up being shown.  It allows the use of converters, and leaves the door open for the addition of validation, and formatters.

If you run this you should see something like this:

FirstColumn

In addition to the Header and DisplayMemberBinding properties, you can also customize the column through properties such as:

Column Properties:

  • CanUserResize - Determines if this column can be resized.  In regards to its relationship with the DataGrid property CanUserResizeColumns, false always wins.
  • IsReadOnly - Determines if this column can be edited.  In regards to its relationship with the DataGrid property IsReadOnly, true always wins.
  • Width - Sets the width of the column.  Setting this value overrides the DataGrid ColumnWidth property.
  • MinWidth - Sets the minimum width for this column.
  • Visibility - Hides or shows this column
  • ElementStyle - The property used to style the display element for a column.  (In the case of the TextBox Column the display element is the TextBlock)
  • EditingElementStyle - The property used to style the editing element for a column.  (In the case of the TextBox Column the display element is the TextBox)

TextBox Column Specific Properties:

  • FontFamily - Sets the FontFamily property on both the TextBlock and TextBox.
  • FontSize - Sets the FontSize property on both the TextBlock and TextBox.
  • FontStyle - Sets the FontStyle property on both the TextBlock and TextBox.
  • FontWeight - Sets the FontWeight property on both the TextBlock and TextBox.
  • Foreground - Sets the Foreground property on both the TextBlock and TextBox.

Side Note: You might have noticed that you cannot specify certain properties such as FontFamily or FontSize using the ElementStyle and EditingElementStyle properties.  This is due to a workaround for the current lack of Font inheritance in the framework.  If and when this functionality is added, the workaround will no longer be necessary and this functionality could return.

CheckBox Column Specific Properties:

  • CheckBoxContentBinding - Similar to DisplayMemberBinding which specifies the IsChecked binding, this property allows you to specify a binding for the Content property of the CheckBox.
  • IsThreeState - Sets the IsThreeState property on the CheckBoxes

Lets now finish off the columns that we used to have by adding two more DataGridTextBoxColumns and a DataGridCheckBoxColumn bound to "LastName", "Age", and "Available".

<my:DataGrid x:Name="dg" AutoGenerateColumns="False" >
    <my:DataGrid.Columns>
        <my:DataGridTextBoxColumn Header="First Name"
                DisplayMemberBinding="{Binding FirstName}" />
        <my:DataGridTextBoxColumn Header="Last Name" 
                DisplayMemberBinding="{Binding LastName}" />
        <my:DataGridTextBoxColumn Header="Age" Width="50" 
                DisplayMemberBinding="{Binding Age}" />
        <my:DataGridCheckBoxColumn Header="Available" 
                DisplayMemberBinding="{Binding Available}" />
    </my:DataGrid.Columns>
</my:DataGrid>

Now when you run the application you have the same output as the auto generation, except now you control the order, what the header says, and you can set properties.  For instance in the code above the width of the Age column is set to 50 instead of the usual 100.

AllColumns

 

Unleashing the Template Column

Now that we have recreated the auto generated DataGrid from before let's add some new functionality, specifically a column that can be bound to a DateTime.  To do this we can either create a new column type, or just use a template column.  Since we only plan on using this column once, let's use a template column.

First let's add something for this column to bind to.  Going back to your Data class, add the following property:

C#

public DateTime Birthday{ get; set; }

VB

Private _birthday As DateTime

Property Birthday() As DateTime
    Get
        Return _birthday
    End Get
    Set(ByVal value As DateTime)
        _birthday = value
    End Set
End Property

Also we need to update the Page.xaml code behind to initialize the new property:

C#

public Page()
{
    InitializeComponent();

    List<Data> source = new List<Data>();
    int itemsCount = 100;

    for (int i = 0; i < itemsCount; i++)
    {
        source.Add(new Data()
        {
            FirstName = "First",
            LastName = "Last",
            Age = i,
            Available = i % 2 == 0,
            Birthday = DateTime.Today.AddYears(-i)
        });
    }

    dg.ItemsSource = source;                
}

VB

Public Sub New()
    InitializeComponent()

    Dim Source As List(Of Data) = New List(Of Data)
    Dim ItemsCount As Integer = 100

    For index As Integer = 1 To ItemsCount
        Source.Add(New Data() With _
        { _
            .FirstName = "First", _
            .LastName = "Last", _
            .Age = index, _
            .Available = (index Mod 2 = 0), _
            .Birthday = DateTime.Today.AddYears(-index) _
        })
    Next

    dg.ItemsSource = Source
End Sub

Now that the data is there we can bind to it.  Going back to the Page.xaml file add a DataGridTemplateColumn to the Columns collection right after the "Available" column.

<my:DataGrid x:Name="dg" AutoGenerateColumns="False" >
    <my:DataGrid.Columns>
        <my:DataGridTextBoxColumn Header="First Name"
                DisplayMemberBinding="{Binding FirstName}" />
        <my:DataGridTextBoxColumn Header="Last Name" 
                DisplayMemberBinding="{Binding LastName}" />
        <my:DataGridTextBoxColumn Header="Age" Width="50"
                DisplayMemberBinding="{Binding Age}" />
        <my:DataGridCheckBoxColumn Header="Available" 
                DisplayMemberBinding="{Binding Available}" />

        <my:DataGridTemplateColumn Header="Birthday">
            <my:DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Birthday}" 
                        FontFamily="Trebuchet MS" FontSize="11" 
                        Margin="5,4,5,4"/>
                </DataTemplate>
            </my:DataGridTemplateColumn.CellTemplate>
            <my:DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <DatePicker 
                        SelectedDate="{Binding Birthday, Mode=TwoWay}" />
                </DataTemplate>
            </my:DataGridTemplateColumn.CellEditingTemplate>
        </my:DataGridTemplateColumn>

    </my:DataGrid.Columns>
</my:DataGrid>

What's going on in the code above:

  1. A DataGridTemplateColumn is being added to the Columns collection with its header being set to "Birthday".
  2. The CellTemplate property, which takes a DataTemplate, is being used to define the UI that cells in the column will use to display the data.
  3. In this instance, the content of that DataTemplate is a TextBlock that has its Text property bound to the "Birthday" property on the Data Item.  Once again notice that no binding source is necessary since the default DataContext of a Template column's contents is the data item that it will be representing.
  4. In addition to the CellTemplate the DataGridTemplateColumn's CellEditingTemplate property is also being set.  This property is also a DataTemplate and defines the UI that cells in the column will display during edit mode.
  5. In this instance, the content of the CellEditingTemplate's DataTemplate is a DatePicker control that has its SelectedDate property also bound to the "Birthday" property.
  6. Notice that its binding has the Mode property set to TwoWay.  This is crucial to make sure that any changes made to this property at runtime will be pushed back into the data source. Note that you do not need to set the Mode when you are using the DisplayMemberBinding property in column types other than DataGridTemplateColumn because the columns themselves take care of this.

When you run this you should see a new column in your DataGrid that uses a TextBlock to display a date, and then switches to a DatePicker during edit mode to provide a richer input experience.

(If your DataGrid still has all of the properties set from Step 3 of the last post you need to set IsReadOnly back to False if you want to be able to see the DatePicker in edit mode)

TemplateColumn

 

Using Value Converters

This column is pretty much exactly what we want, but there is a finishing touch missing.  If you look at the text, it has an annoying 12:00:00 AM after each date.  Since we only care about the date itself and not the time, let's reformat this string to the short date format that matches what is shown in the DatePicker.

Add a new class to your Silverlight project and name it "DateTimeConverter".  Then add:

C#

using System.Windows.Data;
using System.Globalization;

public class DateTimeConverter: IValueConverter
{
    public object Convert(object value, 
                       Type targetType, 
                       object parameter, 
                       CultureInfo culture)
    {
        DateTime date = (DateTime)value;
        return date.ToShortDateString();
    }

    public object ConvertBack(object value, 
                              Type targetType, 
                              object parameter, 
                              CultureInfo culture)
    {
        string strValue = value.ToString();
        DateTime resultDateTime;
        if (DateTime.TryParse(strValue, out resultDateTime))
        {
            return resultDateTime;
        }
        return value;
    }
}

VB

Imports System.Windows.Data

Public Class DateTimeConverter
    Implements IValueConverter

    Public Function Convert(ByVal value As Object, _
                ByVal targetType As System.Type, _
                ByVal parameter As Object, _
                ByVal culture As System.Globalization.CultureInfo) _
                As Object _
                Implements System.Windows.Data.IValueConverter.Convert

        Dim DateValue As DateTime = value
        Return DateValue.ToShortDateString()

    End Function

    Public Function ConvertBack(ByVal value As Object, _
                ByVal targetType As System.Type, _
                ByVal parameter As Object, _
                ByVal culture As System.Globalization.CultureInfo) _
                As Object _
                Implements System.Windows.Data.IValueConverter.ConvertBack

        Dim StrValue As String = value.ToString()
        Dim ResultDateTime As DateTime

        If DateTime.TryParse(StrValue, ResultDateTime) Then
            Return ResultDateTime
        End If

        Return value

    End Function
End Class

If you look at the code, a converter is a class that implements IValueConverter and as part of that provides two methods: Convert and ConvertBack.  These two methods are where you can perform your custom logic, in this case parsing a DateTime to and from a short date string format.

Now that you have your reusable converter class you can use it in data binding.

Back in your Page.xaml add a local xmlns.  To do this add the following in the UserControl tag next to the other xmlns declarations:

xmlns:local="clr-namespace:_____________"

Where the _________ is the name of your Application.  IntelliSense should provide the correct syntax as an option in the dropdown.  Next add the converter to your UserControl as a static resource.

<UserControl.Resources>
    <local:DateTimeConverter x:Key="DateConverter" />
</UserControl.Resources>

Finally use the converter in the TextBox's binding in the DataGridTemplateColumn. 

<TextBlock 
    Text="{Binding Birthday, Converter={StaticResource DateConverter}}" 
    FontFamily="Trebuchet MS" FontSize="11" 
    Margin="5,4,5,4"/>

Now when you run the application, the text will be formatted in the manner that the converter specified.

TemplateColumnWithConverter

Using The Silverlight DataGrid

If you have ever worked on an application that displayed large amounts of data, one of the cornerstones of your application was probably a DataGrid control.  We have provided four .NET DataGrids over the years, two for ASP.NET and two for Windows Forms, but until now Silverlight and WPF were left out of the party. 

At MIX 2008 we shipped the first preview of the Silverlight DataGrid and a preview of it in WPF was also shown.  Now that it is out there people want to know how to use it.  If you are one of those people, then you have come to the right place.  Here's a quick guide on how to get up and running with a Silverlight DataGrid.

Step 0: Create A Silverlight Project

Start a new Silverlight Application as outlined in my previous post.  When given the option, choose the default "Add a new Web" option.

Step 1: Add a DataGrid

If everything went smoothly in the previous step, your project should be loaded and opened to Page.xaml.  Now just find the DataGrid on the Toolbox and drag it into the root layout Grid named "LayoutRoot".

 DataGridOnToolBox

This does a few things behind the scenes:

  1. It adds a reference in your Silverlight project to System.Windows.Controls.Data
  2. It adds an xmlns called "my" to the root UserControl that specifies that the DataGrid is in the System.Windows.Controls namespace and located in the System.Windows.Controls.Data assembly
      xmlns:my="clr-namespace:System.Windows.Controls; assembly=System.Windows.Controls.Data"
  3. It adds an instance of the DataGrid as a child of "LayoutRoot"
      <my:DataGrid></my:DataGrid>

If you are the type of the person who likes to see things working after each step, feel free to F5 (choose the option to allow debugging in the popup) and take in the awesome sight that is an empty DataGrid.

EmptyDataGrid

Not much here, so lets fill it with something.

 Step 2: Set the ItemsSource and AutoGenerateColumns

The way to make a DataGrid interesting is by giving it some data.  This is done through the DataGrid's ItemsSource property.  This is same property that other controls in WPF and Silverlight, such as ListBox, use to specify where they will get their data.  The one difference here is that you cannot place arbitrary content in it and have it create a collection for you.

Instead you need to provide it a collection of anything that implements IEnumerable such as a List or ObservableCollection.

The ItemsSource can be specified inline in XAML such as:

<my:DataGrid x:Name="dg" AutoGenerateColumns="True">
    <my:DataGrid.ItemsSource>
        <!--Something that implements IEnumerable -->
    </my:DataGrid.ItemsSource>
</my:DataGrid>

However, it is more commonly set in code behind, which is what we will do in this example. 

Step 2 A: Name the Grid and Set AutoGenerateColumns to True

Before we go to the code behind there are two things that we will want to do while we are still in XAML:

<my:DataGrid x:Name="dg" AutoGenerateColumns="True"></my:DataGrid>
  1. Give the DataGrid the name "dg"
  2. Set AutoGenerateColumns to true.  If you have worked with previous DataGrids you might have encountered the AutoGenerateColumns property.  This is a convenience property that lets you get up and running quickly, since it will tell the DataGrid to look at the items in its ItemsSource and try to create a column to display each field or property.  You should note that columns are only generated when the ItemsSource property is being set or changed, so if you change the value of AutoGenerateColumns after the DataGrid already has an ItemsSource specified, no columns will be generated until the next time the ItemsSource property is changed.

Step 2 B: Create and Set the Items Source

Now that the DataGrid is ready to have its ItemsSource set, go to the Page's constructor located in the code behind file for Page.xaml (A handy shortcut to do this from within Page.xaml is F7) and add the following line below InitializeComponent:

C#

public Page()
{
    InitializeComponent();
    dg.ItemsSource = "H e l l o W o r l d !".Split();
}

VB

Public Sub New()
    InitializeComponent()
    dg.ItemsSource = "H e l l o W o r l d !".Split()
End Sub

(If you get the build error: "The name 'dg' does not exist in the current context" with the code above be sure to build a second time so that the name has a chance to propigate) 

One of the easiest ways to generate an IEnumerable collection is String.Split.  When the resulting array is set as the ItemsSource of the DataGrid a column will be automatically generated since AutoGenerateColumns is true.  When you run the application, it will look like this:

StringSplitDataGrid

This is a little better, but so far this could be done with a ListBox.  Lets add some more complicated data so that we actually need to use a DataGrid.

Add a new class to your Silverlight project (not the Web project) and name it "Data".

AddNewClass

Then add a few properties to bind to.

C#

If you are using C#, you can use the great 3.0 Automatic Properties feature.

public class Data
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public bool Available { get; set; }
}

VB

Public Class Data
    Private _firstName As String
    Private _lastName As String
    Private _age As Integer
    Private _available As Boolean

    Property FirstName() As String
        Get
            Return _firstName
        End Get
        Set(ByVal value As String)
            _firstName = value
        End Set
    End Property

    Property LastName() As String
        Get
            Return _lastName
        End Get
        Set(ByVal value As String)
            _lastName = value
        End Set
    End Property

    Property Age() As Integer
        Get
            Return _age
        End Get
        Set(ByVal value As Integer)
            _age = value
        End Set
    End Property

    Property Available() As Boolean
        Get
            Return _available
        End Get
        Set(ByVal value As Boolean)
            _available = value
        End Set
    End Property
End Class

Once the Data class is defined, it can now be used to provide data for the DataGrid.  Go back to the code behind file for Page.xaml and replace the previous ItemsSource assignment with the following.  A useful trick here is to use the C# and VB 3.0 Object Initializer feature to initialize the Data objects as we add them to the List.

C#

public Page()
{
    InitializeComponent();
    //dg.ItemsSource = "H e l l o W o r l d !".Split();

    List<Data> source = new List<Data>();
    int itemsCount = 100;

    for (int i = 0; i < itemsCount; i++)
    {
        source.Add(new Data() 
        { 
            FirstName = "First", 
            LastName = "Last", 
            Age = i, 
            Available = (i % 2 == 0) 
        });
    }

    dg.ItemsSource = source;
}

VB

Public Sub New()
    InitializeComponent()
    'dg.ItemsSource = "H e l l o W o r l d !".Split()

    Dim Source As List(Of Data) = New List(Of Data)
    Dim ItemsCount As Integer = 100

    For index As Integer = 1 To ItemsCount
        Source.Add(New Data() With _
        { _
            .FirstName = "First", _
            .LastName = "Last", _
            .Age = index, _      .Available = (index Mod 2 = 0) _
        })
    Next

    dg.ItemsSource = Source
End Sub

When you run this auto generation takes over using reflection to create a column for each property in Data setting the column header to the name of the property and choosing default column types based on the property type.  For instance the Available column is a DataGridCheckBoxColumn.

 DataListDataGrid

Step 3: Simple Customization of the DataGrid

The easiest way to customize the DataGrid is through a variety of properties.  The other two ways are through Styles and Templates which will be covered in future posts.  Some of the most useful properties for customization are:

GridlinesVisibility & HeadersVisibility

These properties are enumerations that control what gridlines and headers are displayed.

RowBackground & AlternatingRowBackground

These properties are shortcuts to setting the background color for both rows and alternating rows.

ColumnWidth & RowHeight

These properties set the default column width and default row height.

IsReadOnly & CanUserResizeColumns

These properties control if the end user can edit the data in the grid and if the columns can be resized.

For instance if you set the following properties to the following values:

<my:DataGrid x:Name="dg" AutoGenerateColumns="True" 
    GridlinesVisibility="Horizontal" HeadersVisibility="Column"
    RowBackground="Cornsilk" AlternatingRowBackground="LemonChiffon"
    ColumnWidth="85" RowHeight="30"
    IsReadOnly="True" CanUserResizeColumns="False"
    >
</my:DataGrid>

You would get this:

CustomizedDataGrid

Step 4: Enjoy

Now that you have the basics, enjoy using the DataGrid.  Next time I'll go into how to explicitly define and customize columns instead of using auto generation.

Getting Started With Silverlight

Odds are if you are on this site you are probably interested in building Silverlight applications. Here’s the rundown of what you need to get up and running.

What You Will Need

You will need either Visual Studio 2008, or the Expression Blend 2.5 Preview.

(For now, you will need Visual Studio 2008 Standard or above. Unfortunately it looks like the Express versions cannot author Silverlight content just yet.)

What Else You Will Need

(All of these are at the very bottom of http://silverlight.net/GetStarted/)

If you encounter any problems, Bradley Bartz has some great troubleshooting tips here: http://weblogs.asp.net/bradleyb/archive/2008/03/06/installation-tips-for-sivliverlight-tools-beta-1-for-visual-studio-2008.aspx

Starting Your First Project

Once you have all of those pieces installed you should be ready to create Silverlight 2 applications. Just Open Visual Studio 2008 and choose File -> New Project. Choose the Silverlight node and choose Silverlight Application. Give it a better name than SilverlightApplication1 and hit OK.

clip_image002

Once you do that there is one last question to answer, do you want to add a web site to host your Silverlight control, or do you want to have this page "automagically" generated each time you run your application.

clip_image004

Pick the Web Site option if:

  • You want to work with Web Services
  • You want to perform cross domain calls
  • You want to reference images that are not part of your application

Pick the Test Page option if:

  • You just want to write a quick sample that only works with local data and resources
  • You plan on adding this application to an existing Web Site

 

Now that you are up and running you can start playing around with Silverlight.  If this is your first time with Silverlight and WPF, Scott Guthrie has a great introductory walk through, or if you are interested in doing things with the Silverlight DataGrid check out these posts.