A WPF Searchable TextBlock Control with Highlighting

So I needed a TextBlock that was searchable and from searching online, it seems a lot of people need one too. So I decided to inherit TextBlock and write a SearchableTextBox. It is really easy to use.

WPF SearchTextBlock Example Project

Here is an example application you can download or clone: WpfSharp.Controls

SearchableTextBlock Explained

Here are the steps for creating this SearchableTextBlock

  1. Hide the Text dependency property by making it private. I did this because TextBlock doesn’t have a TextChanged event.
  2. Create a public HighlightableText dependency property that wraps the Text property. You can now bind to HighlightableText.
  3. Add a dependency property each for HighlightForeground and HighlightBackground.
  4. Added a list of searchable words as a dependency property and some code to turn the word list into a regular expression.
  5. Add a new set to the Text property so that it enters the string value as Run objects and adds the highlighting.

Here is the object for you to browse.

using System;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Collections.Generic;

namespace HighlightText
{
    public class SearchableTextBlock : TextBlock
    {
        #region Constructors
        // Summary:
        //     Initializes a new instance of the System.Windows.Controls.TextBlock class.
        public SearchableTextBlock()
        {
            //Binding binding = new Binding("HighlightableText");
            //binding.Source = this;
            //binding.Mode = BindingMode.TwoWay;
            //SetBinding(TextProperty, binding);
        }

        public SearchableTextBlock(Inline inline)
            : base(inline)
        {
        }
        #endregion

        #region Properties
        new private string Text
        {
            set
            {
                if (string.IsNullOrWhiteSpace(RegularExpression) || !IsValidRegex(RegularExpression))
                {
                    base.Text = value;
                    return;
                }

                Inlines.Clear();
                string[] split = Regex.Split(value, RegularExpression, RegexOptions.IgnoreCase);
                foreach (var str in split)
                {
                    Run run = new Run(str);
                    if (Regex.IsMatch(str, RegularExpression, RegexOptions.IgnoreCase))
                    {
                        run.Background = HighlightBackground;
                        run.Foreground = HighlightForeground;
                    }
                    Inlines.Add(run);
                }
            }
        }

        public string RegularExpression
        {
            get { return _RegularExpression; }
            set
            {
                _RegularExpression = value;
                Text = base.Text;
            }
        } private string _RegularExpression;

        #endregion

        #region Dependency Properties

        #region Search Words
        public List SearchWords
        {
            get
            {
                if (null == (List)GetValue(SearchWordsProperty))
                    SetValue(SearchWordsProperty, new List());
                return (List)GetValue(SearchWordsProperty);
            }
            set
            {
                SetValue(SearchWordsProperty, value);
                UpdateRegex();
            }
        }

        // Using a DependencyProperty as the backing store for SearchStringList.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SearchWordsProperty =
            DependencyProperty.Register("SearchWords", typeof(List), typeof(SearchableTextBlock), new PropertyMetadata(new PropertyChangedCallback(SearchWordsPropertyChanged)));

        public static void SearchWordsPropertyChanged(DependencyObject inDO, DependencyPropertyChangedEventArgs inArgs)
        {
            SearchableTextBlock stb = inDO as SearchableTextBlock;
            if (stb == null)
                return;

            stb.UpdateRegex();
        }
        #endregion

        #region HighlightableText
        public event EventHandler OnHighlightableTextChanged;

