This post has been updated to work with the RTW version of the Silverlight 2 DataGrid. The code examples are not guaranteed to work with previous Beta versions of the DataGrid. Read more about the features that the Silverlight 2 DataGrid has to offer... |
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 in a different post) but the easiest way, and the topic of our discussion today, is by using the Columns collection.
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.
First we need to get our project back to where we ended last time. Go ahead and follow the steps 1 and 2here.
If you followed steps 1 & 2 from the previous post, your DataGrid should look something like this:
<data:DataGrid x:Name="dg"></data: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 set AutoGenerateColumns to False.
<data:DataGrid x:Name="dg" AutoGenerateColumns="False"></data:DataGrid>
Now add the Columns collection, and add a Text column in it bound to the "FirstName" property.
<data:DataGrid x:Name="dg" AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}" />
</data:DataGrid.Columns>
</data:DataGrid>
The code above is doing a few things:
Side Note: Some of you might be wondering why we use Binding 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:
In addition to the Header and Binding properties, you can also customize the column through properties such as:
Column Properties:
TextBox Column Specific Properties:
CheckBox Column Specific Properties:
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".
<data:DataGrid x:Name="dg" AutoGenerateColumns="False" >
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}" />
<data:DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}" />
<data:DataGridTextColumn Header="Age" Width="50"
Binding="{Binding Age}" />
<data:DataGridCheckBoxColumn Header="Available"
Binding="{Binding Available}" />
</data:DataGrid.Columns>
</data: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 Auto.
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.
xmlns:basics="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls"
<data:DataGrid x:Name="dg" AutoGenerateColumns="False" >
<data:DataGrid.Columns>
<data:DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}" />
<data:DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}" />
<data:DataGridTextColumn Header="Age" Width="50"
Binding="{Binding Age}" />
<data:DataGridCheckBoxColumn Header="Available"
Binding="{Binding Available}" />
<data:DataGridTemplateColumn Header="Birthday">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Birthday}" Margin="4"/>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<basics:DatePicker
SelectedDate="{Binding Birthday, Mode=TwoWay}" />
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
Warning: Be sure to add the DatePicker from the ToolBox so the basics namespace and associated reference to System.Windows.Controls is added for you.
What's going on in the code above:
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)
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}}"
Margin="4"/>
Now when you run the application, the text will be formatted in the manner that the converter specified.