Highlight search result in WPF

Wouldn’t it be nice to easily see where in the search result that the search criteria are found? In the same way as most of today’s browsers do when you search for strings on the page, highlight all matches with yellow background.

I want to achieve the following result in WPF:

To be able to achieve the same effect in WPF, I thought I should implement my own TextBlock that has some extensions to it, dependency properties that you can bind to the search textbox. I thought my first attempt would be a custom control and then perhaps later see if it is possible to achieve the same effect with attached properties.

First of all I create a new custom control that extends TextBlock and add a dependency property called HightlightText. I will use HightlightText to find and match substrings in the textbox that will be highlighted with a yellow background. Since it is a dependency property I can then later use element binding to the search field in my application and automatically get all search criteria highlighted in the TextBlock.

A TextBlock element contains an Inline property. This property contains an InlineCollection of the top-level Inline elements that comprise the content of the TextBlock. If we split the HighlightText property to substring, for example, if we search for “Brick 552” then we should highlight all substrings in the TextBlock that contains “Brick” and “552”, then we can build a new InlineCollection containing Run elements with the background set to yellow on all substring matches.

Method to get Highlightable substring (in our application it is possible to use keywords for faster searches, in similar way that Google provides):

        private string[] GetHighlights()
        {
            string[] highligts = HightlightText.Trim().Split(' ');

            // Clean up keywords
            for (int i = 0; i < highligts.Length; i++)
            {
                int index = highligts[i].IndexOf(':');
                if (index != -1)
                {
                    highligts[i] = highligts[i].Substring(index + 1);
                    highligts[i] = highligts[i].Trim();
                }

                // Remove " from the highlight text
                highligts[i] = highligts[i].Replace("\"", "");
            }

            return highligts;
        }

The below code section shows the implementation of the actual InlineCollection “replacement” where I compose a new collection containing Run elements where each highlighted part have a Run element with yellow background.

        private void HighlightSubstrings()
        {            
            List<Run> runs = new List<Run>();

            string text = Text;               

            if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(HightlightText)) return;

            // split the Highlightable Text to substrings if it contains 'space' separated values
            // for example, 'Brick 552' should highlight all Brick and 552 substrings in the Text
            string[] highligts = GetHighlights();

            List<KeyValuePair<int, int>> inlines = new List<KeyValuePair<int, int>>();


            foreach (var highlight in highligts)
            {
                int index = text.IndexOf(highlight, StringComparison.CurrentCultureIgnoreCase);
                while (index != -1)
                {
                    int startIndex = index;
                    int endIndex = index + highlight.Length;

                    inlines.Add(new KeyValuePair<int, int>(startIndex, highlight.Length));

                    // Find next occurance of the current highlight text
                    index = text.IndexOf(highlight, endIndex, StringComparison.CurrentCultureIgnoreCase);
                }
            }

            // Sort inlines and remove/append overlapping inlines
            inlines.Sort(InlineComparison);
            List<KeyValuePair<int, int>> temp = new List<KeyValuePair<int, int>>();
            for (int i = 0; i < inlines.Count; i++)
            {
                if (temp.Count > 0)
                {
                    KeyValuePair<int, int> previous = temp[temp.Count - 1];
                    if (previous.Key + previous.Value > inlines[i].Key)
                    {
                        int maxEndCaret = Math.Max(
                            previous.Key + previous.Value,
                            inlines[i].Key + inlines[i].Value);
                        temp[temp.Count - 1] = new KeyValuePair<int, int>(
                            previous.Key, maxEndCaret - previous.Key);
                    }
                    else
                    {
                        temp.Add(inlines[i]);
                    }
                }
                else
                {
                    temp.Add(inlines[i]);
                }
            }
            inlines = temp;

            // Did we find any text to highlight?
            if (inlines.Count > 0)
            {
                Inlines.Clear();
                int caretPosition = 0;
                foreach (var inline in inlines)
                {
                    // add the text from the current caret position to the highlighted text
                    Inlines.Add(new Run(text.Substring(caretPosition, inline.Key - caretPosition)));
                    // add highlighted text
                    Inlines.Add(new Run
                    {
                        Text = text.Substring(inline.Key, inline.Value),
                        Background = Brushes.Yellow
                    });
                    // Move caret to contiue search for next highlight text
                    caretPosition = inline.Key + inline.Value;
                }

                if (inlines.Count > 0 && caretPosition < text.Length - 1)
                {
                    // Add the remaining part of the text
                    Inlines.Add(new Run(text.Substring(caretPosition)));
                }
            }          
        }

That’s basically it and it seems to work perfectly. Although have not tested it for performance or memory consumption yet. But our application has a ListView that shows the search result and highlights strings in each column and row that matches the search criteria. Our ListView sometimes shows over 5000 rows and runs smoothly, so the performance impact is not something I noticed at least.

Let’s see if I give this another attempt using attached properties later. Would be nice if I could set the “HighligtText” attached property on ListView level and it would then highlight all text in all columns and rows in the ListView. I have not given this any thought yet so perhaps it’s impossible or very hard to accomplish :)

Advertisements
  1. No trackbacks yet.

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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

%d bloggers like this: