[UPDATE: This solution has been superceded by a much better implementation in this later post.]
I recently downloaded the WPF Toolkit for which Microsoft posted the first DataGrid CTP. That DataGrid is their attempt at responding to what is likely one of the most requested missing features in WPF. Many people have suggested that WPF doesn’t need a DataGrid and may be better off not to provide one, but I and others disagree – we do need a DataGrid, but it must be lookless! Anyway, I’m getting off topic.
While I really appreciate the work that went into building such a flexible and feature-packed control, I was quickly disheartened to learn that it has no mechanism built in to handle the scenario where column data (text) gets cut off due to size constraints. Actually there is one means, wrapping the text, but for the task I’m currently working on wrapping is really not an acceptable approach.
The obvious compromise is to display a ToolTip containing the full text when the mouse hovers over it. Easy enough to do by simply using a Style to add a ToolTip whose text is bound to the control’s content (fairly well documented elsewhere.)
There is one big problem that crops up with the simple hover invoked ToolTip, though. When the text is not being trimmed, a popup is still displayed which contains no additional information and obscures whatever data happens to be underneath it. At best it’s annoying for the user and worse could get in the way enough to slow them down.
My first thought was that surely there’s a property on TextBlock that I can hook up a property trigger to and only enable the ToolTip when that property is set. Something like, “IsTextTrimmed” or “HasParagraphEllipsis”, etc. Unfortunately, no such luck. Even worse, because TextBlock is a “lookful” control I can’t just pick apart its ControlTemplate to get after the information I need. My last blog post described that problem in detail and the general approach taken to solve it.
In that post I presented a new custom control called EnhancedTextBlock which derives from TextBlock and uses reflection to provide an IsTextTrimmed read-only dependency property. Simply attaching a style trigger to that property offers the ability to easily adjust behavior based on whether or not the text is being trimmed. Here is a simple example of using that property to conditionally display a ToolTip:
1 <Window.Resources>
2 <Style TargetType=”local:EnhancedTextBlock”>
3 <Style.Triggers>
4 <Trigger Property=”IsTextTrimmed” Value=”True”>
5 <Setter Property=”ToolTip” Value=”{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}” />
6 </Trigger>
7 </Style.Triggers>
8
9 <Style.Setters>
10 <Setter Property=”TextWrapping” Value=”NoWrap” />
11 <Setter Property=”TextTrimming” Value=”WordEllipsis” />
12 </Style.Setters>
13 </Style>
14 </Window.Resources>
15
16 <StackPanel Margin=”40,0,40,0″>
17 <local:EnhancedTextBlock Text=”Short Text” />
18 <local:EnhancedTextBlock Text=”Slightly Longer Text” />
19 </StackPanel>
Which produces something that works approximately like so:
Now that we’ve resolved conditional ToolTips in the general case, let’s get back to the WPF Toolkit’s DataGrid specifically.
First thing to realize is that we’re not going to be able to use the stock DataGridBoundColumn derived DataGridTextColumn because that control’s data template makes use of WPF’s TextBlock. Actually we could override the DataGridTextColumn’s ControlTemplate, but it turns out that’s not necessary! Vincent Sibal has a post describing a type of column control provided with the DataGrid called DataGridTemplateColumn. The sole purpose of that type is to allow us to provide our own custom data templates for both the view and edit case of any given column.
All that’s required is to first disable auto column generation (yes, unfortunately that’s one of the drawbacks of this approach) and second, add a column with a CellTemplate that includes our EnhancedTextBlock control:
35 <dg:DataGrid AutoGenerateColumns=”False” ItemsSource=”{Binding}”>
36 <dg:DataGrid.Columns>
37 <dg:DataGridTemplateColumn Header=”Various Text”>
38 <dg:DataGridTemplateColumn.CellTemplate>
39 <DataTemplate>
40 <local:EnhancedTextBlock Text=”{Binding}” />
41 </DataTemplate>
42 </dg:DataGridTemplateColumn.CellTemplate>
43 </dg:DataGridTemplateColumn>
44 </dg:DataGrid.Columns>
45 </dg:DataGrid>
Then spice it up with our style setters and triggers from above and we end up with this lovely bit of XAML:
1 <Window
2 x:Class=”WpfApplication1.Window1″
3 xmlns:local=”clr-namespace:WpfApplication1″
4 xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
5 xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
6 xmlns:dg=”http://schemas.microsoft.com/wpf/2008/toolkit”
7 xmlns:sys=”clr-namespace:System;assembly=mscorlib”
8 xmlns:collections=”clr-namespace:System.Collections;assembly=mscorlib”
9 Width=”600″
10 Height=”125″>
11
12 <Window.DataContext>
13 <collections:ArrayList>
14 <sys:String>Short Text</sys:String>
15 <sys:String>Slightly Longer Text</sys:String>
16 <sys:String>Text that is so long it makes you wonder if a non-wrapping datagrid cell is really the best way to present it</sys:String>
17 </collections:ArrayList>
18 </Window.DataContext>
19
20 <Window.Resources>
21 <Style TargetType=”local:EnhancedTextBlock”>
22 <Style.Triggers>
23 <Trigger Property=”IsTextTrimmed” Value=”True”>
24 <Setter Property=”ToolTip” Value=”{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}” />
25 </Trigger>
26 </Style.Triggers>
27
28 <Style.Setters>
29 <Setter Property=”TextWrapping” Value=”NoWrap” />
30 <Setter Property=”TextTrimming” Value=”WordEllipsis” />
31 </Style.Setters>
32 </Style>
33 </Window.Resources>
34
35 <dg:DataGrid AutoGenerateColumns=”False” ItemsSource=”{Binding}”>
36 <dg:DataGrid.Columns>
37 <dg:DataGridTemplateColumn Header=”Various Text”>
38 <dg:DataGridTemplateColumn.CellTemplate>
39 <DataTemplate>
40 <local:EnhancedTextBlock Text=”{Binding}” />
41 </DataTemplate>
42 </dg:DataGridTemplateColumn.CellTemplate>
43 </dg:DataGridTemplateColumn>
44 </dg:DataGrid.Columns>
45 </dg:DataGrid>
46 </Window>
And as always, I like to demonstrate my flare with visual aids:
Ultimately, it’s not a very tidy solution because the original trick to add an IsTextTrimmed property required creating a custom control rather than just using the built-in TextBlock and it makes use of reflection which means the EnhancedTextBlock can’t be used in partial-trust applications. Those issues aside, I have at least shown that if you’re willing to accept a few compromises and do some heavy lifting, deep customization of “lookful” WPF controls can be achieved. I’ve also shown that making use of custom controls in the WPF Toolkit DataGrid is really quite straightforward.