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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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
    }
}

6 Comments

  1. […] 也可以参考SearchableTextBlock写一个高亮的文本框,一了百了,但我希望通过这个有趣的功能多介绍几种知识。 […]

  2. world moving says:

    Moving Cost

    A WPF Searchable TextBlock Ccontrol with Highlighting | WPF

  3. 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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      #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.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      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

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

Leave a Reply