WPF TextBlock highlight certain parts based on search condition

By strange coincidence, I have recently written an article that solves the very same problem. It is a custom control that has the same properties as a TextBlock (so you can swap is out for a TextBlock wherever you need it), and it has an extra Property that you can bind to called HighLightText, and wherever the value of HighLightText is found in the main Text property (case insensitive), it is highlighted.

It was a fairly straight-forward control to create, and you can find the full code as a solution here:

SearchMatchTextblock(GitHub)


This question is similar to How to display search results in a WPF items control with highlighted query terms

In answer to that question, I came up with an approach that uses an IValueConverter. The converter takes a text snippet, formats it into valid XAML markup, and uses a XamlReader to instantiate the markup into framework objects.

The full explanation is rather long, so I've posted it to my blog: Highlighting Query Terms in a WPF TextBlock


Differences to other solutions

  • easier to reuse -> attached behavior instead of custom control
  • MVVM friendly -> no code behind
  • works BOTH ways! -> Changing the term to be highlighted OR the text, both updates the highlight in the textblock. The other solutions i checked had the problem, that changing the text does not reapply the highlighting. Only changing the highlighted term/search text worked.

How to use

  • IMPORTANT: do NOT use the regular Text="blabla" property of the TextBlock anymore. Instead bind your text to HighlightTermBehavior.Text="blabla".
  • Add the attached properties to your TextBlock like that
<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="{Binding MyTerm}"
           local:HighlightTermBehavior.Text="{Binding MyText}" />

or hardcoded

<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="highlight this"
           local:HighlightTermBehavior.Text="bla highlight this bla" />

Add this class

  • To change the kind of highlighting, just change these Methods:
    AddPartToTextBlock() for non highlighted text
    AddHighlightedPartToTextBlock() for the highlighted text.
  • At the moment highlighted is FontWeights.ExtraBold and non highlighted text is FontWeights.Light.
  • probably hard to read without an IDE, sorry.
public static class HighlightTermBehavior
{
    public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
        "Text",
        typeof(string),
        typeof(HighlightTermBehavior),
        new FrameworkPropertyMetadata("", OnTextChanged));

    public static string GetText(FrameworkElement frameworkElement)               => (string) frameworkElement.GetValue(TextProperty);
    public static void   SetText(FrameworkElement frameworkElement, string value) => frameworkElement.SetValue(TextProperty, value);


    public static readonly DependencyProperty TermToBeHighlightedProperty = DependencyProperty.RegisterAttached(
        "TermToBeHighlighted",
        typeof(string),
        typeof(HighlightTermBehavior),
        new FrameworkPropertyMetadata("", OnTextChanged));

    public static string GetTermToBeHighlighted(FrameworkElement frameworkElement)
    {
        return (string) frameworkElement.GetValue(TermToBeHighlightedProperty);
    }

    public static void SetTermToBeHighlighted(FrameworkElement frameworkElement, string value)
    {
        frameworkElement.SetValue(TermToBeHighlightedProperty, value);
    }


    private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBlock textBlock)
            SetTextBlockTextAndHighlightTerm(textBlock, GetText(textBlock), GetTermToBeHighlighted(textBlock));
    }

    private static void SetTextBlockTextAndHighlightTerm(TextBlock textBlock, string text, string termToBeHighlighted)
    {
        textBlock.Text = string.Empty;

        if (TextIsEmpty(text))
            return;

        if (TextIsNotContainingTermToBeHighlighted(text, termToBeHighlighted))
        {
            AddPartToTextBlock(textBlock, text);
            return;
        }

        var textParts = SplitTextIntoTermAndNotTermParts(text, termToBeHighlighted);

        foreach (var textPart in textParts)
            AddPartToTextBlockAndHighlightIfNecessary(textBlock, termToBeHighlighted, textPart);
    }

    private static bool TextIsEmpty(string text)
    {
        return text.Length == 0;
    }

    private static bool TextIsNotContainingTermToBeHighlighted(string text, string termToBeHighlighted)
    {
        return text.Contains(termToBeHighlighted, StringComparison.Ordinal) == false;
    }

    private static void AddPartToTextBlockAndHighlightIfNecessary(TextBlock textBlock, string termToBeHighlighted, string textPart)
    {
        if (textPart == termToBeHighlighted)
            AddHighlightedPartToTextBlock(textBlock, textPart);
        else
            AddPartToTextBlock(textBlock, textPart);
    }

    private static void AddPartToTextBlock(TextBlock textBlock, string part)
    {
        textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.Light});
    }

    private static void AddHighlightedPartToTextBlock(TextBlock textBlock, string part)
    {
        textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.ExtraBold});
    }


    public static List<string> SplitTextIntoTermAndNotTermParts(string text, string term)
    {
        if (text.IsNullOrEmpty())
            return new List<string>() {string.Empty};

        return Regex.Split(text, $@"({Regex.Escape(term)})")
                    .Where(p => p != string.Empty)
                    .ToList();
    }
}

I took dthrasers answer and took out the need for an XML parser. He does a great job explaining each of the pieces in his blog, However this didn't require me to add any extra libraries, here's how I did it.

Step one, make a converter class:

class StringToXamlConverter : IValueConverter
{

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string input = value as string;
        if (input != null)
        {
            var textBlock = new TextBlock();
            textBlock.TextWrapping = TextWrapping.Wrap;
            string escapedXml = SecurityElement.Escape(input);

            while (escapedXml.IndexOf("|~S~|") != -1) {
            //up to |~S~| is normal
            textBlock.Inlines.Add(new Run(escapedXml.Substring(0, escapedXml.IndexOf("|~S~|"))));
            //between |~S~| and |~E~| is highlighted
            textBlock.Inlines.Add(new Run(escapedXml.Substring(escapedXml.IndexOf("|~S~|") + 5,
                                      escapedXml.IndexOf("|~E~|") - (escapedXml.IndexOf("|~S~|") + 5))) 
                                      { FontWeight = FontWeights.Bold, Background= Brushes.Yellow });
            //the rest of the string (after the |~E~|)
            escapedXml = escapedXml.Substring(escapedXml.IndexOf("|~E~|") + 5);
            }

            if (escapedXml.Length > 0)
            {
                textBlock.Inlines.Add(new Run(escapedXml));                      
            }
            return textBlock;
        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException("This converter cannot be used in two-way binding.");
    }

}

Step two: Instead of a TextBlock use a ContentBlock. Pass in the string (you would of used for your textBlock) to the content block, like so:

<ContentControl Margin="7,0,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Center"
                Content="{Binding Description, Converter={StaticResource CONVERTERS_StringToXaml}, Mode=OneTime}">
</ContentControl>

Step three: Make sure the text you pass includes |~S~| before and |~E~| after the text part you want to be highlighted. For example in this string "my text |~S~|is|~E~| good" the is will be highlighted in yellow.

Notes:
You can change the style in the run to determine what and how your text is highlighted
Make sure you add your Converter class to your namespace and resources. This might also require a rebuild to get working.