Customizing “lookful” WPF controls – Take 2

Last week I posted a technique for customizing controls in WPF that don’t offer a replaceable ControlTemplate – I described them as “lookful” controls because their behavior and appearance are tied in with their functionality. My approach involved creating a custom control that derives from the one being targeted. That solution has some significant downsides, though, including that if the control is part of another (composition) then you have to modify the parent control to tell it to use your new custom control as a child – it works in most cases, it’s just not ideal. I figured someone might call me out on it eventually and, sure enough, the day I posted it fellow WPF’er “John” ruminated in a comment about using attached properties to accomplish the task instead. That got my wheels turning and I decided to put together a solution that, frankly, doesn’t stink

The control I covered was the TextBlock, and the customizations I made involved adding a dependency property called IsTextTrimmed to my derived control which indicates if the text is being trimmed (clipped with ellipses appended). This time I’ll tackle that same issue, but do it in a way that doesn’t require custom controls, is easy to use, and is flexible enough to support any number of additional customizations.

At the end of this post there is a link to a solution which contains the TextBlockService assembly and a Sample WpfApplication that demonstrates its usage. As with everything we post on The Tranxition Developer Blog, feel free to use it however you wish, ask questions, or suggest alternatives. Rather than cover how all of this works (there are a lot of WPF concepts involved), I’m just going to lay out the key bits and then explain a little about why I made certain design and implementation decisions.

All of the functionality resides in the TextBlockService assembly with most of the logic in TextBlockService.cs. That file holds a single public class called TextBlockService which serves as an attached property provider for two properties, the critical one being TextBlockService.IsTextTrimmed.

The attached properties mechanism is one of the coolest (and least intuitive) things about WPF. In this case, I’m bucking the norm which involves backing all attached properties with corresponding dependency properties. As a result, my class doesn’t need to implement any dependency property requirements and also need not derive from the DependencyObject class. It seems odd at first, but the instance values for these properties are maintained entirely by the framework, with no help from my class.

  1 public static readonly DependencyPropertyKey IsTextTrimmedKey = DependencyProperty.RegisterAttachedReadOnly(

  2     “IsTextTrimmed”,

  3     typeof( bool ),

  4     typeof( TextBlockService ),

  5     new PropertyMetadata( false ) );

That code fragment is what registers my attached property as “TextBlockService.IsTextTrimmed” in XAML, sets it type to bool, and sets the default value to false. Since this property can’t logically be “set” by a consumer, it’s registered as read-only which means instead of returning a DependencyProperty object, it returns a DependencyPropertyKey. That key is used to retrieve a special type of DependencyProperty object:

  1 public static readonly DependencyProperty IsTextTrimmedProperty = IsTextTrimmedKey.DependencyProperty;

Which is used when retrieving the current value:

  1 [AttachedPropertyBrowsableForType( typeof( TextBlock ) )]

  2 public static Boolean GetIsTextTrimmed( TextBlock target )

  3 {

  4     return (Boolean) target.GetValue( IsTextTrimmedProperty );

  5 }

This property (IsTextTrimmed) only makes sense when attached directly to a TextBlock so I used AttachedPropertyBrowsableForType to restrict designers to only displaying it as an attribute on TextBlock elements. This doesn’t actually prevent users from trying to attach it to other element types, but it won’t show as an available option in the IDE. In addition I made the parameter type TextBlock on GetIsTextTrimmed where normally for attached properties this will be something much more generic like DependencyObject or UIElement, etc.

With that bit of code we’ve created a full fledged read-only property that can be attached to any TextBlock control. However, we lack any means to actually set the value (besides its default).

I simply adapted a function that calculates the current trimmed state for any given TextBlock by adding a bit of logic at the top to retrieve the present IsTextTrimmed value if the TextBlock is not in a state where it can be evaluated:

