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’

Templated

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’

RefactorUsings

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!

Comments

  1. August 26th, 2009 | 2:45 pm

    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)

  2. August 26th, 2009 | 3:08 pm

    @Morten,
    Definitely. This control was originally for a “proof-of-concept” implementation – it’s just back from the dead for this post :-)

  3. Gerard
    August 27th, 2009 | 12:00 pm

    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.

  4. August 27th, 2009 | 12:10 pm

    @Gerard,
    interesting – Silverlight only has one UI thread. Any more info on the issue you are seeing?

  5. Gerard
    August 27th, 2009 | 1:31 pm

    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.

  6. Gerard
    August 31st, 2009 | 7:41 am

    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.

  7. Gerard
    August 31st, 2009 | 10:23 am

    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

  8. Jamel
    September 10th, 2009 | 11:43 am

    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

  9. Håkan
    October 22nd, 2009 | 12:17 am

    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.

  10. slyi
    October 25th, 2009 | 11:01 am