1. Problem
You need a customized way of viewing and editing data that is not supported out of the box by any of the typed DataGridColumns like DataGridTextColumn or DataGridCheckBoxColumn. For example, say you want to view a color value rendered as a color stripe instead of the color name string literal.
2. Solution
Use the CellTemplate and the CellEditingTemplate properties of the DataGridTemplateColumn to apply custom viewing and editing templates.
3. How It Works
The various DataGridColumn types like DataGridTextColumn and DataGridCheckBoxColumn are designed to support binding to specific CLR data types, such as String and Boolean,
or to types that can be automatically converted to these types. The way
that the data is viewed and edited in cells of these specific column
types is predetermined by the framework. However, the need for custom
UIs for viewing and editing data was well anticipated; the DataGridTemplateColumn is supplied for exactly that purpose.
The DataGridTemplateColumn exposes two properties, CellTemplate and CellEditingTemplate, both of which accept data templates. When the column is data bound, the DataGrid binds the cell data item to the data template specified in CellTemplate to display the data in view mode. When the cell enters edit mode, the DataGrid switches to the CellEditingTemplate.
4. The Code
In this sample, you use a DataGrid bound to product data fetched from the AdventureWorks WCF service. The Product class exposes a Color property, which is defined as a String on the class. You bind the Color property to one of the DataGrid
columns and thereby create a more intuitive interface where the user
can actually view the color itself for both viewing and editing the Color value, as compared to the default string editing experience exposed by the DataGridTextColumn. Listing 1 shows the XAML.
Listing 1. XAML for the MainPage Used to Demonstrate Custom DataGrid Column Templates
<UserControl x:Class="Recipe5_6.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data=
"clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:local="clr-namespace:Recipe5_6"
Width="800" Height="400"
>
<UserControl.Resources>
<local:ColorNameToBrushConverter x:Key="REF_ColorNameToBrushConverter"/>
<DataTemplate x:Key="dtColorViewTemplate">
<Border CornerRadius="5,5,5,5" BorderBrush="Black"
BorderThickness="1,1,1,1" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" Margin="1,1,1,1"
Background="{Binding Color,
Converter={StaticResource REF_ColorNameToBrushConverter}}"/>
</DataTemplate>
<DataTemplate x:Key="dtColorEditingTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
ItemsSource="{Binding ColorList}"
SelectedItem="{Binding Color, Mode=TwoWay}"
Height="200">
<ListBox.ItemTemplate>
<DataTemplate>
<Border CornerRadius="5,5,5,5" BorderBrush="Black"
BorderThickness="1,1,1,1" Height="25" Width="70"
Margin="2,5,2,5"
Background=
"{Binding Converter=
{StaticResource REF_ColorNameToBrushConverter}}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<data:DataGrid x:Name="dgProducts" AutoGenerateColumns="False">
<data:DataGrid.Columns>
<data:DataGridTextColumn Binding="{Binding ProductID}"
Header="ID" />
<data:DataGridTextColumn Binding="{Binding Name}"
Header="Name" />
<data:DataGridTemplateColumn
CellTemplate="{StaticResource dtColorViewTemplate}"
CellEditingTemplate="{StaticResource dtColorEditingTemplate}"
Header="Color" Width="100"/>
</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>
|
In the DataGrid declaration named dgProducts, you use a DataGridTemplateColumn to bind to Product.Color. To get the custom UI for viewing and editing the Color property, you define two data templates, dtColorTemplate and dtColorEditingTemplate, and use them to set the CellTemplate and the CellEditingTemplate properties.
In view mode, where the bound DataGridTemplateColumn uses the CellTemplate to bind the data, you bind the Color value to the Background property of a Border, as shown in the dtColorViewTemplate template. In edit mode, where CellEditingTemplate is used, dtColorEditingTemplate uses a ListBox to display the list of available colors. The ListBox.SelectedItem is bound to Product.Color to represent the currently selected color. The binding mode is set to TwoWay so that any changes made by the user updates the Product instance and is reflected in the DataGrid when the cell moves out of edit mode.
Listing 2 shows the codebehind for the page.
Listing 2. Codebehind for the MainPage Demonstrating Custom DataGrid Column Templates
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using Recipe5_6.AdvWorks;
namespace Recipe5_6
{
public partial class MainPage : UserControl
{
AdvWorksDataServiceClient client =
new AdvWorksDataServiceClient();
bool EditingColor = false;
public MainPage()
{
InitializeComponent();
GetData();
}
private void GetData()
{
client.GetProductsCompleted +=
new EventHandler<GetProductsCompletedEventArgs>(
delegate(object sender, GetProductsCompletedEventArgs e)
{
dgProducts.ItemsSource = e.Result;
});
client.GetProductsAsync();
}
}
public class ColorNameToBrushConverter : IValueConverter
{
//convert from a string Color name to a SolidColorBrush
public object Convert(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//substitute a null with Transparent
if (value == null)
value = "Transparent";
//make sure the right types are being converted
if (targetType != typeof(Brush) || value.GetType() != typeof(string))
throw new NotSupportedException(
string.Format("{0} to {1} is not supported by {2}",
value.GetType().Name,
targetType.Name,
this.GetType().Name));
SolidColorBrush scb = null;
try
{
//get all the static Color properties defined in
//System.Windows.Media.Colors
List<PropertyInfo> ColorProps = typeof(Colors).
GetProperties(BindingFlags.Public | BindingFlags.Static).ToList();
//use LINQ to find the property whose name equates
//to the string literal we are trying to convert
List<PropertyInfo> piTarget = (from pi in ColorProps
where pi.Name == (string)value
select pi).ToList();
//create a SolidColorBrush using the found Color property.
//If none was found i.e. the string literal being converted
//did not match any of the defined Color properties in Colors
//use Transparent
scb = new SolidColorBrush(piTarget.Count == 0 ?
Colors.Transparent : (Color)(piTarget[0].GetValue(null, null)));
}
catch
{
//on exception, use Transparent
scb = new SolidColorBrush(Colors.Transparent);
}
return scb;
}
//convert from a SolidColorBrush to a string Color name
public object ConvertBack(object value, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
//make sure the right types are being converted
if (targetType != typeof(string) || value.GetType() != typeof(Brush))
throw new NotSupportedException(
string.Format("{0} to {1} is not supported by {2}",
value.GetType().Name,
targetType.Name,
this.GetType().Name));
string ColorName = null;
try
{
//get all the static Color properties defined
//in System.Windows.Media.Colors
List<PropertyInfo> ColorProps = typeof(Colors).
GetProperties(BindingFlags.Public | BindingFlags.Static).ToList();
//use LINQ to find the property whose value equates to the
//Color on the Brush we are trying to convert
List<PropertyInfo> piTarget = (from pi in ColorProps
where (Color)pi.GetValue(null, null)
== ((SolidColorBrush)value).Color
select pi).ToList();
//If a match is found get the Property name, if not use "Transparent"
ColorName = (piTarget.Count == 0 ? "Transparent" : piTarget[0].Name);
}
catch
{
//on exception use Transparent
ColorName = "Transparent";
}
return ColorName;
}
}
}
namespace Recipe5_6.AdvWorks
{
public partial class Product
{
private ObservableCollection<string> _ColorList;
//color literals defined in System.Windows.Media.Colors
public ObservableCollection<string> ColorList
{
get
{
return new ObservableCollection<string> {
"Black",
"Blue",
"Brown",
"Cyan",
"DarkGray",
"Gray",
"Green",
"LightGray",
"Magenta",
"Orange",
"Purple",
"Red",
"Transparent",
"White",
"Yellow" };
}
}
}
}
|
The ListBox.ItemsSource is bound to the Product.ColorList property, defined in a partial extension of the Product proxy data type, which returns a collection of string literals representing names of the Color properties, as defined in the System.Windows.Media.Colors type. To display each item, an ItemTemplate similar to that in the view mode is used, where a Border is used to display the color choice by binding the Color value to Border.Background.
Figure 1 compares the DataGrid Color column in view mode and edit mode.
Also note in Listing 2 the use of a value converter of type ColorNameToBrushConverter. Since the Border.Background property is of type SolidColorBrush and Product.Color is a string literal, you need to facilitate an appropriate value conversion. Listing 5-11 shows the value converter code as well in the ColorNameToBrushConverter class.
In both the conversion
functions in the value converter implementation, you use reflection to
enumerate the list of static properties of type Color defined on the type System.Windows.Media.Colors, each of which are named for the Color they represent. In Convert(), while trying to convert a Color name string to a SolidColorBrush, you find the matching Color property in the enumerated list of properties and use that to create and return the brush. In ConvertBack(), while trying to convert a SolidColorBrush to a color name string, you find the property with a Color value matching the SolidColorBrush.Color and use the property name as the color name string. If no matches are found or if exceptions occur, it falls back to Colors.Transparent as the default value.