WPF Snippet - Determining If Text Is Ellipsed

Skill

WPF Snippet - Determining If Text Is Ellipsed

Posted in:

So here's the problem - say you have a WPF TextBlock with some text (with automatic ellipsing turned on) and you want to know if the text is actually ellipsed at the moment. Sounds simple, right? Unfortunately, there are no properties or methods that you can call to check - well, no public properties or methods, that is. So this makes a simple question into a much harder answer.

Our simple sample application here today is going to take text that you enter in a TextBox and place it in a TextBlock. If the text does not fit, and starts ellipsing, a tooltip will automatically get added to the TextBlock that contains the full text. Below is a screenshot of this super simple UI in action:

Sample Screenshot

Now, there are two tactics that you could take to figuring out if text in a TextBlock is ellipsed. One is to create an approximate replica of the formula that a TextBlock uses to determine when to ellipse text (which is some function of font, font size, other font attributes, padding, margin, the width of the TextBlock, etc..). This can get tedious and annoying and never seems to be right in every case. The other possibility is not quite so clean (not that the first solution is clean) - use Reflector to find the private/internal methods/properties to check to see if text is currently ellipsed, and then use reflection to call those in code.

Both of these solutions have the unfortunate side effect of tying you to Microsoft's current implementation of TextBlock - but there really isn't any way around that until Microsoft exposes a nice clean public way to get at this attribute. I decided to go with the second route, because while it is a little uglier to code, it has the benefit of always being correct (without having to make tiny adjustments to a formula). A second reason that I used the reflection approach is that it is more likely to blow up all over the place if Microsoft changes the internals of TextBlock, as opposed to having an incorrect formula and not even realizing it.

After exploring the class surrounding TextBlock in Reflector for 20-30 minutes, I decided that the easiest way to determine ellipse status was to use the internal method GetLineDetails:

internal void GetLineDetails(int dcp, int index, double lineVOffset,
   out int cchContent, out int cchEllipses)
{
  ...
}

One of the problems with just using stuff that you found in Reflector is that what the arguments to a method are supposed to be is not always easy. In this case, it isn't too bad. First off, we don't have to worry about the last two arguments - they are just out parameters. liveVOffset seems pretty obvious - the vertical offset of the line. Since I'm only worrying about TextBlocks that contain one line of text, this will always be 0. The other two arguments are not obvious just from the name, but after reading the code in Reflector, you can determine that index is the line number (again always 0 since we only care about the single line case), and a character position offset (which can also always be zero in our case, since we are always starting at the beginning).

Now, I said we don't care about those two out parameters, but that is not actually quite true. We do care about one of them - cchEllipses. This is how we will know if a line has ellipses. If the value returned in this variable is 0, there are no ellipses, but if the value is greater than 0, ellipses are showing.

Ok, time to dive into some code that uses this method. First, the simple XAML for our example:

<Window x:Class="EllipseTest.Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="100" Width="300">
  <StackPanel Margin="5">
    <TextBox x:Name="_InputBox" />
    <TextBlock x:Name="_OutputBox" TextTrimming="CharacterEllipsis"
              Text="{Binding ElementName=_InputBox, Path=Text}" />
  </StackPanel>
</Window>

All we have here is a TextBlock whose text property is bound to the text of a TextBox. Really can't get much simpler than that. So now let's get set up to check for ellipses:

var getLineDetails =
  typeof(TextBlock).GetMethod("GetLineDetails",
  BindingFlags.NonPublic | BindingFlags.Instance);
 
var args = new object[] { 0, 0, 0, 0, 0 };
getLineDetails.Invoke(_OutputBox, args);

if ((int)args[4] > 0)
{ MessageBox.Show("Ellipsing"); }
else
{ MessageBox.Show("No Ellipsing."); }

This is the basic setup that we need. First, we have to grab the method GetLineDetails using reflection. Second, we have to set up the argument array (which, as we determined before, can be all zeros). Generally, I just set up the argument array right inside the invoke call, but because there is an out parameter that we need to get the value of, we need to maintain a reference to the array. This is because the last element in the array after the invoke call will hold the new value that we care about.

So once we do the invoke call against the _OutputBox TextBlock, we check that last element in the array to see if it is greater than 0. If it is, then we have ellipsing. If not, then there are no ellipses.

Ok, now that we know how to use this GetLineDetails method, let's throw together the actual code:

using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace EllipseTest
{
  public partial class Window1 : Window
  {
    private MethodInfo GetLineDetails =
      typeof(TextBlock).GetMethod("GetLineDetails",
      BindingFlags.NonPublic | BindingFlags.Instance);

    public Window1()
    {
      InitializeComponent();
      var d = DependencyPropertyDescriptor.FromProperty(
        TextBlock.TextProperty, typeof(TextBlock));
      d.AddValueChanged(_OutputBox, OutputBoxTextChanged);
    }

    private void OutputBoxTextChanged(object sender, EventArgs e)
    { Dispatcher.Invoke((Action)SetUpToolTip, DispatcherPriority.Background, null); }

    private void SetUpToolTip()
    {
      var args = new object[] { 0, 0, 0, 0, 0 };
      GetLineDetails.Invoke(_OutputBox, args);

      if ((int)args[4] > 0)
      { _OutputBox.ToolTip = _OutputBox.Text; }
      else
      { _OutputBox.ToolTip = null; }
    }
  }
}

So here we hook up a value changed event handler to the Text dependency property on _OutputBox. This way we will get notified whenever the text changes. This event handler, in turn, dispatches a call to SetUpToolTip, which is very similar to the code we just saw above (except in this case we are setting a tooltip, not showing a message box). One thing we did optimize here is we pullout out the MethodInfo for GetLineDetails to a field - this way we are not calling GetMethodInfo every time text changes.

You might be wondering why SetUpToolTip is dispatched instead of being called directly. This is because when the text value changed event fires, the TextBlock is not in a good stable state (the text is changing) and if you call GetLineDetails right then, your application fall apart into a smoking pile of rubble. By dispatching the SetUpToolTip call, we make sure that the TextBlock is ready by the time we call GetLineDetails. This is one of the major hazards of using reflection to hit at internal and private methods and properties - they aren't supposed to be used by people like you and me, and so you can run into all sorts of issues like this one.

Well, that is it for determining if text is ellipsed in a TextBlock. This technique is not for the faint of heart (stuff using reflection never is), but unfortunately there are no other ways to accomplish this (as far as I know). You can grab the code for the sample app below, and if you have any questions drop a comment and I'll do my best to answer them.

Om
07/14/2009 - 09:39

As usual this website rocks !! & article too rock!!

reply

Kolor
11/25/2009 - 22:09

Good job!

reply

pasza
09/23/2010 - 04:08

that's works faster... mb...

private delegate void GetLineDetails_Delegate(TextBlock txt, int dcp, int index, double lineVOffset, out int cchContent, out int cchEllipses);

        private static GetLineDetails_Delegate CreateGetLineDetails()
        {
            DynamicMethod dynamic_method = new DynamicMethod("Dynamic_TextBlock_GetLineDetails", null, new Type[] { typeof(TextBlock), typeof(int), typeof(int), typeof(double), typeof(int).MakeByRefType(), typeof(int).MakeByRefType() }, typeof(TextBlock));
            ILGenerator il = dynamic_method.GetILGenerator();
            il.Emit(OpCodes.Ldarg_S, 0);
            il.Emit(OpCodes.Ldarg_S, 1);
            il.Emit(OpCodes.Ldarg_S, 2);
            il.Emit(OpCodes.Ldarg_S, 3);
            il.Emit(OpCodes.Ldarg_S, 4);
            il.Emit(OpCodes.Ldarg_S, 5);
            il.Emit(OpCodes.Call, typeof(TextBlock).GetMethod("GetLineDetails", BindingFlags.NonPublic | BindingFlags.Instance));
            il.Emit(OpCodes.Ret);

            return dynamic_method.CreateDelegate(typeof(GetLineDetails_Delegate)) as GetLineDetails_Delegate;
        }

        private static GetLineDetails_Delegate DynamicGetLineDetails = CreateGetLineDetails();

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.
CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.