WPF 4.0 will finally contain a DataGrid. If you can't wait for that one, then all you have to do is download the current release of the WPF Toolkit. The DataGrid control in this toolkit is considered as stable, so why not give it a test drive? Let's create a list of Formula 1 Drivers and two-way data bind it to a WPF DataGrid. In our little object model -the ViewModel if you like- a Formula 1 Driver is associated to a Formula 1 Team:
Here's the startup code for the Formula 1 Driver class:
namespace U2UConsult.DockOfTheBay
{
using System.Collections.Generic;
/// <summary>
/// A Formula1 Team.
/// </summary>
public class FormulaOneTeam
{
/// <summary>
/// Gets the entire list of Formula1 teams.
/// </summary>
public static Dictionary<int, FormulaOneTeam> GetAll
{
get
{
return new Dictionary<int, FormulaOneTeam>()
{
{ 0, new FormulaOneTeam { TeamId = 0, Name = "Unknown" } },
{ 1, new FormulaOneTeam { TeamId = 1, Name = "Nintendo" } },
{ 2, new FormulaOneTeam { TeamId = 2, Name = "Top Gear" } },
{ 3, new FormulaOneTeam { TeamId = 3, Name = "Wacky Races" } }
};
}
}
/// <summary>
/// Gets or sets the id of the Formula1 team.
/// </summary>
public int TeamId { get; set; }
/// <summary>
/// Gets or sets the name of the Formula1 team.
/// </summary>
public string Name { get; set; }
}
}
In a real life scenario a class that's involved in data binding should implement the INotifyPropertyChanged and IDataError interfaces. An example of the latter can be found in a previous article, an alternative for validating is the BindingGroup class, that I will discuss in a future article (no hyperlink yet ). In M-V-VM applications you will probably implement this behavior by inheriting from some ViewModel base class.
Here's the implementation of the Formula 1 Team class:
namespace U2UConsult.DockOfTheBay
{
using System.Collections.Generic;
/// <summary>
/// A Formula1 Team.
/// </summary>
public class FormulaOneTeam
{
/// <summary>
/// The list of all Formula 1 Teams.
/// </summary>
private static Dictionary<int, FormulaOneTeam> getAll;
/// <summary>
/// Initializes static members of the FormulaOneTeam class.
/// </summary>
static FormulaOneTeam()
{
getAll = new Dictionary<int, FormulaOneTeam>()
{
{ 0, new FormulaOneTeam { TeamId = 0, Name = "Unknown" } },
{ 1, new FormulaOneTeam { TeamId = 1, Name = "Nintendo" } },
{ 2, new FormulaOneTeam { TeamId = 2, Name = "Top Gear" } },
{ 3, new FormulaOneTeam { TeamId = 3, Name = "Wacky Races" } }
};
}
/// <summary>
/// Gets the entire list of Formula 1 Teams.
/// </summary>
public static Dictionary<int, FormulaOneTeam> GetAll
{
get
{
return getAll;
}
}
/// <summary>
/// Gets or sets the id of the Formula 1 Team.
/// </summary>
public int TeamId { get; set; }
/// <summary>
/// Gets or sets the name of the Formula 1 Team.
/// </summary>
public string Name { get; set; }
}
}
The ideal collection type for complex data binding (that's binding one control to a collection of objects) is ObservableCollection(T). ObservableCollection(T) is to WPF what BindingList(T) is to WinForms. ObservableCollection(T) only implements the INotifyCollectionChanged interface. This is sufficient to do complex data binding -at least for WPF's ItemControl subclasses like ListView, ComboBox and DataGrid. In a WinForms application the ObservableCollection(T) class loses all its magic. To continue with the sample, add to the Formula 1 Driver class a method that returns such a collection:
/// <summary>
/// Gets a two-way bindable list of Formula 1 drivers.
/// </summary>
public static ObservableCollection<FormulaOneDriver> GetAll
{
get
{
ObservableCollection<FormulaOneDriver> drivers =
new ObservableCollection<FormulaOneDriver>()
{
new FormulaOneDriver(){ Name = "Super Mario",
TeamId = 1,
PolePositions = 2 },
new FormulaOneDriver(){ Name = "The Stig",
TeamId = 2,
PolePositions = 20,
LatestVictory = DateTime.Today },
new FormulaOneDriver(){ Name = "Dick Dastardley",
TeamId = 3,
PolePositions = 0 },
new FormulaOneDriver(){ Name = "Luigi",
TeamId = 1,
PolePositions = 2 }
};
return drivers;
}
}
If you use this collection as ItemSource of the DataGrid, then the result should look like this:
The DataGrid has a couple of intuitive properties, as you can see in its XAML definition:
<toolkit:DataGrid
x:Name="DriversDataGrid"
ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"
AutoGenerateColumns="False"
CanUserAddRows="True"
CanUserDeleteRows="True"
CanUserSortColumns="True"
CanUserReorderColumns="True"
AlternatingRowBackground="WhiteSmoke"
RowHeaderWidth="16"
Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<!-- toolkit:DataGrid.Columns [...] -->
</toolkit:DataGrid>
The DataGrid can be populated with four column types:
- DataGridTextColumn,
- DataGridCheckBoxColumn,
- DataGridComboBoxColumn,
- DataGridHyperlinkColumn, and
- DataGridTemplateColumn.
This is an example of a DataGridTextColumn:
<toolkit:DataGridTextColumn
Header="Name"
Binding="{Binding Name}"
CanUserReorder="True"
IsReadOnly="False"
CanUserSort="True"
SortMemberPath="Name"/>
Let's look for an excuse to use a DataGridComboBoxColumn. It's not a good idea to confront end users with technical keys, so instead of showing the team's identity we'll display its name. When the grid cell is in edit mode, we let the user select from a ComboBox:
The embedded ComboBox is bound to a Dictionary, its citizens have a Key and a Value property that you should respectively bind to SelectedValuePath and DisplayMemberPath:
<toolkit:DataGridComboBoxColumn
x:Name="TeamsCombo"
Header="Team"
ItemsSource="{Binding Source={x:Static local:FormulaOneTeam.GetAll}}"
SelectedValueBinding="{Binding TeamId}"
SelectedValuePath="Key"
DisplayMemberPath="Value.Name"
SortMemberPath="Team.Name" />
Make sure to specify the correct SortMemberPath. Users can sort the rows by clicking on the column header(s). The 'Team' column is bound to the TeamId, but it displays the team's name, so it should sort by name. The following screenshot shows that the grid is sorted on the Team column (notice the sort icon in the column header):
The LatestVictory property of the Formula 1 Driver is of the type DateTime. In edit mode, it makes sense to use a DatePicker control to let the user select the date. This control can also be found in the WPF Toolkit. Here's how it can be used in a DataGridTemplateColumn:
<toolkit:DataGridTemplateColumn
Header="Latest Victory"
CanUserSort="True"
SortMemberPath="LatestVictory">
<toolkit:DataGridTemplateColumn.CellTemplate >
<DataTemplate >
<toolkit:DatePicker
SelectedDate="{Binding LatestVictory}"
BorderThickness="0"/>
</DataTemplate >
</toolkit:DataGridTemplateColumn.CellTemplate >
</toolkit:DataGridTemplateColumn>
Just like any WPF control the DataPicker is über-stylable. I don't like the default GUI settings for it, so I tweaked the GUI a little bit by hiding its border and giving the embedded TextBox a transparent backcolor. You have access to this via a Style. That's also the place to override the default watermark (through the Text property):
<Style TargetType="{x:Type toolkit:DatePickerTextBox}">
<Setter Property="Text" Value="None" />
<Setter Property="Background" Value="#00000000" />
</Style>
Here's how the DatePicker looks like when it's expanded:
When inserting is allowed, then the DataGrid displays an empty row at the bottom. Unfortunately there seems to be no way to display the intuitive asterisk in its row header. When you start editing this row, the default constructor of the bound type is called to initialize the cells. Here's how the one for the Formula 1 driver looks like:
/// <summary>
/// Initializes a new instance of the FormulaOneDriver class.
/// </summary>
public FormulaOneDriver()
{
this.Name = "<Name>";
}
For completeness' sake, here's the full XAML for the sample form:
<Window
x:Class="U2UConsult.DockOfTheBay.DataGridSampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:U2UConsult.DockOfTheBay"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="WPF DataGrid Sample"
SizeToContent="WidthAndHeight"
Icon="/DataGridSample;component/dotbay.png" >
<Window.Resources>
<Style TargetType="{x:Type toolkit:DatePickerTextBox}">
<Setter Property="Text" Value="None" />
<Setter Property="Background" Value="#00000000" />
</Style>
</Window.Resources>
<Grid>
<toolkit:DataGrid
x:Name="DriversDataGrid"
ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"
AutoGenerateColumns="False"
CanUserAddRows="True"
CanUserDeleteRows="True"
CanUserSortColumns="True"
CanUserReorderColumns="True"
AlternatingRowBackground="WhiteSmoke"
RowHeaderWidth="16"
Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn
Header="Name"
Binding="{Binding Name}"
CanUserReorder="True"
IsReadOnly="False"
CanUserSort="True"
SortMemberPath="Name"/>
<toolkit:DataGridComboBoxColumn
x:Name="TeamsCombo"
Header="Team"
ItemsSource="{Binding Source={x:Static local:FormulaOneTeam.GetAll}}"
SelectedValueBinding="{Binding TeamId}"
SelectedValuePath="Key"
DisplayMemberPath="Value.Name"
SortMemberPath="Team.Name" />
<toolkit:DataGridTextColumn
Header="Pole Positions"
Binding="{Binding PolePositions}"
CanUserSort="True" />
<toolkit:DataGridTemplateColumn
Header="Latest Victory"
CanUserSort="True"
SortMemberPath="LatestVictory">
<toolkit:DataGridTemplateColumn.CellTemplate >
<DataTemplate >
<toolkit:DatePicker
SelectedDate="{Binding LatestVictory}"
BorderThickness="0"/>
</DataTemplate >
</toolkit:DataGridTemplateColumn.CellTemplate >
</toolkit:DataGridTemplateColumn>
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
</Grid>
</Window>
And here's the C# code:
namespace U2UConsult.DockOfTheBay
{
using System.Windows;
/// <summary>
/// Two-way binding to a Data Grid Sample.
/// </summary>
/// <remarks>Not much to see here: it's all in the XAML.</remarks>
public partial class DataGridSampleWindow : Window
{
/// <summary>
/// Initializes a new instance of the DataGridSampleWindow class.
/// </summary>
public DataGridSampleWindow()
{
InitializeComponent();
}
}
}