1. Problem
You want to create a custom column type for a DataGrid to enable specific functionality to handle a particular data type or data item.
2. Solution
Extend the DataGridBoundColumn class, and add functionality to handle the view and edit modes for the intended data type or data item.
3. How It Works
The framework ships with a few prebuilt DataGrid column types for handling some of the standard data types. DataGridTextColumn
is one of the most useful ones and can be used to view and edit any
data that can be converted to a meaningful text representation. DataGridCheckBoxColumn is another one that can be used to view and edit a Boolean value as a CheckBox and with the current value mapped to its checked state.
In some situations, you want to implement custom logic to handle a specific data type or a program data item bound to a DataGrid column. One way to achieve that is through the use of the DataGridTemplateColumn column type and the use of CellTemplate and CellEditingTemplate.
Yet another way is to create a new column type that encapsulates the
custom logic, much like the ones that the framework ships with. The
logic encapsulation in creating this custom column offers you the
advantage of not having to rely on the consumers (UI layer developers)
of your data to supply appropriate data templates, as well as the
ability to standardize and lock down how a specific data type or item
gets treated inside a DataGrid.
In this second approach, you start by creating a new class extending the System.Windows.Controls.DataGridBoundColumn class in the System.Windows.Controls.Data assembly, which is also the base class for the framework-provided column types mentioned earlier. The DataGridBoundColumn
exposes an API of abstract methods that allows you to easily control
the UI and data-binding logic of cells in the custom column as they are
switched between view and edit modes. The methods in this API that you
will override most often are GenerateElement(), GenerateEditingElement(), PrepareCellForEdit(), and CancelCellEdit(). All of these methods, except CancelCellEdit(), are abstract methods; therefore it is mandatory that you provide an appropriate implementation in your custom column code.
3.1. The GenerateElement() Method
This method is expected to create the UI that would be used by the DataGrid to display the bound value in every cell of that column. The created UI is returned in the form of a FrameworkElement from GenerateElement().
By overriding this method and supplying your custom logic, you can
change the UI a bound cell uses to display its content. You are also
expected to create and set appropriate data bindings for your newly
created UI in this method so that data items are appropriately displayed
in every cell. To do that, you can obtain the data binding set by the
user in the XAML for the column through the DataGridBoundColumn.Binding property. You can then use the SetBinding() method to apply that binding to the appropriate parts of the UI you create before returning the UI as a FrameworkElement.
Also note that this method accepts two parameters passed in by the containing DataGrid: a cell of type DataGridCell and a data item of type object. The first parameter contains a reference to the instance of the DataGridCell
that is currently being generated, and the second parameter refers to
the data item bound to the current row. You don't have to use these
parameters to successfully implement this method. One interesting use of
these parameters is in a computed column scenario. Since the dataItem
parameter contains the entire item bound to that row, you can easily
compute a value based on parts of the data item and use that as the cell
value bound to the UI you return from this method. We leave it to you
to experiment further with these parameters.
3.2. The GenerateEditingElement() Method
This method is somewhat similar to GenerateElement()
in purpose but is used for edit mode rather than view mode. When the
user switches a cell to edit mode—for example, by clicking it—the DataGrid calls this method on the column type to generate the UI for the editing experience. The generated UI is again returned as a FrameworkElement.
You can override this method in your custom column type to create a
custom edit UI for your data type or item. The same requirements for
applying the appropriate data bindings before you return the generated
UI, as discussed for GenerateElement() earlier, apply here. Also note that this method accepts the same parameter set as GenerateElement().
3.3. The PrepareCellForEdit() Method
This method is called by the DataGrid to obtain the unedited value from the bound cell before entering edit mode. The unedited value is retained by the DataGrid and made available to you in CancelCellEdit() so that edits made to the cell can be undone should a user choose to cancel an edit operation. The FrameworkElement type you created for the edit mode UI in GenerateEditingElement() is made available to you as the editingElement parameter. You can use that to obtain the current unedited value for the cell. The second parameter to this method, editingEventArgs, is of type RoutedEventArgs.
It contains information about the user gesture that caused the cell to
move to edit mode. For keyboard-based input, it can be cast to KeyEventArgs; for mouse input gestures, it can be cast to MouseButtonEventArgs.
You should check the result of your cast to verify that it is non-null
before using the parameter. This parameter can be used to implement
additional logic, such as different editing behaviors if a specific key
is pressed.
3.4. The CancelCellEdit() Method
This method is called if a user cancels an edit
operation. The unedited value bound to the cell, prior to any changes
made by the user in edit mode, is made available to you via the uneditedValue parameter, as is the FrameworkElement representing the edit UI through the editingElement parameter. You can undo the changes made by resetting the editingElement using the uneditedValue. The uneditedValue parameter is of type object,
and consequently you will need to cast it to the appropriate type based
on the bound data before you use it to reset the edit changes.
4. The Code
The code sample in this recipe creates a custom column type named DataGridDateColumn for editing DateTime types using the DatePicker control. Listing 1 shows the code for DataGridDateColumn.
Listing 1. DataGridDateColumn Class
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Recipe5_7
{
public class DataGridDateColumn : DataGridBoundColumn
{
[TypeConverter(typeof(DataGridDateTimeConverter))]
public DateTime DisplayDateStart { get; set; }
public Binding DisplayDateEndBinding { get; set; }
protected override void CancelCellEdit(FrameworkElement editingElement,
object uneditedValue)
{
//get the DatePicker
DatePicker datepicker = (editingElement as Border).Child as DatePicker;
if (datepicker != null)
{
//rest the relevant properties on the DatePicker to the original value
//to reflect cancellation and undo changes made
datepicker.SelectedDate = (DateTime)uneditedValue;
datepicker.DisplayDate = (DateTime)uneditedValue;
}
}
//edit mode
protected override FrameworkElement GenerateEditingElement(
DataGridCell cell, object dataItem)
{
//create an outside Border
Border border = new Border();
border.BorderBrush = new SolidColorBrush(Colors.Blue);
border.BorderThickness = new Thickness(1);
border.HorizontalAlignment = HorizontalAlignment.Stretch;
border.VerticalAlignment = VerticalAlignment.Stretch;
//create the new DatePicker
DatePicker datepicker = new DatePicker();
//bind the DisplayDate to the bound data item
datepicker.SetBinding(DatePicker.DisplayDateProperty,
base.Binding);
//bind the SelectedDate to the same
datepicker.SetBinding(DatePicker.SelectedDateProperty,
base.Binding);
//bind the DisplayDate range
//start value is provided directly through a property
datepicker.DisplayDateStart = this.DisplayDateStart;
//end value is another binding allowing developer to bind
datepicker.SetBinding(DatePicker.DisplayDateEndProperty,
this.DisplayDateEndBinding);
border.Child = datepicker;
//return the new control
return border;
}
//view mode
protected override FrameworkElement GenerateElement(DataGridCell cell,
object dataItem)
{
//create a TextBlock
TextBlock block = new TextBlock();
//bind the displayed text to the bound data item
block.SetBinding(TextBlock.TextProperty, base.Binding);
//return the new control
return block;
}
protected override object PrepareCellForEdit(FrameworkElement editingElement,
RoutedEventArgs editingEventArgs)
{
//get the datepicker
DatePicker datepicker = (editingElement as Border).Child as DatePicker;
//return the initially displayed date, which is the
//same as the unchanged data item value
return datepicker.DisplayDate;
}
}
}
|
In GenerateElement(), you create a TextBlock as your control of choice to display the bound data. You then set the binding on the TextBlock.Text property to the Binding property on the column so that the date is displayed inside the TextBlock. In GenerateEditingElement(), you instead create a Border and nest a DatePicker control in it for date editing. Once the DatePicker control is created, you set both the DisplayDate (the date displayed in the editable text portion of the DatePicker) and the SelectedDate (the date value selected in the drop-down portion of the DatePicker) initially to the Binding
property on the column. You also set a couple of other bindings that
will be explained later in the recipe before you return the Border. In PrepareCellForEdit(), you return the currently displayed date to the DataGrid for retention in case of a cancellation, and in CancelCellEdit(), you reset the appropriate values on the DatePicker instance to the unedited value saved earlier through PrepareCellForEdit().
Listing 2 shows the XAML declaration of a DataGrid using the DataGridDateColumn type. Again, you use the AdventureWorks WCF service as a data source.
Listing 2. XAML for the MainPage Demonstrating Custom DataGrid Column
<UserControl x:Class="Recipe5_7.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_7"
Width="800" Height="400"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
</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" />
<local:DataGridDateColumn
Binding="{Binding SellStartDate}"
DisplayDateStart="01/01/2000"
DisplayDateEndBinding="{Binding DisplayDateEnd}"
Header="Available From" />
</data:DataGrid.Columns>
</data:DataGrid>
</Grid>
</UserControl>
|
One of the challenges of this approach is that the developer using the DataGridDateColumn may want to control some behavior of the internal DatePicker instance. For example, the DatePicker control exposes DisplayDateStart and DisplayDateEnd properties that determine the date range that the DatePicker drop-down is limited to; however, the developer may want to specify this range when using the DataGridDateColumn. Unfortunately, since the DatePicker control instance is not visible outside the DataGridDateColumn code, there is no direct way for the developer to do so.
One way to allow developers to control these properties is to create corresponding properties on DataGridDateColumn so that they can be set in XAML, and those values can be used in the code to set the DatePicker properties. Referring to the DataGridDateColumn class in Listing 1, you can see the DisplayDateStart property of type DateTime; note that it is being set to a date string in the XAML in Listing 2. The value of this property is then used inside GenerateEditingElement() to set the similarly named property on the DatePicker instance.
Since the date string set in XAML needs to be converted to a DateTime type for the code to work correctly, you need a type conversion mechanism. The framework contains the TypeConverter class, which you can extend to create a type converter of your own. Listing 3 shows a type converter that converts from String to DateTime.
Listing 3. DataGridDateTimeConverter Class
using System;
using System.ComponentModel;
using System.Globalization;
namespace Recipe5_7
{
public class DataGridDateTimeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context,
Type sourceType)
{
return (typeof(string) == sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context,
Type destinationType)
{
return (typeof(DateTime) == destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
DateTime target;
target = DateTime.ParseExact(value as string, "d",
CultureInfo.CurrentUICulture);
return target;
}
}
}
|
The TypeConverterAttribute can be used to attach this type converter to your DataGridDateColumn.DisplayDateStart property, as shown in Listing 1. Note that in overriding the ConvertFrom() method in the TypeConverter implementation, the system passes in a CultureInfo instance as the second parameter. The CultureInfo
parameter allows you to inspect the current culture. If you need to
implement any additional conversion logic based on the locale, you can
check this parameter and take the needed action in your code. In your
case, you do not use the value, but just pass in CultureInfo.CurrentUICulture in your call to Datetime.ParseExact() to allow the DateTime value type to handle the rest of the conversion logic.
You might want to have such a property set as a binding instead of a direct value setting, much like the Binding property on any DataGrid
column. This allows the developer to associate a data binding with the
property and lets its value be derived at runtime from the source it is
bound to, as opposed to being hard-coded.
As an example, say you want to expose a property named DisplayDateEndBinding on the DataGridDateColumn and use that to drive the value of the DisplayDateEnd property of the DatePicker instance. You can see the declaration of this property in Listing 1, and it is bound to a property named DisplayDateEnd on the data source in the XAML in Listing 2. It can then be used to attach the same binding to the DatePicker.DisplayDateEnd property, as shown in the GenerateEditingElement() method in Listing 1. There is not much code to discuss, beyond a call to the AdventureWorks WCF service; we encourage the user to refer to the sample code for the book.
Figure 1 shows the DataGridDateColumn in action.