okay this one was fun, and is not only corrected as a north american phone number mask, but as a fully masked textbox for wpf - my hat goes off to matthew macdonald and his apress book "Pro WPF with VB 2008" - i used vb 2010 in this case and had the same results.
Here's what you do:
- right-click your project and select add, then class
- call the class MaskedTextBox.vb
- add the imports statement at the very top
Code:
Imports System.ComponentModel
- paste the following code between 'Public Class MaskedTextBox' and 'End Class'
Code:
Inherits System.Windows.Controls.TextBox
Public Shared maskproperty As DependencyProperty
Shared Sub New()
maskproperty = DependencyProperty.Register("Mask", GetType(String), GetType(MaskedTextBox), New FrameworkPropertyMetadata(AddressOf maskchanged))
Dim metadata As New FrameworkPropertyMetadata()
metadata.CoerceValueCallback = AddressOf coercetext
TextProperty.OverrideMetadata(GetType(MaskedTextBox), metadata)
End Sub
Private Shared Function coercetext(ByVal d As DependencyObject, ByVal value As Object)
Dim textbox As MaskedTextBox = CType(d, MaskedTextBox)
Dim maskprovider As New MaskedTextProvider(textbox.Mask)
maskprovider.Set(CStr(value))
Return maskprovider.ToDisplayString()
End Function
Public Property Mask() As String
Get
Return CStr(GetValue(maskproperty))
End Get
Set(ByVal value As String)
SetValue(maskproperty, value)
End Set
End Property
Private Function getmaskprovider() As MaskedTextProvider
Dim maskprovider As New MaskedTextProvider(Mask)
maskprovider.Set(Text)
Return maskprovider
End Function
Private Sub refreshtext(ByVal maskprovider As MaskedTextProvider, ByVal pos As Integer)
Me.Text = maskprovider.ToDisplayString()
Me.SelectionStart = pos
End Sub
Public ReadOnly Property maskcompleted() As Boolean
Get
Dim maskprovider As MaskedTextProvider = getmaskprovider()
Return maskprovider.MaskCompleted
End Get
End Property
Private Shared Sub maskchanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim textbox As MaskedTextBox = CType(d, MaskedTextBox)
d.CoerceValue(TextProperty)
Dim maskprovider As MaskedTextProvider = textbox.getmaskprovider()
textbox.refreshtext(maskprovider, 0)
End Sub
Private Function skiptoeditablecharacter(ByVal startpos As Integer) As Integer
Dim maskprovider As MaskedTextProvider = getmaskprovider()
Dim newpos As Integer = maskprovider.FindEditPositionFrom(startpos, True)
If newpos = -1 Then
Return startpos
Else
Return newpos
End If
End Function
Protected Overrides Sub OnPreviewTextInput(ByVal e As System.Windows.Input.TextCompositionEventArgs)
Dim maskprovider As MaskedTextProvider = getmaskprovider()
Dim pos As Integer = Me.SelectionStart
'Adding a character
If pos < Me.Text.Length Then
pos = skiptoeditablecharacter(pos)
'Overwrite mode is on.
If Keyboard.IsKeyToggled(Key.Insert) Then
If maskprovider.Replace(e.Text, pos) Then
pos += 1
End If
' insert mode is on
Else
If maskprovider.InsertAt(e.Text, pos) Then
pos += 1
End If
End If
'find the new cursor position
pos = skiptoeditablecharacter(pos)
End If
refreshtext(maskprovider, pos)
e.Handled = True
MyBase.OnPreviewTextInput(e)
End Sub
Protected Overrides Sub OnPreviewKeyDown(ByVal e As System.Windows.Input.KeyEventArgs)
MyBase.OnKeyDown(e)
Dim maskprovider As MaskedTextProvider = getmaskprovider()
Dim pos As Integer = Me.SelectionStart
'deleteing a character (delete key)
' this does nothing if you try to delete a format character
If e.Key = Key.Delete AndAlso pos < (Me.Text.Length) Then
If maskprovider.RemoveAt(pos) Then
refreshtext(maskprovider, pos)
End If
e.Handled = True
'deleting a character (backspace)
' this steps over a format character, but doesn't delete the next character
ElseIf e.Key = Key.Back Then
If pos > 0 Then
pos -= 1
If maskprovider.RemoveAt(pos) Then
refreshtext(maskprovider, pos)
End If
End If
e.Handled = True
End If
End Sub
Public Sub New()
MyBase.New()
Dim commandbinding1 As New CommandBinding(ApplicationCommands.Paste, Nothing, AddressOf suppresscommand)
Me.CommandBindings.Add(commandbinding1)
Dim commandbinding2 As New CommandBinding(ApplicationCommands.Cut, Nothing, AddressOf suppresscommand)
Me.CommandBindings.Add(commandbinding2)
End Sub
Private Sub suppresscommand(ByVal sender As Object, ByVal e As CanExecuteRoutedEventArgs)
e.CanExecute = False
e.Handled = True
End Sub
- Build your project
you should now have MaskedTextBox in your toolbox - drag and drop it on to your window.
- under the properties of the control, open up the 'Other' subcategory (if it isn't already) and find the 'Mask' property
- enter your mask - in my case it was (999) 999-9999
- for other masks, use the following characters:
0 = required digit
9 = optional digit or space, if left blank, a space is inserted automatically
# = optional digit, space, or plus/minus symbol. if left blank, a space is inserted automatically
L = required ascii letter (a-z or A-Z)
? = optional ascii letter
& = required unicode character. allows anything that isn't a control key, including punctuation and symbols
C = optional unicode character
A = required alphanumeric character (allows letter or number but not punctuation or symbols)
a = optional alphanumeric character
. = decimal placeholder
, = thousands placeholder
: = time separator
/ = date separator
$ = currency symbol
< = all the characters that follow will be converted automatically to lowercase as the user types them. (there is no way to switch back to mixed-case entry mode once you use this character)
> = all the characters that follow will be converted automatically to uppercase as the user types them
\ = escapes a masked character, turning it in to a literal. thus, if you use \&, it is interpreted as the literal character &, which will be inserted in to the text box
All other characters = All other characters are treated as literals and are shown in the text box.
- as an example, an ip address mask would be 990.990.990.990
- a north american phone number would be (999) 999-9999
- etc etc etc
Hopefully this will save others the vast amount of time i had to spend on a trivial item like this, however, i'm glad its resolved because i'll be using this class a million times over i'm sure, or at least until wpf 5 (or silverlight 5) contains a native masked textbox..... :-)
thanks to TG and bflosabre91 for their input, it was invaluable in moving forward on this one.
Enjoy!!!