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: />