Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

Masked TextBox in WPF (and Keyboard in WPF)

by Ioannis Panagopoulos

Last week, I was wondering whether it is easy to implement a Masked TextBox in WPF since WPF does not have any by default. It turns out that this quest is an interesting one and consists of discovering the MaskedTextProvider .NET class and finding out how to control the Keyboard and especially the Insert Key in WPF. So we will start with the following small example:

We have a simple TextBox Control that we want to mask and a simple Button Control that serves as an alternative place for the focus, in order to test the behavior of the TextBox when it looses it. We want to implement a Masked TextBox, therefore we create a new class named MyMaskedTextBox which inherits from TextBox:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Input;

namespace MaskedTextBox
{

    class MyMaskedTextBox:TextBox
    {

    }
}

All the mentioned assemblies will be needed so leave them there for now.

The System.ComponentModel.MaskedTextProvider class basically works as follows: We provide a mask (the same mask we used to apply in the WinForms MaskedTextBox) at the constructor of the object. We then submit characters or strings at specific positions of the mask and ask the provider whether they are ok to be there. We can also use the internal MaskTextProvider's string that stores the submitted charaters, in order to see whether the mask has been completed.

So, in our exaple, we create a public property for the developper to supply the mask and instantiate a MaskedTextProvider in its setter (that is whenever the user sets the mask, a new provider is instantiated):

        private MaskedTextProvider _mprovider=null;
        public string Mask
        {
            get
            {
                if (_mprovider != nullreturn _mprovider.Mask;
                else return "";
            }
            set
            {
                _mprovider = new MaskedTextProvider(value);
                this.Text = _mprovider.ToDisplayString();
            }
         }

We will simplify our lives a lot if we apply some constraints:

  • The Delete and BackSpace keys should be disabled when within the TextBox.
  • The Insert Key should be always off (always replace the previous value).
  • Space is speically handled  by the MaskedTextProvider and therefore we shoud give the user the ability to choose whether he/she wants this functionality.
  • We do not want to support Text selection within the TextBox.

To achieve the previous requirements, we add another property that holds the user's preference wrt the Space key and override the OnPreviewKeyDown event as follows:

        private bool _ignoreSpace = true;
        public bool IgnoreSpace
        {
            get { return _ignoreSpace; }
            set { _ignoreSpace = value; }
        }

        protected override void OnPreviewKeyDown(KeyEventArgs e)
        {
            if (this.SelectionLength > 1)
            {
                this.SelectionLength = 0;
                e.Handled = true;
            }
            if (e.Key==Key.Insert || 
                e.Key == Key.Delete || 
                e.Key==Key.Back || 
               (e.Key==Key.Space && _ignoreSpace))
            {
                e.Handled = true;
            }
            base.OnPreviewKeyDown(e);
         }

This basically says, that when a key is down and something is selected then just unselect it and ignore the key. Also, if the key is one of those mentioned in the list then ignore them also. But how do we make sure that Insert will be always off? (Replace Mode). Well, at first, whenever a TextBox Control gets the focus for the first time the Insert is always ON. So the first time the TextBox gets the focus, we need to set the Insert to OFF. Actually we want to simulate the Key-Press of the Insert Key. But we don't have the Windows Forms' SendKeys method anymore. Well, the equivalent method that does exactly that -simulates the pressing of a key- in WPF is as follows:

         private void PressKey(Key key)
        {
            KeyEventArgs eInsertBack = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, key);
            eInsertBack.RoutedEvent = KeyDownEvent;
            InputManager.Current.ProcessInput(eInsertBack);
         }

We use a Boolean property which is false upon initialization of the TextBox and becomes true after the first time the TextBox has got the focus. So we check this property and if it is false we "press" the insert key. In consecutive times that the TextBox will get the focus the Insert won't be pressed again:

        private bool _InsertIsON = false;
        protected override void OnGotFocus(RoutedEventArgs e)
        {
            base.OnGotFocus(e);
            if (!_InsertIsON)
            {
                PressKey(Key.Insert);
                _InsertIsON = true;
            }
         }

Having done that, we can now work with the mask. We want to achieve two things: First we want to Preview the user's input and set a property to true or false depending on whether the input will be accepted or not (this property may come handy to the developer who uses our control). Second we want to actually allow/prevent the insertion of the new character in the TextBox based on the mask. 

So, before the insertion of any text, we inform the developer (if he/she likes) that the inserted text will be valid/invalid. Therefore we add the following:

        private bool _NewTextIsOk = false;
        public bool NewTextIsOk
        {
            get { return _NewTextIsOk; }
            set { _NewTextIsOk = value; }
        }
        protected override void OnPreviewTextInput(TextCompositionEventArgs e)
        {
             System.ComponentModel.MaskedTextResultHint hint;
             int TestPosition;

             if (e.Text.Length == 1)
                 this._NewTextIsOk = _mprovider.VerifyChar(e.Text[0], this.CaretIndex, out hint);
             else
                 this._NewTextIsOk = _mprovider.VerifyString(e.Text, out TestPosition, out hint);
             
            base.OnPreviewTextInput(e);
         }

This says that according to the length of the submitted text (1 or greater than 1) call the provider to check if it is ok for this text to be inserted at the current position and set the property accordingly. The current position is given by the property this.CaretIndex.

When the text is about to be inserted:

        protected override void OnTextInput(System.Windows.Input.TextCompositionEventArgs e)
        {
            string PreviousText = this.Text;
            if (NewTextIsOk)
            {
                base.OnTextInput(e);
                if (_mprovider.VerifyString(this.Text) == falsethis.Text = PreviousText;
                while (!_mprovider.IsEditPosition(this.CaretIndex) && _mprovider.Length>this.CaretIndex) this.CaretIndex++;

            }
            else
                e.Handled = true;
         }

We have another verification which seems redundant. This verification occurs AFTER the text is inserted and may come handy if in another implementation we leave the Insert Key to ON and we want to make sure that the new shifted text in the TextBox is still valid. We also move the caret until we reach the end of the mask or the caret position is in an editable character.

Finally we may want to prevent the control from losing the focus until the mask is full with valid data. So:

        public bool StayInFocusUntilValid
        {
            get { return _stayInFocusUntilValid; }
            set { _stayInFocusUntilValid = value; }
        }
        protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
        {
            if (StayInFocusUntilValid)
            {
                _mprovider.Clear();
                _mprovider.Add(this.Text);
                if (!_mprovider.MaskFull) e.Handled = true;
            }
            
            base.OnPreviewLostKeyboardFocus(e);
         }

Having done all those we can now use the new control as follows:

<Window x:Class="MaskedTextBox.Window1"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="clr-namespace:MaskedTextBox"

Title="Window1" Height="126" Width="256">

<StackPanel Orientation="Horizontal" Width="228">

<local:MyMaskedTextBox x:Name="MaskedDemo" Mask="(LLL)00/00/0000" StayInFocusUntilValid="True" IgnoreSpace="True" Width="118" Height="26" Margin="20"/>

<Button Content="OK" Height="24" Width="50"/>

</StackPanel>

</Window>


And the result is:

You can download the demo project here: MaskedTextBox.zip (53,22 kb)

kick it on DotNetKicks.com

blog comments powered by Disqus
hire me