private static bool CalculateIsTextTrimmed( TextBlock textBlock )
    if ( !textBlock.IsArrangeValid )
        return GetIsTextTrimmed( textBlock );

    Typeface typeface = new Typeface(
        textBlock.FontStretch );

    // FormattedText is used to measure the whole width of the text held up by TextBlock container
    FormattedText formattedText = new FormattedText(
        textBlock.Foreground );

    formattedText.MaxTextWidth = textBlock.ActualWidth;

    // When the maximum text width of the FormattedText instance is set to the actual
    // width of the textBlock, if the textBlock is being trimmed to fit then the formatted
    // text will report a larger height than the textBlock. Should work whether the
    // textBlock is single or multi-line.
    return ( formattedText.Height > textBlock.ActualHeight );
(Note that I’m using a version of the alternate trimmed state algorithm with input from Jean-Marie Pirelli that was referred to by John in the comment on my original blog entry. This algorithm may have issues when certain transforms are applied, but it has much better maintainability than my original reflection-based approach.)

Now that the state can be determined, we need something to trigger that determination. Meet EventManager.RegisterClassHandler:

  1 static TextBlockService()

  2 {

  3     // Register for the SizeChanged event on all TextBlocks, even if the event was handled.

  4     EventManager.RegisterClassHandler(

  5         typeof( TextBlock ),

  6         FrameworkElement.SizeChangedEvent,

  7         new SizeChangedEventHandler( OnTextBlockSizeChanged ),

  8         true );

  9 }

This code in the service class’s static constructor causes an event handler for the SizeChangedEvent (from FrameworkElement) to be registered for every instance of the TextBlock class. Additionally it allows me to specify that the event handler should be called even if the event has already been marked “handled” by a previous handler. I’m greedy like that. The actual event handler is fairly straight forward, just checking to make sure trimming is even possible before bothering to evaluate it:

  1 public static void OnTextBlockSizeChanged( object sender, SizeChangedEventArgs e )

  2 {

  3     var textBlock = sender as TextBlock;

  4     if ( null == textBlock )

  5     {

  6         return;

  7     }


  9     if ( TextTrimming.None == textBlock.TextTrimming )

 10     {

 11         SetIsTextTrimmed( textBlock, false );

 12     }

 13     else

 14     {

 15         SetIsTextTrimmed( textBlock, CalculateIsTextTrimmed( textBlock ) );

 16     }

 17 }

That handler uses an additional function called SetIsTextTrimmed. In a read-write attached property, that function is required to be present as that’s what WPF uses to write a new property value. In our case the property is read-only, so I still went ahead and created the function but marked it private:

  1 private static void SetIsTextTrimmed( TextBlock target, Boolean value )

  2 {

  3     target.SetValue( IsTextTrimmedKey, value );

  4 }

(This uses the DependencyPropertyKey instance rather than the DependencyProperty object because SetValue has 2 overloads, one that takes a DependencyProperty (for read-write properties) and one that takes a DependencyPropertyKey (for read-only properties). GetValue doesn’t care about writeability, so it only has a single version which expects a DependencyProperty instance.)

That actually covers the entirety of adding a (read-only) attached property to every TextBlock with the only usage requirements being (obviously) a reference to the TextBlockService assembly and (to use it from XAML) importing TextBlockService as a clr-namespace:


Once that’s imported, making use of the property is super easy (assuming you’ve figured out property triggers!)

  1 <TextBlock Text=”Some not-so-random text” TextTrimming=”WordEllipsis” TextWrapping=”NoWrap”>

  2     <TextBlock.Resources>

  3         <Style TargetType=”TextBlock”>

  4             <Style.Triggers>

  5                 <Trigger Property=”tbs:TextBlockService.IsTextTrimmed” Value=”True”>

  6                     <Setter Property=”Background” Value=”Pink” />

  7                 </Trigger>

  8             </Style.Triggers>

  9         </Style>

 10     </TextBlock.Resources>

 11 </TextBlock>

And as always, the pudding:

image image

Don’t know about you, but I find that incredibly cool. We were able to develop and build an attached property in a class in a separate assembly and all that was needed to make use of it from a consuming application is to import the class’ CLR namespace!

