Creating a highlighting text block for Silverlight 3, revisited
Now that Silverlight 3 has shipped, I’d like to take a moment to revisit the highlighting AutoCompleteBox control that I blogged about back in November of ‘08, and again earlier this year, thanks to tooling improvements: Expression Blend 3 is out, and the Visual Studio 2008 tools have changed as well. There are new project and item templates this time around.
This short post re-creates the HighlightingTextBlock control, using the Templated Silverlight Control item template that ships in the Silverlight Tools. When I last blogged about the highlighting text block control, I had to describe in detail how to go about creating a library, creating the default control styles file (Generic.xaml), setting properties, and putting it all together.
Now it is a lot easier! Using the advanced copy-and-paste coding technique, you can create and build this control in about 2 minutes.
Create a new Silverlight Class Library Project
- Open Visual Studio 2008 SP1
- File | New Project, Visual C# | Silverlight | Silverlight Class Library project type
Remove Class1.cs
The default class file, Class1.cs, can be removed. Right-click on it in the Solution Explorer and select the ‘Delete’ menu item.
Use the ‘Silverlight Templated Control’ template
The new template is great since it creates a simple class for the control, sets up the default style key, and then creates/modifies the Generic.xaml theme file for the library, setting all the right properties along the way.
- Click on the Project menu (or right-click on the project in the Solution Explorer)
- Select ‘Add New Item’
- Use the ‘Silverlight Templated Control’ template
- Change the name from TemplatedControl1.cs to HighlightingTextBlock.cs
- Click ‘Add’
Insert the control code
Borrowed from my previous post on the topic, just paste this class’ code into the namespace, replacing what is already there:
/// <summary>
/// A specialized highlighting text block control.
/// </summary>
public partial class HighlightingTextBlock : Control
{
/// <summary>
/// The name of the TextBlock part.
/// </summary>
private string TextBlockName = "Text";
/// <summary>
/// Gets or sets the text block reference.
/// </summary>
private TextBlock TextBlock { get; set; }
/// <summary>
/// Gets or sets the inlines list.
/// </summary>
private List<Inline> Inlines { get; set; }
#region public string Text
/// <summary>
/// Gets or sets the contents of the TextBox.
/// </summary>
public string Text
{
get { return GetValue(TextProperty) as string; }
set { SetValue(TextProperty, value); }
}
/// <summary>
/// Identifies the Text dependency property.
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(HighlightingTextBlock),
new PropertyMetadata(OnTextPropertyChanged));
/// <summary>
/// TextProperty property changed handler.
/// </summary>
/// <param name="d">AutoCompleteBox that changed its Text.</param>
/// <param name="e">Event arguments.</param>
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HighlightingTextBlock source = d as HighlightingTextBlock;
if (source.TextBlock != null)
{
while (source.TextBlock.Inlines.Count > 0)
{
source.TextBlock.Inlines.RemoveAt(0);
}
string value = e.NewValue as string;
source.Inlines = new List<Inline>();
if (value != null)
{
for (int i = 0; i < value.Length; i++)
{
Inline run = new Run { Text = value[i].ToString() };
source.TextBlock.Inlines.Add(run);
source.Inlines.Add(run);
}
source.ApplyHighlighting();
}
}
}
#endregion public string Text
#region public string HighlightText
/// <summary>
/// Gets or sets the highlighted text.
/// </summary>
public string HighlightText
{
get { return GetValue(HighlightTextProperty) as string; }
set { SetValue(HighlightTextProperty, value); }
}
/// <summary>
/// Identifies the HighlightText dependency property.
/// </summary>
public static readonly DependencyProperty HighlightTextProperty =
DependencyProperty.Register(
"HighlightText",
typeof(string),
typeof(HighlightingTextBlock),
new PropertyMetadata(OnHighlightTextPropertyChanged));
/// <summary>
/// HighlightText property changed handler.
/// </summary>
/// <param name="d">AutoCompleteBox that changed its HighlightText.</param>
/// <param name="e">Event arguments.</param>
private static void OnHighlightTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HighlightingTextBlock source = d as HighlightingTextBlock;
source.ApplyHighlighting();
}
#endregion public string HighlightText
#region public Brush HighlightBrush
/// <summary>
/// Gets or sets the highlight brush.
/// </summary>
public Brush HighlightBrush
{
get { return GetValue(HighlightBrushProperty) as Brush; }
set { SetValue(HighlightBrushProperty, value); }
}
/// <summary>
/// Identifies the HighlightBrush dependency property.
/// </summary>
public static readonly DependencyProperty HighlightBrushProperty =
DependencyProperty.Register(
"HighlightBrush",
typeof(Brush),
typeof(HighlightingTextBlock),
new PropertyMetadata(null, OnHighlightBrushPropertyChanged));
/// <summary>
/// HighlightBrushProperty property changed handler.
/// </summary>
/// <param name="d">HighlightingTextBlock that changed its HighlightBrush.</param>
/// <param name="e">Event arguments.</param>
private static void OnHighlightBrushPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HighlightingTextBlock source = d as HighlightingTextBlock;
source.ApplyHighlighting();
}
#endregion public Brush HighlightBrush
#region public FontWeight HighlightFontWeight
/// <summary>
/// Gets or sets the font weight used on highlighted text.
/// </summary>
public FontWeight HighlightFontWeight
{
get { return (FontWeight)GetValue(HighlightFontWeightProperty); }
set { SetValue(HighlightFontWeightProperty, value); }
}
/// <summary>
/// Identifies the HighlightFontWeight dependency property.
/// </summary>
public static readonly DependencyProperty HighlightFontWeightProperty =
DependencyProperty.Register(
"HighlightFontWeight",
typeof(FontWeight),
typeof(HighlightingTextBlock),
new PropertyMetadata(FontWeights.Normal, OnHighlightFontWeightPropertyChanged));
/// <summary>
/// HighlightFontWeightProperty property changed handler.
/// </summary>
/// <param name="d">HighlightingTextBlock that changed its HighlightFontWeight.</param>
/// <param name="e">Event arguments.</param>
private static void OnHighlightFontWeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HighlightingTextBlock source = d as HighlightingTextBlock;
FontWeight value = (FontWeight)e.NewValue;
}
#endregion public FontWeight HighlightFontWeight
/// <summary>
/// Initializes a new HighlightingTextBlock class.
/// </summary>
public HighlightingTextBlock()
{
DefaultStyleKey = typeof(HighlightingTextBlock);
Loaded += OnLoaded;
}
/// <summary>
/// Loaded method handler.
/// </summary>
/// <param name="sender">The loaded event.</param>
/// <param name="e">The event data.</param>
private void OnLoaded(object sender, RoutedEventArgs e)
{
OnApplyTemplate();
}
/// <summary>
/// Override the apply template handler.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Grab the template part
TextBlock = GetTemplateChild(TextBlockName) as TextBlock;
// Re-apply the text value
string text = Text;
Text = null;
Text = text;
}
/// <summary>
/// Apply the visual highlighting.
/// </summary>
private void ApplyHighlighting()
{
if (Inlines == null)
{
return;
}
string text = Text ?? string.Empty;
string highlight = HighlightText ?? string.Empty;
StringComparison compare = StringComparison.OrdinalIgnoreCase;
int cur = 0;
while (cur < text.Length)
{
int i = highlight.Length == 0 ? -1 : text.IndexOf(highlight, cur, compare);
i = i < 0 ? text.Length : i;
// Clear
while (cur < i && cur < text.Length)
{
Inlines[cur].Foreground = Foreground;
Inlines[cur].FontWeight = FontWeight;
cur++;
}
// Highlight
int start = cur;
while (cur < start + highlight.Length && cur < text.Length)
{
Inlines[cur].Foreground = HighlightBrush;
Inlines[cur].FontWeight = HighlightFontWeight;
cur++;
}
}
}
}
Then, refactor the Using statements to make the code a little crisper:
- Right-click on one of the ‘using’ statements at the top of the file
- Select ‘Organize Usings’, then ‘Remove and Sort’
Define the default control style
Now, Generic.xaml is already created in the Themes folder – so go ahead and open it, then use this for the control template. Our default style is simple: sets the default highlight brush color, plus a single template part – a text block named ‘Text’.
<Style TargetType="local:HighlightingTextBlock">
<Setter Property="HighlightBrush" Value="Blue" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:HighlightingTextBlock">
<TextBlock x:Name="Text" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Build the project, and you’re good to go and use that control now. Hope this helps!

