Custom Filter for a WPF TextBox

So, I had a need to implement a TextBox in WPF with auto filtering functionality. For example the initial requests were to limit a TextBox to numeric characters only. This was simple enough. Handle the PreviewTextInput Event and only allow numeric characters to be entered. So my first, simplistic, hack version was simply this:

   1: 'handle numeric only textboxes
   2: Private Sub TextBox_PreviewTextInput(ByVal sender As Object, ByVal e As System.Windows.Input.TextCompositionEventArgs) Handles Me.PreviewTextInput
   3:    If NumericValuesOnly AndAlso Not IsNumeric(e.Text) Then
   4:        e.Handled = True
   5:    End If
   6: End Sub

 

Of course, the next request was for a non-numeric TextBox that allowed any character except numerical. So, I could just invert the logic I have shown above. Or, based on a good recommendation from a fellow developer, I could take a few minutes and make the filtering functionality “real” by having a Regular Expression being used for all filtering under the hood and then allow custom Regular Expressions to support those “one-off” situations that will inevitably come up.

So, I added some properties and fields to support the new functionality. A CustomTextFilterRegexPattern String to allow custom filtering. A mode flag to allow the RegEx to function as an “Allow” filter or a “Deny” filter.

   1: Private mTextFilteringRegEx As Regex
   2: Private Shared sNumericRegex As New Regex("[0-9]")
   3:  
   4: Private mCustomTextFilterRegexExpression As String
   5: Public Property CustomTextFilterRegexExpression() As String
   6:   Get
   7:       Return mCustomTextFilterRegexExpression
   8:   End Get
   9:   Set(ByVal value As String)
  10:       mCustomTextFilterRegexExpression = value
  11:   End Set
  12: End Property
  13:  
  14: Private mIsFilterInAllowMode As Boolean
  15: Public Property IsFilterInAllowMode() As Boolean
  16:   Get
  17:       Return mIsFilterInAllowMode
  18:   End Get
  19:   Set(ByVal value As Boolean)
  20:       mIsFilterInAllowMode = value
  21:   End Set
  22: End Property

Then I added two properties to make defining built-in filters easier.

   1: Private mNumericValuesOnly As Boolean
   2: Public Property NumericValuesOnly() As Boolean
   3:     Get
   4:         Return mNumericValuesOnly
   5:     End Get
   6:     Set(ByVal Value As Boolean)
   7:         mNumericValuesOnly = Value
   8:     End Set
   9: End Property
  10:  
  11: Private mNonNumericValuesOnly As Boolean
  12: Public Property NonNumericValuesOnly() As Boolean
  13:     Get
  14:         Return mNonNumericValuesOnly
  15:     End Get
  16:     Set(ByVal value As Boolean)
  17:         mNonNumericValuesOnly = value
  18:     End Set
  19: End Property

Now in the Initialized Handler I wire up to the Paste Command and also set up my internal Regex to match what options have been defined in the XAML.

   1: Private Sub Textbox_Initialized(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Initialized
   2:     Dim pasteBinding As New CommandBinding(ApplicationCommands.Paste, _
   3:             New ExecutedRoutedEventHandler(AddressOf OnPasteCommandExecute))
   4:     Me.CommandBindings.Add(pasteBinding)
   5:  
   6:     'todo - add check to enforce mutually exclusive settings
   7:  
   8:     If NumericValuesOnly Then
   9:         mTextFilteringRegEx = sNumericRegex
  10:         IsFilterInAllowMode = True
  11:     End If
  12:  
  13:     If NonNumericValuesOnly Then
  14:         mTextFilteringRegEx = sNumericRegex
  15:         IsFilterInAllowMode = False
  16:     End If
  17:  
  18:     If Not String.IsNullOrEmpty(CustomTextFilterRegexExpression) Then
  19:         mTextFilteringRegEx = New Regex(CustomTextFilterRegexExpression)
  20:     End If
  21: End Sub

All that is left now is to handle PreviewTextInput and the Paste Command Execute Handler.

It was easy enough to deny characters in the PreviewTextInput, if the new value did not match the current filter setting then cancel the event by marking it Handled.

For the paste command, however, I needed to process an entire string. The negative filter was easy, any match of a “deny” filter would cancel the paste command. For the positive filter, how could I easily screen a string of unknown length for any characters that did not match the filter Regex. Since the filter was a character filter, would I need to loop thru the entire string and check each value? Then I realized that using the Regex to do a Replace() and set the replace string to String.Empty() would only leave a non-empty string as the result if there were invalid characters in the string.

   1: Private Sub OnPasteCommandExecute(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)
   2:     Dim toExecutePaste As Boolean = True
   3:     If Not mCustomTextFilterRegexExpression Is Nothing Then
   4:         Dim pasteText As String = Clipboard.GetText()
   5:         If IsFilterInAllowMode Then
   6:             'try a replace, if any characters survive the replace then the string is invalid
   7:             If Not String.IsNullOrEmpty(mTextFilteringRegEx.Replace(pasteText, String.Empty)) Then
   8:                 toExecutePaste = False
   9:             End If
  10:         Else
  11:             'any match on the deny mode filter equals a cancel of the paste
  12:             If mTextFilteringRegEx.IsMatch(pasteText) Then
  13:                 toExecutePaste = False
  14:             End If
  15:         End If
  16:  
  17:     End If
  18:  
  19:     If toExecutePaste Then Me.Paste()    
  20:     e.Handled = True
  21: End Sub
  22:  
  23: Private Sub Textbox_PreviewTextInput(ByVal sender As Object, ByVal e As System.Windows.Input.TextCompositionEventArgs) Handles Me.PreviewTextInput
  24:    'If filtering is applied and we fail the regex then cancel event
  25:    If Not mTextFilteringRegEx Is Nothing Then
  26:        If mTextFilteringRegEx.IsMatch(e.Text) = Not IsFilterInAllowMode Then
  27:            e.Handled = True
  28:        End If
  29:    End If
  30: End Sub

 

So now I can implement a TextBox with custom filtering like this:

   1: <myFramework:CustomTextBox  UIProperty="{Binding Path=IntegerValueUIProperty}"
   2:                                     CustomTextFilterRegexExpression="[4,5,6,x,y]"
   3:                                     IsFilterInAllowMode="True"
   4:                                              />

No Comments