Now that we’ve built out our IsTextTrimmed attached property and shown how easy it is to use, let’s take it a step further and add an some additional custom TextBlock behavior into TextBlockService. I wanted to add an attached property that, when set to true for any given TextBlock, automatically enables a ToolTip that shows the hidden text whenever the text is being trimmed. I called that property TextBlockService.AutomaticToolTipEnabled and here’s how it works.

Like IsTextTrimmed, the AutomaticToolTipEnabled attached property does not have an associated dependency property that backs it. Unlike IsTextTrimmed, AutomaticToolTipEnabled is not a read-only property and, its implementation is much less involved since the property itself has no custom logic:

  1 public static readonly DependencyProperty AutomaticToolTipEnabledProperty = DependencyProperty.RegisterAttached(

  2     “AutomaticToolTipEnabled”,

  3     typeof( bool ),

  4     typeof( TextBlockService ),

  5     new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.Inherits ) );


  7 [AttachedPropertyBrowsableForType( typeof( DependencyObject ) )]

  8 public static Boolean GetAutomaticToolTipEnabled( DependencyObject element )

  9 {

 10     if ( null == element )

 11     {

 12         throw new ArgumentNullException( “element” );

 13     }

 14     return (bool) element.GetValue( AutomaticToolTipEnabledProperty );

 15 }


 17 public static void SetAutomaticToolTipEnabled( DependencyObject element, bool value )

 18 {

 19     if ( null == element )

 20     {

 21         throw new ArgumentNullException( “element” );

 22     }

 23     element.SetValue( AutomaticToolTipEnabledProperty, value );

 24 }

Some additional differences to note are:

  1. The value defaults to true.
  2. The value is automatically inherited by child elements.
  3. The property can be attached to any DependencyObject.

Points two and three are incredibly powerful when it comes to applying this property to multiple controls which makes it even more wonderful that all of that behavior is provided for free by the framework.

OK, so we’ve created a new attached property – but by itself that’s not going to provide any additional behavior. There are a lot of ways you could go about this, but the way I felt was most reusable under any number of scenarios was to create a ResourceDictionary (TextBlockServiceDictionary.xaml) with some simple property triggers.

  1 <ResourceDictionary

  2    xmlns=

  3    xmlns:x=

  4    xmlns:tbs=”clr-namespace:TextBlockService”>


  6     <!–

  7     Rather than forcing *all* TextBlocks to adopt TextBlockService styles,

  8     using x:Key allows a more friendly opt-in model.

  9     –>

 10     <Style TargetType=”TextBlock” x:Key=”TextBlockService”>

 11         <Style.Triggers>

 12             <MultiTrigger>

 13                 <MultiTrigger.Conditions>

 14                     <Condition Property=”tbs:TextBlockService.AutomaticToolTipEnabled” Value=”True” />

 15                     <Condition Property=”tbs:TextBlockService.IsTextTrimmed” Value=”True” />

 16                 </MultiTrigger.Conditions>


 18                 <Setter Property=”ToolTip” Value=”{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}” />

 19             </MultiTrigger>

 20         </Style.Triggers>

 21     </Style>


 23 </ResourceDictionary>

First off, if you’ve never worked with resource dictionaries before, just think of it as a collection of resources (styles, templates, etc). If you import a dictionary, you import all of the resources it contains.

Two things are crucial here: First, I’m importing the TextBlockService CLR namespace, which means I automatically have access to the IsTextTrimmed attached property and the new AutomaticToolTipEnabled attached property.

Second, I’ve defined a style using TextBlock as the target type but also giving it an x:Key identifier. As a result, this style is considered an “explicit” style and will not be automatically applied to TextBlocks – something has to explicitly cause the style to be applied. I did this for several reasons, but a major one is that there are a lot of intricacies in WPF’s style system and many of them come into play when you attempt to use multiple implicit styles for the same type. As a result, it’s just easier for everyone if this style remains explicit. (By the way, Ian Griffiths has a great blog entry which explains default styles that I ran across while researching some of those intricacies.)

I defined a multi-condition trigger that says (in prose form): whenever AutomaticToolTipEnabled is true and IsTextTrimmed is true, set the value of the ToolTip property for this TextBlock to the value of the TextBlock’s Text property.

Making use of that style is also quite straightforward, it just requires merging in the resource dictionary. In addition to pulling in the styles, you also need to cause your TextBlocks to use the style. That’s easy to do and it’s also easy to cause those styles to apply to all TextBlocks in a control, window or even application (if it’s a WPF application.)

  1 <Window.Resources>

  2     <ResourceDictionary>

  3         <ResourceDictionary.MergedDictionaries>

  4             <!– Merge in the TextBlockService style –>

  5             <ResourceDictionary Source=”/TextBlockService;component/TextBlockServiceDictionary.xaml” />

  6         </ResourceDictionary.MergedDictionaries>


  8         <!– Use BasedOn to apply TextBlockService behavior to every TextBlock in this Window –>

  9         <Style TargetType=”TextBlock” BasedOn=”{StaticResource TextBlockService}”>

 10             <Style.Setters>

 11                 <Setter Property=”TextWrapping” Value=”NoWrap” />

 12                 <Setter Property=”TextTrimming” Value=”WordEllipsis” />

 13                 <Setter Property=”Text” Value=”Some not-so-random text” />

 14             </Style.Setters>

 15         </Style>

 16     </ResourceDictionary>

 17 </Window.Resources>

(Note: the ResourceDictionary.Source property uses Pack URI syntax which is fairly well documented on MSDN.)

In this case I’m not only applying the TextBlockService style to all my TextBlocks (by attaching it to my style via the BasedOn attribute), but I’m also adding some additional global properties. Remember, since the default value of AutomaticToolTipEnabled is true, the above fragment will enable the full host of functionality my TextBlockService has to offer for every TextBlock in that Window.

Finally, the AutomaticToolTipEnabled attached property can also be used in a more granular fashion:

 25 <StackPanel DockPanel.Dock=”Top”>

 26     <!– Adopts the default True value for AutomaticToolTipEnabled –>

 27     <TextBlock />


 29     <!– Explicity sets AutomaticToolTipEnabled –>

 30     <TextBlock tbs:TextBlockService.AutomaticToolTipEnabled=”False” Background=”LightGray” />


 32     <Grid tbs:TextBlockService.AutomaticToolTipEnabled=”True”>

 33         <!– Inherits a value for AutomaticToolTipEnabled –>

 34         <TextBlock />

 35     </Grid>

 36 </StackPanel>


Well, this post has droned on long enough. If you’re interested, please take a look at the Sample project I included in this zip file (right click, Save As…, and change the extension to .zip – WordPress doesn’t permit uploading files with a .zip extension.)

And as I mentioned earlier, please feel free to leave questions, comments or suggestions. We’re all learning.

About these ads

