Friday, June 26, 2009

How to display tool tips in WPF DataGrid text column

Like many people before me, while using the DataGrid from the WPFtoolkit, I recently had a requirement to display text nicer than the default. First, the default is simply to truncate the text like so:

I needed the truncated text to have ellipses after it and have a ToolTip showing the entire text. Something like this:

I did a lot of searching the web and didn't find anything straight-forward and elegant. I found things that intimated using styles in XAML might work, although nothing directly addressing what I wanted to do. Furthermore, I found others saying XAML styles won't work at all for cell level changes along with some ugly hacks where, inside the LoadingRow method, they walked back up the VisualTree to change the cell style in code. My search was confounded by the fact that there are a number of different DataGrid components, three by Microsoft (WinForms, WPF and Silverlight) and several by other 3rd party vendors. All have issues doing this very basic thing and all with different workarounds.

After spending the better part of a day researching and testing various possible solutions, I found a comment on a Silverlight forum with the idea of using a DataGridTemplateColumn descendant to handle the new needs. I started to adapt the example code from Silverlight to WPF and thought, "There must be an easier way." Since the existing DataGridTextColumn already did much of what was in this example, I went off to look at its source code. While browsing through it, I found a protected virtual method called GenerateElement that apparently is a factory for the cells' display. On a hunch, I created a descendant of this column type and overrode this one method, changing the properties of the returned value as needed.

Amazingly, it worked the first time. So, to add my own workaround to the mix that's found on the web, here is the class I created:
public class DataGridToolTipTextColumn : DataGridTextColumn
{
    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        var result = (TextBlock)base.GenerateElement(cell, dataItem);
        result.TextTrimming = TextTrimming.CharacterEllipsis;
        ApplyBinding(result, FrameworkElement.ToolTipProperty);
        return result;
    }

    // Copied from DataGridTextColumn because it's not protected there either. Seems like it should be.
    internal void ApplyBinding(DependencyObject target, DependencyProperty property)
    {
        var binding = Binding;
        if (binding == null)
            BindingOperations.ClearBinding(target, property);
        else
            BindingOperations.SetBinding(target, property, binding);
    }
}
The GenerateElement method simply calls the base class and typecasts the return value to a TextBlock. For the moment this is safe since the base class has the type hard coded. The TextTrimming property is then set for CharacterEllipsis and the ToolTip property is bound to the same value as the Text property. This class is twice as long as it needs to be because the ApplyBinding method that exists in the base class is marked internal rather than protected, so I simply made a copy of it for use here.

In my usage, this DataGrid is only to display information; editing is turned off. I haven't checked to see if this would cause any issues in edit mode.

Related material

In all this searching, I did find a couple good resources:
  • Samuel Moura has a 4 part series doing some pretty intensive DataGrid styling and templating: Introduction, Custom Styling, Playing with Columns and Cells and TemplateColumns and Row Grouping.
  • The Tranxition Developer Blog has a good article on adding dependency properties to the TextBlock type. Unfortunately, this doesn't seem to work for TextBlocks that are in another control's VisualTree.
  • This Silverlight thread is what gave me the idea for creating a descendant class.
  • I ran across this great little utility called Snoop to inspect the VisualTree of a running WPF application. I think I'd heard of it before in passing but had never looked into it. I don't know how many times I've written throw-away VisualTree walkers to dump information to the console. Now I'll never have to do that again.

5 comments:

TB86 said...

I struggled with this too, and thought there must be a way to do it with a simple databinding. After some trial and error, I finally got it. Your comment system gets confused with XAML, thinking it is HTML tags, so I have replaced '<' and '>' with '~':

~Style x:Key="{x:Type dg:DataGridCell}" TargetType="{x:Type dg:DataGridCell}"~
~Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=Content.Text}" /~"

I am not sure how this will work for non-Text cells, such as Comboboxes, but it works for grids that are made up only of simple Text cells.

Harley Pebley said...

Thanks TB86! I'll have to give that a try.

Anonymous said...

Thanks it was most helpful..

Unknown said...

@Harley

Hi, I am fighting with a related topic: I worte a usercontrol consisting of atextbox and a button. This usercontrol works fine when used as a "normal" control opposed to the usage in a datagrid.
I have a new class derived from DataGridTextColumn and overwrite both GenerateElement and GenerateEditingElement, and all works but the changes I make in the textbox are not going back to the bound property. All bindings are TwoWay. I am not quite sure I f I do everything right in the GenerateEditingElement event, here is my code:
Protected Overloads Overrides Function GenerateEditingElement(ByVal oCell As DataGridCell, ByVal oDataItem As Object) As FrameworkElement

Dim oUc As New UcSelect
Dim oBinding As Binding = CType(Me.Binding, Binding)
oUc.SetBinding(UcSelect.UcTextProperty, oBinding)
Return oUc

End Function

Any help would be appreciated
Kind regards
Klaus

Harley Pebley said...

Klaus,

I don't have a ready answer on the binding question. I'll have to dig into it a bit and reply later in the week.

Harley