Jul 3, 2009

TextTrimming in Silverlight 2

In a previous post I found myself hacking around in the Silverlight 2 trying to emulate drop shadows on text blocks (something that is natively supported in WPF but missing in Silverlight 2)

Once again, I find myself trying to emulate functionality that is natively supported in WPF and again it is related to TextBlocks.

In WPF when you have a TextBlock whose text could extend beyond the available space you have the option to trim the text using a specified TextTrimming setting. To do this in WPF you would set the TextTrimming property of the TextBlock element and be done. Something like this:

<TextBlock Text="A really really long string that should be trimmed"
TextTrimming="CharacterEllipsis" />


In Silverlight there is no such property on a TextBlock. So I created a quick and dirty implementation that works (for the most part)



I created a new UserControl called TrimmingTextBlock. The XAML for this control looks like this:



<UserControl x:Class="TextTrimming.TrimmingTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<TextBlock x:Name="TrimmedTextBlock" />
</UserControl>


All the “magic” (a.k.a hacking) happens in the code behind. I wanted the consumer of the control to be able to bind to a Text property, just like a normal TextBlock so I created a custom dependency property called Text



private static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text",
typeof(String),
typeof(TrimmingTextBlock),
new PropertyMetadata(String.Empty,OnTextChanged));


I need to display trimmed text but I still wanted the “Text” property of my control to maintain the original string. Trimming is only a display issue and shouldn’t impact the underlying data. So I created a second private dependency property called TrimmedText where I will store the display version of the string.



private static readonly DependencyProperty TrimmedTextProperty = DependencyProperty.Register(
"TrimmedText",
typeof(String),
typeof(TrimmingTextBlock),
new PropertyMetadata(String.Empty, OnTrimmingTextChanged));


So the first step is to make sure the TrimmedText property gets the value that is bound to the Text property. I do this in the OnTextChanged method (this is fired whenever the Text dependency property is changed). In the changed method I am getting an instance of my control and finding the TextBlock element and setting it’s text property to the new value.



private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var tb = d as TrimmingTextBlock;
if (tb != null)
{
tb.TrimmedTextBlock.Text = e.NewValue.ToString();
}
}


Now that the data is wired up correctly I have to do the actual trimming. In the ArrangeOverride method of my control I call my TrimText method



protected override Size ArrangeOverride(Size finalSize)
{
TrimText(this);
return base.ArrangeOverride(finalSize);
}


In TrimText I get the ActualWidth of the TextBlock. Since I have set the Text property already the ActualWidth will be the width in pixels that is needed to display the specified string of text.



I compare this ActualWidth to the DesiredWidth, that is the width that is available to the TextBlock. If the text is larger then the available space, that is if the ActualWidth is greater than the DesiredWidth, I need to trim.



My trimming implementation is simplistic but seems to work. I chop off one character at a time from the end of the string and then update the TextBlock with the new value. This causes a measure and arrange pass to fire which will call my trim method again and we can re-check the width.



I continue cutting off one character at time until the text will fit. Here is what the Trim method looks like:



private static void TrimText(TrimmingTextBlock block)
{
//Check the desired size of the text block and the actual size and trim accordingly
var actualWidth = block.TrimmedTextBlock.ActualWidth;
var desiredWidth = Double.MinValue;

if (desiredWidth == Double.MinValue)
{
desiredWidth = block.TrimmedTextBlock.DesiredSize.Width;
}

if (desiredWidth < actualWidth)
{
//Trim
String trimmedText = block.TrimmedTextBlock.Text;

if (!trimmedText.Contains("…"))
trimmedText += "…";

trimmedText = String.Concat(trimmedText.Substring(0, trimmedText.IndexOf("…") - 1), "…");
block.SetValue(TrimmedTextProperty, trimmedText);
}

}


This approach seems to work in most cases. For my purposes it does the job. This isn’t the most optimized code as it can cause a ton of Measure and Arrange operations. In fact in Silverlight 2 there is a known bug (which is fixed in SL3 beta1) that you cannot have more than 250 layout operations on one page. See this thread



Because of this bug this solution doesn’t really scale that nicely if you have the need for a bunch of TrimmingTextBlocks on one page. Or if you have a really long string that could cause more than 250 layout recursions.



This code could be optimized so that large strings cause fewer layout passes by intelligently removing more than one character at a time. But for me this works for now.



You can get the code with a few examples of usage from here

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home