25 Responses to Customizing “lookful” WPF controls – Take 2

  1. Jean-Marie Pirelli says:

    Fantastic ! works great, thanks a lot for sharing.

    I made a small change in OnTextBlockSizeChanged: I removed the condition on the TextWrapping property as the ellipsis works fine in this case (although the 3.5 docs say it’s not supported).

  2. Jean-Marie Pirelli says:

    I found out that the algorithm does not work for a multi-line text box. I changed it like this:

    Replace the last return statement in CalculateIsTextTrimmed by:

    formattedText.MaxTextWidth = textBlock.ActualWidth;

    return (formattedText.Height > textBlock.ActualHeight);

    So, the FormattedText instance is given a max width, so if it has to occupy more height than the given TextBlock, it means that the text was trimmed.
    This also works for a single-line text block.

  3. hempelcx says:

    Great, Jean-Marie!

    I had a hunch that algorithm wouldn’t work for multi-line text boxes and if I remember correctly, is partly why I added the condition on TextWrapping (though it’s been a while, now).

    Glad you got it working; I’m updating the code sample to reflect the more inclusive algorithm. It now works with multi-line TextBlocks and with TextWrapping enabled or disabled.

  4. [...] Customizing "lookful" WPF controls [UPDATE: This solution has been superceded by a much better implementation in this later post.] [...]

  5. kevin says:

    I find it doesn’t work if there is only one word in the Text and I set Trimming=”CharacterEllipsis”.

  6. kevin says:

    I found this can be resolved by this:

    // If there is only a word
    if (!string.IsNullOrEmpty(formattedText.Text) &&
    !formattedText.Text.Contains(” “))
    return (int)formattedText.Width >

  7. Matt says:

    hempel, Jean-Marie and kevin,

    Thank You!!!! Handling wrapped TextBlocks was the final missing piece, it works perfectly now!

  8. STEVE F says:

    Anyone have code for this working in a textbox.
    If so could you email it to me at

  9. Richard Jones says:

    To solve the problem with calculating IsTextTrimmed when the field contains a single word, a simpler solution is to add the line:
    formattedText.Trimming = TextTrimming.None
    formattedText.MaxTextWidth = textBlock.ActualWidth
    (rather than Kevin’s extra code from August 8th). This means that formattedText’s height always increases if its content needs to wrap around.

    I have another problem: The IsTextTrimmed property is recalculated if the textblock size is changed (e.g. if the UI is resized). However, it doesn’t get recalculated if the textblock’s content is changed. I’m using binding to set the textblock’s value, and IsTextTrimmed is calculated correctly when the field is displayed initially. However if the underlying Text value is then changed to something else, IsTextTrimmed doesn’t get recalculated (unless you jiggle the edge of the UI!)

    It seems that where we register the handler for the SizeChangedEvent, we also need to do something similar for when the Text property’s value is changed.
    I’ve tried various things but haven’t found an elegant solution. Any ideas?

  10. jan says:

    Hi! Thanks for this post. I downloaded the project file and it works on the multi-line but not if there’s just one word in it and Trimming = ”CharacterEllipsis” as Kevin noted above. Do you have an updated project file?

  11. jan says:

    oh never mind, i figured it out.
    sorry for the spam =)

  12. Marcel Kunz says:

    If you want to set Padding and Margin to the TextBlock you must subtract them from the textblock size in the CalculateIsTextTrimmed method.

  13. hempelcx says:

    Thanks for the comment, Marcel.

    Sice “a non-zero margin applies space outside the element layout’s ActualWidth and ActualHeight” ( you shouldn’t need to take that into account for CalculateIsTextTrimmed. Padding is an interesting issue, however, as well as any render transforms which may be applied to the TextBlock (and may or may not be applied when CalculateIsTextTrimmed executes.) I haven’t tested the proposed algorithm against either scenario, but I expect it will fail under both.

    This solution is suboptimal and would definitely be better served by a custom control allowing access to MeasureOverride and ArrangeOverride. In this case, most of us are just looking for a clean hack to overcome the lookful nature of the TextBlock control when that’s the only option.

    If you have a revised general purpose algorithm which you believe covers more cases, please post it in the comments!

  14. Alexey says:


    It works fine for stand-alone TextBlock, but do not work properly for TextBlock embedded into DataGrid. It just trim text by column width. No elipses, no tooltip… Can you help me? Sorry for my english…

    Cut from Sample code:

    …other columns defenitions…

    …other columns defenitions…

    • hempelcx says:

      Hi Alexey,

      The source code you tried to include didn’t come through. WordPress formatting can be frustrating.

      I have definitely used it within a DataGrid (that was the primary use case that drove this design). Is it possible you’re not getting the style applied to the TextBlocks within the grid? That’s easy to overlook.

      Be sure you’ve added the <Style TargetType=”TextBlock” BasedOn=”{StaticResource TextBlockService}”> … bit to either the grid itself or to one of its logical parent elements.

      • Alexey says:

        Hi, hempelcx!

        Can you post keypoints of code of your working datagrid example?


      • hempelcx says:

        You can try something like this, Alexey. Here I’m explicitly setting the element style for the DataGridTextColumn to one that applies the TextBlockService base style:

        <Style x:Key="TextColumnElementStyle" TargetType="TextBlock"
               BasedOn="{StaticResource TextBlockService}">
                <Setter Property="TextWrapping" Value="NoWrap" />
                <Setter Property="TextTrimming" Value="WordEllipsis" />
        <dg:DataGrid AutoGenerateColumns="False"
                     ItemsSource="{Binding Source={StaticResource FilteredEvents}}" >
                <dg:DataGridTextColumn Width="40" Header="Test" Binding="{Binding Test}"
                                       ElementStyle="{StaticResource TextColumnElementStyle}" />

        I know that approach was working when I wrote this code.

  15. Alexey says:

    Hi, hempelcx.

    I am new to WPF and WordPress.
    Yes I add
    Style TargetType=”TextBlock” BasedOn=”{StaticResource TextBlockService}”



    But what should I do to use it in


    I have wrote
    WpfToolkit:DataGridTemplateColumn Header=”ddd”
    TextBlock tbs:TextBlockService.AutomaticToolTipEnabled=”True” Text=”{Binding Path=Attribute[Message].Value}”/

    What have I missed? Thnaks!

  16. Alexey says:

    May it be because I use ItemsSource=”{Binding}” in DataGrid?

    This column works as needed:
    WpfToolkit:DataGridTextColumn Header=”sss” Binding=”{Binding Path=Attribute[System].Value}”/

    This shows nothing:

    Just TextBlock/ written after this DataGrid above works as sample template described – shows text “Some not-so-random text Some not-so-random text”.

    So may be problem in my DataGrid? Can you post your working DataGrid code?


  17. Dashus says:

    A late entry; I’ve found that in some cases, specifically where items in a treeview are at first not trimming and then become trimmed due to a resize of the treeview, the height of the formatted text is actually less than the textblock’s if word wrapping is off. The following seems to work:

    bool isTrimmed;
    if (textBlock.TextWrapping == TextWrapping.NoWrap)
    isTrimmed = formattedText.Width > textBlock.ActualWidth;
    formattedText.MaxTextWidth = textBlock.ActualWidth;
    // When the maximum text width of the FormattedText instance is set to the actual
    // width of the textBlock, if the textBlock is being trimmed to fit then the formatted
    // text will report a larger height than the textBlock.
    isTrimmed = formattedText.Height > textBlock.ActualHeight;
    return isTrimmed;

  18. [...] of attached properties and a ClassHandler (event handler)  with the WPF Framework.  See Customizing “lookful” WPF controls – Take 2 on the Tranxition Developer Blog for a detailed breakdown of the code in the [...]

  19. meas_meas says:

    I use this:

    // do not use this line
    // formattedText.MaxTextWidth = textBlock.ActualWidth;

    var oneLineHeight = (int)(textBlock.ActualHeight / formattedText.Height);
    var textBlockWidth = oneLineHeight * textBlock.ActualWidth;
    return textBlockWidth < formattedText.Width;

  20. Stefan says:

    @dashus: Thanks for your code. It also will help if you use WPF 4 with this feature on the TextBlock:

    • Stefan says:

      Here is the setter again:
      Property=”TextOptions.TextFormattingMode” Value=”Display”

    • Stefan says:

      Another thing has to be adjusted in CalculateIsTextTrimmed:
      FormattedText formattedText = new FormattedText(
      textBlock.Foreground, new NumberSubstitution(), (TextFormattingMode)textBlock.GetValue(TextOptions.TextFormattingModeProperty));

      Without adding the last parameter the calculation is not always correct if TextFormattingMode is ‘Display’.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Get every new post delivered to your Inbox.

%d bloggers like this: