Category Archives: WPF

Circular Layout Panel

I’ve been given an opportunity to write a custom WPF layout panel. This is something that I’ve been wanting to try for ages but have never really had the need. Rather than jumping straight in to some potentially complex layout algorithms, I figured that I’d start with something trivial just to get the hang of things. Hence the CircularPanel was born.

The CircularPanel is a simple Panel derivative that lays out its children in a circular arrangement. It has some useful dependency properties to allow some customization.

  • StartAngle – Angle in degrees at which the first child element will be positioned.
  • EndAngle – Angle in degrees at which the last child element will be positioned. If the EndAngle forms a complete circle, e.g. Math.Abs(StartAngle-EndAngle) > 360 then rather than positioning the last element on top of the first the element an angle will be included between first and last elements.
  • Padding – As per standard – reduces the space within the panel used to layout the child elements.

When adding these properties I originally missed the AffectsArrange flag on the property registration’s FrameworkElementMetaData. Without this flag changing the property (for instance in the designer) would not cause the control to re-render.

I also added some attached dependency properties. These properties can be used by the child UI elements to dictate an override to the default behaviour.

  • FixedAngle – Overrides the default automatic assignment of angle based on child index. Instead the child element is placed at the fixed angle specified in degrees.
  • RadiusScaleX – Allows the radius to be scaled in the X direction for this child element – defaults to 1.
  • RadiusScaleY – Allows the radius to be scaled in the Y direction for this child element – defaults to 1.

When adding these attached properties I made sure I included the AffectsArrange flag. Of course this wasn’t right – changing the attached property requires the parent to re-calculate the arrangement, it doesn’t affect the applied elements arrangement. I updated the flag to AffectsParentArrange and all was good.

Now I’m not sure how much real-world value this panel has – but I did get the combination of these properties to produce some interesting affects. For example:

Set StartAngle = 0, EndAngle = 1080 and then have each element decrease the RadiusScaleX/Y via binding. This produces a nice sprial.

CircularPanel - Spiral

Add 12 auto-placed elements, then three more using FixedAngle combined with an animation to produce a clock with hour, minute and second hands.

CircularPanel - Clock

Use an animation over StartAngle and EndAngle to produce an effect similar to opening a fan.

CircularPanel - Fan

Here’s the interesting code from the ArrangeOverride method on CircularPanel.

protected override Size ArrangeOverride(Size finalSize)
{
int numberOfVisibleChildren = InternalChildren
.OfType<UIElement>()
.Count(u => u.Visibility != Visibility.Collapsed
&& !GetFixedAngle(u).HasValue); if (numberOfVisibleChildren == 0) return finalSize;
// Short circuit if there are no children int currentChildPosition = 0; double startArcAngle = StartAngle / 180 * Math.PI; double endArcAngle = EndAngle / 180 * Math.PI; double arcDelta; if ( Math.Abs(startArcAngle-endArcAngle) >= Math.PI * 2 ) // If we have a full circle then don't end the last element on the
// EndAngle because that would overlay the StartAngle.
arcDelta = (endArcAngle - startArcAngle ) / (double) numberOfVisibleChildren;
else // If we have less than a full circle then make sure we spread the
// elements with first and last on the start and end angles.
arcDelta = (endArcAngle - startArcAngle) / ((double)numberOfVisibleChildren - 1); double maxChildWidth = InternalChildren
.OfType<UIElement>()
.Max( u => u.DesiredSize.Width ); double maxChildHeight = InternalChildren
.OfType<UIElement>()
.Max( u => u.DesiredSize.Height ); double radiusX = ( finalSize.Width - Padding.Left - Padding.Right - maxChildWidth ) / 2; double radiusY = ( finalSize.Height - Padding.Top - Padding.Bottom - maxChildHeight ) / 2; Point midPoint = new Point( radiusX + Padding.Left + maxChildWidth / 2,
radiusY + Padding.Top + maxChildHeight / 2); foreach (UIElement child in InternalChildren) { var childAngle = startArcAngle + arcDelta * currentChildPosition; double? fixedAngle = GetFixedAngle(child); if (fixedAngle.HasValue) childAngle = fixedAngle.Value / 180 * Math.PI; double x = Math.Cos( childAngle ) * radiusX * GetRadiusScaleX(child) +
midPoint.X - child.DesiredSize.Width / 2; double y = Math.Sin( childAngle ) * radiusY * GetRadiusScaleY(child) +
midPoint.Y - child.DesiredSize.Height / 2; child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
// Ignore collapsed children and FixedAngle children. if ( child.Visibility != Visibility.Collapsed && !fixedAngle.HasValue ) currentChildPosition++; } return finalSize; }

Source code with simple sample application here. It’s a VS2010 solution/project but should be easy to converted to VS2008 – just remove the EasingFunctions in the sample app XAML.

Windows 7 RC and Visual Studio 2010 Beta – Hands on

A while back I posted that I had installed the Windows 7 64bit Beta on my main development machine as a dual boot with Vista and that all looked good. A couple of weeks after that post my machine inexplicably stopped booting into Vista despite my attempts to perform a repair. So I’ve been running Windows 7 now for quite a while.

This week, along with everyone else I re-paved the machine with Windows 7 RC. On top of that I installed Visual Studio 2010 Beta and also jumped through the numerous hoops to install VS2010 Team Foundation Server on a virtual Windows 2008 Server instance running on my WHS box.

So far:

  • The Windows 7 taskbar is improved by the additional key/mouse combos, e.g. Ctrl-LeftClick to quickly cycle through instances of an application.
  • Windows 7 install is still the best Windows installation ever – which is good because for a while there Windows was getting consistently worse.
  • Team Foundation Server is still a long installation process, but mainly now due just to the pre-requisites, e.g. Windows Server, IIS, SharePoint, SQL Server/Reporting Services. The install documentation is good – but it would pay to read through most of it before you install – which of course no-one (myself included) will ever do.
  • Visual Studio 2010 is looking pretty good. If the new WPF text editor is anything to go by then the new font improvements in WPF 4/DirectWrite have worked well.
  • There are typical Beta quirks in VS 2010 – it crashes occasionally, when you zoom in the editor the scroll bars also zoom (which looks most odd).
  • Visual Studio start page is so easy to customize now. This article shows how you simple toggle an option and then build whatever XAML you like for the startpage. Either just extending what’s already there or completely re-writing or re-skinning. Very cool!
  • A number of Visual Studio extensions are beginning to popup. Including editor extensions for creating Regex’s with intellisense, adding images inline with source code as well as some project templates, e.g. WPF application with tray icon.

Xceed WPF DataGrid – Part 1

Vendor Choice

Having met a number of obstacles in getting Microsoft’s WPF DataGrid control to function as I required I decided it may be prudent to look at some of the other offerings. In the past I’ve been a big fan of the DevExpress suite of WinForms controls.

However, DevExpress have been a little slow to get on the WPF bandwagon. In fact even now (April 2009) their grid control is still only in Beta (expecting release in mid 2009). That’s a two full years behind the offerings from Xceed and Infragistics – what were they thinking!? Even if they did release now I’m really not sure I want to be working with a v1 DataGrid control when other vendors have more mature products available.

I remember taking a look at the Xceed WPF DataGrid when it first appeared – back in early 2007. At the time I found the demo app to be quite off-putting. All orange glow and black gloss with ridiculously rounded corners.

However, whilst searching the internet for clues on using Microsoft’s WPF DataGrid I had stumbled across a number of posts/articles related to the Xceed grid. In fact many more so than any other WPF grid control. So from this I figured it must have an active community, which got me to thinking that maybe it was time to see how far the Xceed offering has come in the last two years?

Comparison against Microsoft’s WPF DataGrid

So how does the Xceed DataGrid stack up against Microsoft’s? Here are some of the benefits that I’ve found so far:

  • Binding to SelectedItem works just fine.
  • ReadOnly properties support at Grid, Column and Row level.
  • CheckBox column allows simple styling whilst preserving ReadOnly value.
  • Automatically supports current selection and edit indicators in the row header.
  • The grid theme matches the OS theme out of the box. This is how it should be. Sure the grid can be custom styled to suit but it only makes sense that by default the grid should match the look and feel of the standard themed controls.
  • When auto-generating column headers it correctly uses any System.ComponentModel.DisplayName attributes that have been applied to the underlying class.
  • There are lots of options at grid and column level that determine how a cell should enter edit mode. This is very useful for columns such as CheckBox columns where requiring a click to enter edit mode can be highly annoying (since the user would expect the click to toggle the checkbox).

In short – most of the stumbling blocks that I’ve hit getting the Microsoft WPF DataGrid to do something pretty trivial “just work” with the Xceed grid. That’s not to say there was no learning curve at all – I had to wrap my head around setting the ItemsSource to a DataGridCollectionView rather than direct to a ObservableCollection<T> to get anything to display for a start. But this was glaringly obvious as soon as I looked at any of the samples and “how to” guides. The supplied documentation is OK, but what impressed me most was the feedback provided on Xceeds community forums. Some really good answers provided by the Xceed team in an ultra timely fashion.

So what could be improved with Xceed’s DataGrid? Well, its a little early for me to provide a comprehensive list here – but off the cuff I’d make the following suggestions:

  • An easier mechanism for custom sorting. Rather than having to specific custom IComparer implemenations often it is easier to refer to an unbound property that contains the raw data. Like Microsoft’s SortMemberPath property. Hmm… I wonder if you could use a generic SortComparer to provide the same functionality?
  • Smaller assembly size. I know these days 2.08Mb shouldn’t be an issue but for my current contract it is. We have a ClickOnce application that is deployed to machines in remote country areas. Many of these machines are still using dial-up! Adding another another 2Mb to our current 4.5Mb total is a decision not to be made too lightly. [We’ve already been burned with a ridiculously bloated NHibernate assembly (1.6Mb)]

As much as I’ve had fun with the Microsoft DataGrid I’m now considering making the Xceed’s my DataGrid of choice – certainly for my own projects. Expect to see a few more posts on the Xceed DataGrid coming up soon.

References

Microsoft WPF DataGrid – www.codeplex.com/wpf

Xceed WPF DataGrid – www.xceed.com

Problems binding to SelectedValue with Microsoft’s WPF DataGrid

I had been seeing some odd exceptions being thrown by the WPF DataGrid code when interacting with the “new row” place holder.

GridEditing - SelectedItem FormatException

I could identify that the error was occurring because I had data-bound to the SelectedItem property on the DataGrid like so:

<toolkit:DataGrid ItemsSource="{Binding Persons}" AutoGenerateColumns="False"
                  SelectedItem="{Binding SelectedPerson}"
IsSynchronizedWithCurrentItem="True">

Both the Persons collection and SelectedPerson are properties on my ViewModel (VM). Its possible to use CollectionViewSource.GetDefaultView(Persons).CurrentItem – but I find it useful to expose and bind a simple read/write property. I’ve used this previously for ListView and ListBox without a problem.

I spent some time debugging this right down through BindingExpression and DependencyObject.SetValue. As far as I can tell the exception is thrown because a ConvertBack method (on the default converter) fails when dealing with the MS.Internal.NamedObject that represents the NewItemPlaceholder. This instance is used to represent the blank “new row” if CanUserAddRows is set to True (and the collection supports it). In fact it appears as if the FormatException is actually being thrown within an exception handler whilst attempting to Trace the binding failure. Whoops!

Initially I tried simply putting an try/catch block around the DataGrid code shown above. However, the exception occurred under various conditions – focus on new row, begin edit on a new row and rollback on a new row. Not all of these could be easily caught because they would leave the grid in an invalid state. Eventually the answer (HACK) became obvious – to use a converter on the binding.

using System;
using System.Windows;
using System.Windows.Data;
namespace GridEditing.Converters
{
public class IgnoreNewItemPlaceHolderConverter : IValueConverter
{
private const string NewItemPlaceholderName = "{NewItemPlaceholder}";
public object Convert( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
{
return value;
}
public object ConvertBack( object value, Type targetType, object parameter, System.Globalization.CultureInfo culture )
{
if ( value != null && value.ToString() == NewItemPlaceholderName )
return DependencyProperty.UnsetValue;
return value;
}
}
}

All we are doing here is not binding when we encounter the “new row” instance. Notice the curly braces in the NewItemPlaceholder ToString() representation? This, I believe, is why it causes the FormatException since the ToString() is used to construct a formatString which is then passed to the TraceEvent method. However, because the curly braces aren’t escaped it expects a string token number, e.g. {0} as per string.Format().

Anyhow, using the converter above means that binding to DataGrid.SelectedValue works as expected with the “new row” place holder.

<Window.Resources>
<converters:IgnoreNewItemPlaceHolderConverter x:Key="ignoreNewItemPlaceHolderConverter"/>
</Window.Resources>
<toolkit:DataGrid ItemsSource="{Binding Persons}" AutoGenerateColumns="False"
SelectedItem="{Binding SelectedPerson,Converter={StaticResource ignoreNewItemPlaceHolderConverter
}}" IsSynchronizedWithCurrentItem="True">

WPF Charting

Yesterday, I received an e-mail from a developer who has been working on a set of WPF charting controls called amCharts for WPF. I was intrigued so I went over to the website to have a look around. Suffice to say I was easily impressed enough by the demos on the website to download myself a copy of the free (link ware) version.

The download, if anything, was even more impressive. The class library is concise, consistent and obviously well thought through (or evolved). Also, unlike other “bloatware” libraries this one weighs in at only just over 200Kb!

amCharts Torus

If you like what you see over at amCharts and are considering purchasing the Pro version then you may want to take advantage of a 50% discount code “spencen” when placing your order. Thanks go to Alan from amCharts for making me aware of his product and providing the discount which should be good till the end of May 2009.

Editing Indicator in DataGrid Row Header

Editable grids will quite commonly show an indicator in the row header area to indicate that a row is currently being edited. This is trivial to achieve using Microsoft’s WPF DataGrid.

 GridEditing - Edit Indicator

All that is needed is a DataTemplate assigned to the RowHeaderTemplate property of the DataGrid. The template simply shows or hides the editing image based upon whether the current row is being edited.

<SolidColorBrush x:Key="gridLineBrush" Color="#FFCDEFFE"/>
<DataTemplate x:Key="rowHeaderTemplate">
<StackPanel Orientation="Horizontal">
<Image x:Name="editImage" Source="Images/Edit.png" Width="16" Margin="1,0" Visibility="Hidden"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type toolkit:DataGridRow}},Path
=Item.IsEditing}"
Value
="True"> <Setter TargetName="editImage" Property="Visibility" Value="Visible"/> </DataTrigger> </DataTemplate.Triggers> </DataTemplate> <Style TargetType="{x:Type toolkit:DataGrid}"> <Setter Property="GridLinesVisibility" Value="All"/> <Setter Property="HorizontalGridLinesBrush" Value="{StaticResource gridLineBrush}"/> <Setter Property="VerticalGridLinesBrush" Value="{StaticResource gridLineBrush}"/> <Setter Property="RowHeaderTemplate" Value="{StaticResource rowHeaderTemplate}"/> <Style.Resources> <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/> <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black"/> <SolidColorBrush x:Key="{x:Static toolkit:DataGrid.FocusBorderBrushKey}" Color="{StaticResource gridLineBrush}"/> </Style.Resources> </Style>

