RecentComments

Comment RSS

Masked TextBox in WPF (and Keyboard in WPF)

by Ioannis 4. February 2009 18:26

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

Tags:

WPF

Comments

2/4/2009 5:28:37 PM #

trackback

Trackback from DotNetKicks.com

Masked TextBox in WPF (and Keyboard in WPF)

DotNetKicks.com

2/4/2009 5:33:06 PM #

trackback

Trackback from DotNetShoutout

C# and .NET Tips and Tricks | Masked TextBox in WPF (and Keyboard in WPF)

DotNetShoutout

2/14/2009 4:15:09 PM #

Mikael Bager

Hi,
Nice code. However, it won't work, when the textbox is databound.

Any suggestions?

Mikael

Mikael Bager Denmark

2/15/2009 10:51:06 AM #

Admin

Hi Mikael,
thanks for the comment. I assume when you say the textbox is databound you mean its Text Property is databound.

The TextBox works whent its Text Property is databound as long as the initial value that comes from the binding is valid for the mask.

If the property your are binding is not in the form of the mask I would recommend considering Binding.Converters (msdn.microsoft.com/.../...a.ivalueconverter.aspx).

You can also use them to filter out bad initial values if you like.

Ioannis

Admin

12/24/2009 8:22:42 AM #

Chris

Great example, thanks.  Quick question, is there a way to get the input prompt out of the text property if the user doesn’t enter the whole thing?

I have a mask of 999.999 which is displayed to the user as ___.___ .  Now if the user only enters text after the decimal point I have the following ___.012 .  If I try double.parse on the text property of that control it sees the input characters as well.

I thought it might be a problem with my mask value so I tried ###.### as well, but no luck.

Any suggestions?

Chris United States

12/24/2009 2:05:34 PM #

Admin

Chris hi,  thanks for reading my post. I think a possible solution is to add in the textbox code the following:

protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
this.Text = MethodThatReturnsTheStringWithoutTheMask();
base.OnPreviewLostKeyboardFocus(e);
}

This will change the Text to the desired clean text without the mask and you will be able to use the parse the value.

Admin Greece

6/13/2010 11:28:26 PM #

Feri

Hi,

I have tried your solution. I liked it very much, but I had problems with it.
When I binded a number to it (with a correct mask) it displayed correctly, but I could not change the value of it. So the textbox lost its main essence: using as input field. Is it a known problem? Do you know the solution? Can you provide me an example?

Br,
Feri

Feri Hungary

6/25/2010 6:05:13 AM #

Adrian McGrath

This is a good example, but I don't understand why you disable the Delete and BackSpace keys?

What problems / side effects are you guarding yourself against?

Adrian

Adrian McGrath Australia

6/27/2010 9:20:17 AM #

Admin

Adrian hi, thanks for your comment. The Delete/Backspace keys are disabled since I was not sure about what should happen when the user presses them. If the user presses for example the "Delete" key the text on the right of the cursor should move one position to the left. But this could lead to a text which is not valid for the mask. I could alter the behavior so the text will not move to the left and just clear the cursor's position but I think the best solution (in terms of usability) was to disable the functionality altogether.  

Admin Greece

6/27/2010 9:24:46 AM #

Admin

Feri hi, thanks for reading my post. I don't think there is a problem with the numbers. Does the number have decimal places? If yes check if your decimal separator is correct in the mask.

Admin Greece

Add comment


(Will show your Gravatar icon)

Enter the word
CAPTCHA word
Add 1 to the number above


  Country flag

biuquote
  • Comment
  • Preview
Loading



Powered by BlogEngine.NET 1.5.0.7

Programming Blogs - BlogCatalog Blog Directory Add to Technorati Favorites

MVP Award

Ioannis Panagopoulos





This blog is using BlogEngine.Net and is hosted in the hoster below. I have not experienced any problems installing BlogEngine.Net in the host and I am satisfied with the host's response times. Therefore I recommend it.


DiscountASP Add