Wouldn’t it make more sense to make this a behavior you attach to the textblock instead? It would make the code simpler, and you can just throw it on existing textblocks instead of having to change them out with other controls. You could also make it a TargetedTriggerAction and use the textchanged event to trigger the action. This would make it more flexible on what triggers the highlight (ie. highlight on mouseenter on another UIElement etc)
@Morten,
Definitely. This control was originally for a “proof-of-concept” implementation – it’s just back from the dead for this post
Looks like there may be a problem with threading concurrency at private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
My AutoCompleteBox has a DataTemplate with a stackpanel holding a TextBlock and HighlightingTextBlock both bound to the same field of my entity. The TextBlock is always precisely correct as I can see and I arrow throught the results. The HighlightingTextBlock begins show text from later fields that do not accurately represent the current entity.
@Gerard,
interesting – Silverlight only has one UI thread. Any more info on the issue you are seeing?
When I drag the AutoCompleteBox scrollbar up and down, the HighlightingTextBlock in the DataTemplate continues to change to other entities in the list. The TextBlock is always bound to the correct entity. The HighlightingTextBlock seems to do this as it redraws. The initial, visible portion of the list, before scrolling, matches the entities. No matter how I scroll, the HighlightingTextBox starts to display text from other entities in the list.
Simple test to reproduce the HighlightingTextBlock paint/draw issue. In a ListBox, add items bound to entities with a DataTemplate. In the DataTemplate bind a TextBlock and a HighlightingTextBlock to the same entity field. Make sure there are enough entities to scroll. Drag the scroll bar up and down fast. Notice how the TextBlock and the HighlightingTextBlock display different values.
Any helpful insight would be appreciated.
I modified the class to extend ContentControl instead of Control and modified the constructor to instantiate the TextBlock and add it to the Content.
That corrected the issue. Robby Ingebretsen’s TextBloc blog helped.
http://blog.nerdplusart.com/archives/texttrimming-textblock-for-silverlight
I have a larger list that requires me to scroll when I do the data bound to the HighlightTextBlock is out of sync with the data in the regular textblock. I tried Gerards suggestion of changing the class to extend form ContentControl and instantiating the TextBlock and add it to Content but this did not work. Any suggestions would be greatful
I too, have the same problem as Gerard and not only when scrolling.
Sometimes items that does not match the text is displayed in the list, but when moving down to it with the arrow key and select it, it will be something else.
So it seems that the filtered list is correct but it displays the wrong values in the DataTemplate that uses the HighligtingTextBox.
If I remove the highlighting from my DataTemplate it works fine.
Thanks for this i created a syntax hightlighter based on this and delay sample control http://blogs.msdn.com/delay/archive/2008/06/19/text-from-a-slightly-different-perspective-verticaltextblock-control-sample-for-silverlight-2.aspx http://forums.silverlight.net/forums/t/138293.aspx