There is an IsEditingRowItem property on the DataGrid but unfortunately its private. As it happens my base model class has an IsEditing property as part of its IEditableObject implementation. So I simply bind the visibility of the image to that.

#region IEditableObject Members
public void BeginEdit()
{
IsEditing = true;
}
public void CancelEdit()
{
IsEditing = false;
    // RollbackPropertyValues( _preEditValues );
}
public void EndEdit()
{
IsEditing = false;
}
#endregion

ReadOnly Rows and Cells in a DataGrid

A common requirement for a DataGrid control is to have cells, or entire rows and/or columns that are read-only, or in other words non-editable. My requirements for this are as follows:

  1. ReadOnly can be applied to the entire grid, a column, a row, or an individual cell.
  2. The cell(s) must not allow the cell value to be modified.
  3. The cell(s) must be highlighted in some manner (e.g. background colour) to indicate that they are different to the editable cells.
  4. ReadOnly columns require only single direction data-binding (i.e. Mode=OneWay to read-only properties).

GridEditing - ReadOnly Rows

The Microsoft WPF DataGrid meets these requirements via:

  • DataGrid.IsReadOnly property
<toolkit:DataGrid IsReadOnly="True"

  • DataColumn.IsReadOnly property
<toolkit:DataGridTextColumn Binding="{Binding FullName,Mode=OneWay}" IsReadOnly="True"/>

We can use a simple style targeting all DataGridCells to change the background colour.

<Style TargetType="{x:Type toolkit:DataGridCell}">
    <Style.Triggers>
        <Trigger Property="IsReadOnly" Value="True">
<Setter Property="Background" Value="LightGray"/>
</Trigger>
</Style.Triggers>
</Style>

So the only thing that’s really missing here is the ability to mark an entire row as read-only. In my experience this is a common requirement – we have a list of records displayed in the grid some of which are locked/completed/secured, whilst others can be edited.

One solution is to override the OnBeginningEdit method of the DataGrid. The following example assumes that I have an attached property ControlSupport.IsReadOnly.

protected override void OnBeginningEdit( DataGridBeginningEditEventArgs e )
{
base.OnBeginningEdit( e );
bool isReadOnlyRow = ControlSupport.GetIsReadOnly( e.Row );
if ( isReadOnlyRow )
e.Cancel = true;
}

However, since I’ve got used to “tweaking” some of the DataGrid code I decided to instead to simply add an IsReadOnly property to the DataGridRow class.

public bool IsReadOnly
{
get { return (bool) GetValue( IsReadOnlyProperty ); }
set { SetValue( IsReadOnlyProperty, value ); }
}
// Using a DependencyProperty as the backing store for IsReadOnly.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register( "IsReadOnly", typeof( bool ), typeof( DataGridRow ), 
new FrameworkPropertyMetadata( false, OnNotifyRowAndCellsPropertyChanged ) );

private static void OnNotifyRowAndCellsPropertyChanged( DependencyObject d, 
DependencyPropertyChangedEventArgs e ) { ( d as DataGridRow ).NotifyPropertyChanged( d, e, NotificationTarget.Rows | NotificationTarget.Cells ); }

Then I just needed to make sure that the read-only property DataGridCell.IsReadOnly would correctly return the right value when its row was marked as read-only.

private static object OnCoerceIsReadOnly(DependencyObject d, object baseValue)
{
var cell = d as DataGridCell;
var column = cell.Column;
var row = cell.RowOwner;
var dataGrid = cell.DataGridOwner;
return DataGridHelper.GetCoercedTransferPropertyValue(
cell,
baseValue,
IsReadOnlyProperty,
row,
DataGridRow.IsReadOnlyProperty,
column,
DataGridColumn.IsReadOnlyProperty,
dataGrid,
DataGrid.IsReadOnlyProperty);
}

There’s a fairly complex series of notification propagation calls going on within the DataGrid classes. For this solution to work it requires that the DataGridRow.IsReadOnly property changing flows down and causes the read-only DataGridCell.IsReadOnly property to be re-evaulated (via the coerce method above). I had to add another GetCoervedTransferPropertyValue method that took another pair of object/property parameters, and also tweaked the DataGridCell.NotifyPropertyChanged as follows:

else if (e.Property == DataGrid.IsReadOnlyProperty || 
e.Property == DataGridColumn.IsReadOnlyProperty ||
e.Property == DataGridRow.IsReadOnlyProperty ||
e.Property == IsReadOnlyProperty) { DataGridHelper.TransferProperty(this, IsReadOnlyProperty); }

Now in my XAML I can do the following (assuming I have an IsReadOnly property on my business objects):

<Style TargetType="{x:Type toolkit:DataGridRow}">
<Setter Property="IsReadOnly" Value="{Binding IsReadOnly}"/>
</Style>

When is a WPF DataGrid read-only CheckBox not read-only?

I found the answer to this riddle when I decided to style the DataGridCheckBoxColumn of Microsoft’s WPF DataGrid. By default the CheckBox displayed by the column template is not centered horizontally or vertically. I thought this looked at little tacky so I decided to apply a custom Style to the ElementStyle (and EditElementStyle) property.

<Style  x:Key="CheckBoxStyle" TargetType="{x:Type CheckBox}" BasedOn="{StaticResource {x:Type CheckBox}}">
    <Setter Property="HorizontalAlignment" Value="Center"/> 
<Setter Property="Margin" Value="0,2,0,0"/> </Style>
<toolkit:DataGridCheckBoxColumn Binding="{Binding IsPensioner}" 
Header
="Pensioner?"
ElementStyle="{StaticResource CheckBoxStyle}"
EditingElementStyle="{StaticResource CheckBoxStyle
}"/> <toolkit:DataGridCheckBoxColumn Binding="{Binding IsEditing,Mode=OneWay}"
Header="Is Editing"
ElementStyle="{StaticResource CheckBoxStyle}"

IsReadOnly="True"
/>

What I later discovered is that applying this style has somehow made my read-only CheckBox editable. But only via mouse clicks! Using the keyboard to focus to the cell and pressing Space didn’t cause the CheckBox to toggle, but left clicking on the CheckBox did. What’s going on?

My guess is that the default style applied to the DataGridCheckBoxColumn’s CheckBox in non-edit mode (ElementStyle) sets the IsHitTestVisible property to False to disable clicking on the cell. The keyboard events are swallowed by the DataGrid using Preview events – so no styling is required to prevent keyboard access.

The rule would therefore appear to be that if you set ElementStyle on the DataGridCheckBoxColumn you must include IsHitTestVisible=”False” to prevent it from “seeming” that the control allows edits.

<Style  x:Key="CheckBoxStyle" TargetType="{x:Type CheckBox}" BasedOn="{StaticResource {x:Type CheckBox}}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,2,0,0"/>
</Style>
<Style  x:Key="ReadOnlyCheckBoxStyle" TargetType="{x:Type CheckBox}" BasedOn="{StaticResource CheckBoxStyle}">
<Setter Property="IsHitTestVisible" Value="False"/>
</Style>
<toolkit:DataGridCheckBoxColumn Binding="{Binding IsPensioner}"
Header="Pensioner?"
ElementStyle="{StaticResource ReadOnlyCheckBoxStyle}"
EditingElementStyle="{StaticResource CheckBoxStyle}"/>
<toolkit:DataGridCheckBoxColumn Binding="{Binding IsEditing,Mode=OneWay}"
Header="Is Editing"
ElementStyle="{StaticResource ReadOnlyCheckBoxStyle}"
IsReadOnly="True"/>

Binding an Enum Property to a ComboBox using customized text

The problem

I want to data bind a property on my Model to a ComboBox that allows selection from a list of Enum values. For example, my Person class has a HighestEducationLevel property of type EducationLevel.

 GridEditing

The second part of this problem is that I want to provide an optional customized text description for each of my enumeration values. For example, the enumeration value EducationLevel.PreSchool should be displayed as “Pre-school” in the ComboBox.

GridEditing - TypeConverter ComboBox

Solution 1 – Bind to Enum.GetValues()

There are plenty of blog posts and forum answers that show the following technique that can be accomplished in XAML alone. I first saw this on a post by Karl Schifflet.

You define a static resource as an ObjectDataProvider that simply uses a method call to get an array of the enumerated values.

<ObjectDataProvider x:Key="EducationLevelList" MethodName="GetValues" ObjectType="{x:Type local:EducationLevel}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:EducationLevel"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

This can then be data bound as the ItemsSource for a ComboBox – in my case its a ComboBox column of a WPF DataGrid.

<toolkit:DataGridComboBoxColumn Header="Education Level"
SelectedValueBinding="{Binding HighestEducationLevel}"
ItemsSource="{Binding Source={StaticResource EducationLevelList}}"/>

Solution 2 – Bind to Custom Method

This technique can be taken a step further by providing your own method to be used by the ObjectDataProvider rather than relying on Enum.GetValues(Type). This then provides the opportunity to provide custom sorting, filtering and text for the list of enumeration values.

One way to achieve the custom text values is to have the method return a list of “wrapper” objects that provide access to both the underlying enumeration value and the custom text.

public class EnumMapper
{
public EnumMapper( object enumValue, string enumDescription )
{
Enum = enumValue;
Description = enumDescription;
}
public object Enum { get; private set; }
public string Description { get; private set; }
}

The enum type can then have each of its members (fields) that require a custom text decorated with a custom attribute as follows.

public enum EducationLevel
{
None,
[EnumDisplayName("Pre-school")]
PreSchool,
[EnumDisplayName( "Junior school" )]
JuniorSchool,
[EnumDisplayName( "Senior School" )]
SeniorSchool,
Graduate,
[EnumDisplayName( "Post Graduate" )]
PostGraduate,
Professor
}

The new attribute class itself is trivial…

[AttributeUsage(AttributeTargets.Field, AllowMultiple=false)]
public class EnumDisplayNameAttribute : Attribute
{
public EnumDisplayNameAttribute( string displayName )
{
DisplayName = displayName;
}
public string DisplayName { get; set; }
}

The custom method to generate the list then becomes…

public static IList<EnumMapper> GetEnumDescriptions( Type enumType )
{
if ( !enumType.IsEnum )
throw new ArgumentException( "This method can only be called for enum types." );
var list = new List<EnumMapper>();
foreach ( var enumValue in Enum.GetValues( enumType ) )
list.Add( new EnumMapper( enumValue, enumType.GetDisplayName( enumValue ) ) );
return list;
}
public static string GetDisplayName( this Type enumType, object enumValue )
{
if ( !enumType.IsEnum )
throw new ArgumentException( "This method can only be called for enum types." );
var displayNameAttribute = enumType.GetField( enumValue.ToString() )
.GetCustomAttributes( typeof( EnumDisplayNameAttribute ), false )
.FirstOrDefault() as EnumDisplayNameAttribute; if ( displayNameAttribute != null ) return displayNameAttribute.DisplayName; return Enum.GetName( enumType, enumValue ); }

The XAML has to change a little. First the ObjectDataProvider must use the new method, and secondly because the ItemsSource is now a list of EnumMapper instances we must provide DisplayMemberPath and SelectedValuePath properties for the ComboBox.