        public string HighlightableText
        {
            get { return (string)GetValue(HighlightableTextProperty); }
            set { SetValue(HighlightableTextProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HighlightableText.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HighlightableTextProperty =
            DependencyProperty.Register("HighlightableText", typeof(string), typeof(SearchableTextBlock), new PropertyMetadata(new PropertyChangedCallback(HighlightableTextChanged)));

        public static void HighlightableTextChanged(DependencyObject inDO, DependencyPropertyChangedEventArgs inArgs)
        {
            SearchableTextBlock stb = inDO as SearchableTextBlock;
            stb.Text = stb.HighlightableText;

            // Raise the event by using the () operator.
            if (stb.OnHighlightableTextChanged != null)
                stb.OnHighlightableTextChanged(stb, null);
        }
        #endregion

        #region HighlightForeground
        public event EventHandler OnHighlightForegroundChanged;

        public Brush HighlightForeground
        {
            get
            {
                if ((Brush)GetValue(HighlightForegroundProperty) == null)
                    SetValue(HighlightForegroundProperty, Brushes.Black);
                return (Brush)GetValue(HighlightForegroundProperty);
            }
            set { SetValue(HighlightForegroundProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HighlightForeground.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HighlightForegroundProperty =
            DependencyProperty.Register("HighlightForeground", typeof(Brush), typeof(SearchableTextBlock), new PropertyMetadata(new PropertyChangedCallback(HighlightableForegroundChanged)));

        public static void HighlightableForegroundChanged(DependencyObject inDO, DependencyPropertyChangedEventArgs inArgs)
        {
            SearchableTextBlock stb = inDO as SearchableTextBlock;
            // Raise the event by using the () operator.
            if (stb.OnHighlightForegroundChanged != null)
                stb.OnHighlightForegroundChanged(stb, null);
        }
        #endregion

        #region HighlightBackground
        public event EventHandler OnHighlightBackgroundChanged;

        public Brush HighlightBackground
        {
            get
            {
                if ((Brush)GetValue(HighlightBackgroundProperty) == null)
                    SetValue(HighlightBackgroundProperty, Brushes.Yellow);
                return (Brush)GetValue(HighlightBackgroundProperty);
            }
            set { SetValue(HighlightBackgroundProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HighlightBackground.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HighlightBackgroundProperty =
            DependencyProperty.Register("HighlightBackground", typeof(Brush), typeof(SearchableTextBlock), new PropertyMetadata(new PropertyChangedCallback(HighlightableBackgroundChanged)));

        public static void HighlightableBackgroundChanged(DependencyObject inDO, DependencyPropertyChangedEventArgs inArgs)
        {
            SearchableTextBlock stb = inDO as SearchableTextBlock;
            // Raise the event by using the () operator.
            if (stb.OnHighlightBackgroundChanged != null)
                stb.OnHighlightBackgroundChanged(stb, null);
        }
        #endregion

        #endregion

        #region Methods
        public void AddSearchString(String inString)
        {
            SearchWords.Add(inString);
            Update();
        }

        public void Update()
        {
            UpdateRegex();
        }

        public void RefreshHighlightedText()
        {
            Text = base.Text;
        }

        private void UpdateRegex()
        {
            string newRegularExpression = string.Empty;
            foreach (string s in SearchWords)
            {
                if (newRegularExpression.Length > 0)
                    newRegularExpression += "|";
                newRegularExpression += RegexWrap(s);
            }

            if (RegularExpression != newRegularExpression)
                RegularExpression = newRegularExpression;
        }

        public bool IsValidRegex(string inRegex)
        {
            if (string.IsNullOrEmpty(inRegex))
                return false;

            try
            {
                Regex.Match("", inRegex);
            }
            catch (ArgumentException)
            {
                return false;
            }

            return true;
        }

        private string RegexWrap(string inString)
        {
            // Use positive look ahead and positive look behind tags
            // so the break is before and after each word, so the
            // actual word is not removed by Regex.Split()
            return String.Format("(?={0})|(?<={0})", inString);
        }
        #endregion
    }
}

4 Comments

  1. Juergen says:

    Hallo developer,
    thanks for this usefull Control implementation. It works very fine. I have used it in a Datatemplate of a Datagrid.

    Now i want to use a ‘case sensitive’ search.
    For that i have added a new Dependency Property bool “IsMatchCase” in that Control.

    I put this in:

     public bool IsValidRegex(string inRegex)
     {
         if (string.IsNullOrEmpty(inRegex))
             return false;
         
         //new:check Searchmode
         RegexOptions options = !IsMatchCase ? RegexOptions.IgnoreCase : RegexOptions.None;
    
         try
         {
           Regex.Match("", inRegex, options);
         }
         catch (ArgumentException)
         {
           return false;
         }
    
         return true;
      }
    

    But it matched always insensitive.

    What works wrong?

    Greetings
    Jürgen

    • Rhyous says:

      You wouldn’t do it in the IsValidRegex method. The regex is going to be correct or not regardless of the options.

      You would create Bool dependency property for whether the SearchTextBlock is IgnoreCase or not, which you have done, I see, but you should really name it IgnoreCase not IsMatchCase, but that is semantics.

              #region IgnoreCase
              public event EventHandler OnIgnoreCaseChanged;
      
              public bool IgnoreCase
              {
                  get { return (bool)GetValue(IgnoreCaseProperty); }
                  set { SetValue(IgnoreCaseProperty, value); }
              }
              public static readonly DependencyProperty IgnoreCaseProperty =
                  DependencyProperty.Register("IgnoreCase", typeof(bool), typeof(SearchableTextBlock), new PropertyMetadata(new PropertyChangedCallback(IgnoreCaseChanged)));
      
      
              public static void IgnoreCaseChanged(DependencyObject inDO, DependencyPropertyChangedEventArgs inArgs)
              {
                  SearchableTextBlock stb = inDO as SearchableTextBlock;
                  stb.OnIgnoreCaseChanged?.Invoke(stb, null);
              }
              #endregion
      

      You then need to make the change in the “Text” properties set. Lines 42 and 46.

              new private string Text
              {
                  set
                  {
                      if (string.IsNullOrWhiteSpace(RegularExpression) || !IsValidRegex(RegularExpression))
                      {
                          base.Text = value;
                          return;
                      }
      
                      Inlines.Clear();
                      string[] split = Regex.Split(value, RegularExpression, IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None); //<--- Here use the IgnoreCase property
                      foreach (var str in split)
                      {
                          Run run = new Run(str);
                          if (Regex.IsMatch(str, RegularExpression, IgnoreCase ? RegexOptions.IgnoreCase : RegexOptions.None)) //<--- Here use the IgnoreCase property
                          {
                              run.Background = HighlightBackground;
                              run.Foreground = HighlightForeground;
                          }
                          Inlines.Add(run);
                      }
                  }
              }
      

      You could make the DependencyProperty of type RegexOptions instead of bool, and then you support all RegexOptions. 🙂

    • Rhyous says:

      Even better, I updated the solution for you. I’ve been meaning to start a WpfSharp.Controls project on GitHub. Now is as good of a time as any.
      https://github.com/rhyous/WpfSharp.Controls

  2. […] A WPF Searchable TextBlock Control with Highlighting Category: WPF  |  Comment (RSS)  |  Trackback […]

Leave a Reply

*