<ObjectDataProvider x:Key="EducationLevelList" MethodName="GetEnumDescriptions" ObjectType="{x:Type local:BindingSupport}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:EducationList"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

<toolkit:DataGridComboBoxColumn Header="Highest Education Level"
                                SelectedValueBinding="{Binding HighestEducationLevel}"
ItemsSource="{Binding Source={StaticResource EducationLevelList}}"
DisplayMemberPath="Description"
SelectedValuePath="Enum"/>

Solution 3 – Use a customized TypeConverter

Solution 2 provides quite a lot of flexibility but it does mean a couple of extra property setters are required in the XAML (though I guess these could go into a Style). The third approach is to use a TypeConverter to “magically” convert enum values to strings. The major benefit of this approach is that it will work not just for ComboBoxs but anywhere an enum is bound to text property, e.g. in a TextBlock or TextBox.

First we declare a new TypeConverter that has some special processing that allows its to generate the custom text. All enums by default use the EnumConverter anyway – we are just providing an extra lookup to check for a custom attribute.

public class EnumTypeConverter : EnumConverter
{
public EnumTypeConverter( Type enumType ) : base( enumType ) { }
public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType )
{
if ( destinationType == typeof(string) && value != null )
{
var enumType = value.GetType();
if ( enumType.IsEnum )
return GetDisplayName( value );
}
return base.ConvertTo( context, culture, value, destinationType );
}
    private string GetDisplayName( object enumValue )
{
var displayNameAttribute = EnumType.GetField( enumValue.ToString() )
.GetCustomAttributes( typeof( EnumDisplayNameAttribute ), false )
.FirstOrDefault() as EnumDisplayNameAttribute; if ( displayNameAttribute != null ) return displayNameAttribute.DisplayName; return Enum.GetName( EnumType, enumValue ); } }

The next step is to make sure that all our enums use the new TypeConverter. This is done by decorating the enum with a TypeConverter attribute.

[TypeConverter(typeof(EnumTypeConverter))]
public enum EducationLevel
{
None,
[EnumDisplayName("Pre-school")]
PreSchool,
[EnumDisplayName( "Junior school" )]
JuniorSchool,
[EnumDisplayName( "Senior School" )]
SeniorSchool,
Graduate,
[EnumDisplayName( "Post Graduate" )]
PostGraduate,
Professor
}

Now we can change our XAML back to its original simplified form (as per Solution 1) and yet we still get our customized text appearing.

<ObjectDataProvider x:Key="EducationLevelList" MethodName="GetValues" ObjectType="{x:Type local:EducationLevel}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:EducationLevel"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>

<toolkit:DataGridComboBoxColumn Header="Education Level"

                               
SelectedValueBinding="{Binding HighestEducationLevel
}"

                               
ItemsSource="{Binding Source={StaticResource EducationLevelList}}"/>

Other Considerations

In the example here I’ve just used hard-coded strings for the EnumDisplayName attributes. However, there is no reason these couldn’t be resource IDs or the like and the GetDisplayName method changed accordingly.

Also, if we want the TypeConverter to have the ability to ConvertFrom the customized text then we need to do a little more work. This is handy in the scenarios where the user may be able to type (as opposed to select from a list) the enum values. So the EnumTypeConverter changes to the following.

public class EnumTypeConverter : EnumConverter
{
private IEnumerable<EnumMapper> _mappings;
public EnumTypeConverter( Type enumType ) : base( enumType )
{
_mappings = from object enumValue in Enum.GetValues(enumType)
select new EnumMapper( enumValue, GetDisplayName(enumValue) );
}
public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType )
{
if ( destinationType == typeof(string) && value != null )
{
var enumType = value.GetType();
if ( enumType.IsEnum )
return GetDisplayName( value );
}
return base.ConvertTo( context, culture, value, destinationType );
}
public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value )
{
if ( value is string )
{
var match = _mappings.FirstOrDefault( mapping => string.Compare( mapping.Description, (string) value, true, culture ) == 0 );
if ( match != null )
return match.Enum;
}
return base.ConvertFrom( context, culture, value );
}

private string
GetDisplayName( object enumValue )
{
var displayNameAttribute = EnumType.GetField( enumValue.ToString() )
.GetCustomAttributes( typeof( EnumDisplayNameAttribute ), false )
.FirstOrDefault() as EnumDisplayNameAttribute;

if ( displayNameAttribute != null )
return displayNameAttribute.DisplayName;

return Enum.GetName( EnumType, enumValue );
} }

WPF DataGrid Tips

I’ve been struggling at work to use Microsoft’s WPF DataGrid (from their WPF Toolkit) to fulfil a fairly basic set of requirement. The following is simply a list of lessons learned.

  1. Remember to set the SortMemberPath for all DataGridTemplateColumns. Otherwise the column header is “inactive” and doesn’t allow sorting regardless of setting CanUserSort.
  2. Remember to set the ClipboardContentBinding for all DataGridTemplateColumns. Otherwise the column data will be copied to the clipboard as an empty string.

    <toolkit:DataGridTemplateColumn Header="Date of Birth"
                                                    
    SortMemberPath="DateOfBirth"
                                                    
    ClipboardContentBinding="{Binding DateOfBirth
    }">
          <
    toolkit:DataGridTemplateColumn.CellTemplate
    >
              <
    DataTemplate
    >
                  <
    TextBlock Text="{Binding DateOfBirth,Mode=OneWay,StringFormat=d}" Margin
    ="2,0,2,2"/>
              </
    DataTemplate
    >                    
          </
    toolkit:DataGridTemplateColumn.CellTemplate
    >
          <
    toolkit:DataGridTemplateColumn.CellEditingTemplate
    >
              <
    DataTemplate
    >
                  <
    toolkit:DatePicker SelectedDate="{Binding DateOfBirth,Mode
    =TwoWay}"/>
              </
    DataTemplate
    >
          </
    toolkit:DataGridTemplateColumn.CellEditingTemplate
    >
      </
    toolkit:DataGridTemplateColumn>

  3. As with other ItemsControls you will most likely want to redefine HighlightBrush and HighlightTextBrush to avoid the high contrast (and very old fashioned) row selection colours.
  4. Use Styles embedded in the DataGrid’s style’s Resources collection to override attributes of controls that will be used for editing. For example, setting the BorderThickness=”0” and Padding=”0” on the DatePicker.
  5. Watch out for implicit Styles that effect Button. The grid is made up of lots of buttons (grid, row and column headers) so having an implicit style define MinWidth or Margins can lead to some unsightly grid layouts.
  6. DataGridCheckBoxColumn doesn’t centre vertically and its default margin doesn’t seem to match the DataGridTextColumn. Setting the Margin for DataGridCheckBoxColumns to  “2”, or at least “0,2,0,0” seems to do the trick.
  7. Setting an EditingElementStyle for a DataGridTextBoxColumn to CharacterCasing=”Upper” is not honoured if the keystroke is used to enter edit mode. Requires a simple fix to the PrepareCellForEdit method in DataGridTextBoxColumn. Refer http://www.codeplex.com/wpf/Thread/View.aspx?ThreadId=36985.

                        // If text input started the edit, then replace the text with what was typed.
                    string inputText;
                    switch (textBox.CharacterCasing)
                    {
                        case CharacterCasing.Upper:
                            inputText = textArgs.Text.ToUpper();
                            b
    reak
    ;
                        case CharacterCasing.Lower:
                            inputText = textArgs.Text.ToLower();
                            break;
                        default:
                            inputText = textArgs.Text;
                            break;
                    }
                    textBox.Text = inputText;

  8. Placing a DatePicker in a DataGridTemplateColumn doesn’t cause the DatePIcker to automatically gain focus when pressing F2 (enter edit). To get around that I overrode PrepareCellForEdit in DataGridTemplateColumn as follows:

        protected override object PrepareCellForEdit(FrameworkElement editingElement,
                                                                      RoutedEventArgs editingEventArgs)
        {
            editingElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
            return base.PrepareCellForEdit(editingElement, editingEventArgs);
        }

  9. The DatePicker uses the Enter key to highlight the date within its embedded TextBox. This is annoying when used in a DataGridTemplateColumn since the standard behaviour for the Enter key is to commit edits and move down a row. Commenting out this functionality in the DatePicker (within ProcessDatePickerKey) does the trick. 

            private bool ProcessDatePickerKey(KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.System:
                    {
                        switch (e.SystemKey)
                        {
                            case Key.Down: { if ((Keyboard.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt)
                                {
                                    TogglePopUp();
                                    return true;
                                }
                                break;
                            }
                        }
                        break;
                    }
                  
    // Removing this functionality makes the control work better in the grid – ENTER goes to next row.
                  
    //case Key.Enter:
                  
    //{
                  
    //    SetSelectedDate();
                  
    //    return true;
                  
    //}
                }
                return false;
            }

Othe
r Resources