Creating a Numbers only TextBox

So this isn’t a major problem, but in an app like Shoppers Calculator, there are various places where numbers get entered, and a lot of repeat code to validate entry, or limit the user to a certain number of decimal places, or even just to stop them pressing the decimal key twice.  It makes sense, therefore, to wrap all that up into a re-usable control. So here’s what it ought to do:

  • Force the number keypad
  • Ensure that only one decimal place can be entered
  • Allow the developer to restrict the length or the number,
  • Check that if you try to paste text in that’s not a number, it doesn’t allow it.

You can get the source code and a dll containing the control from here.  Compatibility is with both Windows Phone 7 and Windows Phone 8.

So getting started then, we don’t want to create this from scratch so add a new class, name it and have it inherit from System.Windows.Controls.TextBox. In the constructor, we will set the InputScope to satisfy the first requirement.

public class NumberTextBox: TextBox
{
public NumberTextBox()
: base()
{
  //Set the input scope - this forces the Numberic Keypad. You can override it by setting the property in XAML, but shouldn't.
  this.InputScope = new InputScope { Names = { new InputScopeName() { NameValue = InputScopeNameValue.Number } } };
}

Next, create dependency properties for any extra’s you want the developer to control through XAML. In my case, that’s properties for the max number of whole digits, and the max number of decimal places.  If you don’t know much about dependency properties, take a read through the WPFTutorial.net article on Dependency Properties before continuing.

///
/// The number of digits allowed before any decimal place. -1 = infinate. 0 or greater limits it. Default: -1;
///
public static readonly DependencyProperty WholeNumbersProperty = DependencyProperty.Register("WholeNumbers", typeof(int), typeof(NumberTextBox), null); 
public int WholeNumbers 
{ 
   get { return (int)GetValue(WholeNumbersProperty); } 
   set { SetValue(WholeNumbersProperty, value); } 
}
 ///
/// The number of digits allowed after any decimal place. -1 = infinite, 0 blocks decimal point and numbers after that. 
/// Greater than 0 caps decimals to the given max. Default: -1 
///
public static readonly DependencyProperty DecimalPlacesProperty = DependencyProperty.Register("DecimalPlaces", typeof(int), typeof(NumberTextBox), null); 
public int DecimalPlaces 
{ 
  get { return (int)GetValue(DecimalPlacesProperty); } 
  set { SetValue(DecimalPlacesProperty, value); } 
}

///
/// Should a message be displayed when pasting invalid text? Default: true 
///
public static readonly DependencyProperty ShowMessageProperty = DependencyProperty.Register("ShowMessage", typeof(bool), typeof(NumberTextBox), null); 
public bool ShowMessage 
{
  get { return (bool)GetValue(ShowMessageProperty); } 
  set { SetValue(ShowMessageProperty, value); } 
} 

///
/// Allow number rules for WHole NUmber and Decimal Places to be broken. 
/// Useful if you want to show validation error rather than just not allowing the number entry at all. If you use this, remember to bind to the TextValidationFailed event. 
/// Default: false 
///
public static readonly DependencyProperty BreakingNumberRulesIsAllowedProperty = DependencyProperty.Register("BreakingNumberRulesIsAllowed", typeof(bool), typeof(NumberTextBox), null); 
public bool BreakingNumberRulesIsAllowed 
{ 
  get { return (bool)GetValue(BreakingNumberRulesIsAllowedProperty); } 
  set { SetValue(BreakingNumberRulesIsAllowedProperty, value); } 
}

To check for valid text entry, we need to override the OnKeyDown event. Here we check for adding multiple decimal points, the maximum number of decimals being reached (“Separator” in the snippet below hold a string representing the localized decimal point e.g in France it would be “,”). To check for maximum number of whole digits being exceeded, we need a check in OnKeyDown and in the TextChanged even handler (this is not an override, we bind to the even in our constructor. This is because the digit being added could be added in the middle of the text.

 //Check for various error conditions on text entry - these tests block the character pressed from being included in the text.
protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
{
    IsPasted = false; //This is used in the TextChanged handler
    //Check for multiple decimal points
    if (e.Key == System.Windows.Input.Key.Unknown)
    {
       if (Text.Contains(Separator))
       {                    
           e.Handled = true;
       }
       return;
    }
    //Check for max decimal places reached
    if (!e.Handled)
    {
       if (Text.Contains(Separator) && DecimalPlaces > 0)
       {
           string check = Text.Substring(Text.IndexOf(Separator));
           if (check.Length > DecimalPlaces)
           {
               e.Handled = true;                        
           }
        }
   }
   //Check for max whole numbers reached if decimals are blocked
   if (WholeNumbers > 0 && !Text.Contains(Separator))
   {
       if (Text.Length >= WholeNumbers)
       {                    
          e.Handled = true;
          return;                    
       }
   }
   base.OnKeyDown(e);
}

Lastly, we need to handle copy/past and make sure text being pasted into the box is a number. To do this, we put a flag in the OnKeyDown event handler which is checked in the TextChanged event – this tells us if a key was pressed on the keyboard or not to get the number in there – or, if it was copied (i.e no key was pressed.  In TextChanged, if this flag has not been changed, we have the same checks there that would be done in the OnKeyDown method.

//Check for more errors on text entry that can't be immdiatley covered by OnKeyDown
//these tests revert the text back to how it was before OnKeyDown
void NumberTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
   //IsPasted defaults true, and is set false by OnKeyDown
   if (IsPasted)
   {
      //check pasted text represents a number (as long as the inputscope is number, this should only occur for pasted text)
      decimal d = 0;
      if (this.Text.Length > 0 && (Text != Separator || (Text == Separator && DecimalPlaces == 0)))
      {
         //Check for multiple decimal points
         if (Text.Count(x => x == Separator.First()) > 1)
         {
             this.Text = _text; 
             return;
         }

         //Check it's a number
         if (!decimal.TryParse(this.Text, out d))
         {
             this.Text = _text; 
             return;
         }

         //Check for max decimal places reached                    
         if (Text.Contains(Separator) && DecimalPlaces > 0)
         {
             string check = Text.Substring(Text.IndexOf(Separator));
             if (check.Length > DecimalPlaces)
             {
                this.Text = _text; 
                return;
             }
        }
        //Check for max whole numbers reached if decimals are blocked
        if (WholeNumbers > 0 && !Text.Contains(Separator))
        {
           if (Text.Length >= WholeNumbers)
           {
              this.Text = _text; 
              return;
           }
        }

        //check that text fits for whole numbers with decimals
        if (WholeNumbers > 0 && !(Text.Length > 0 && Text.Last() == Separator.First()))
        {
            string wholeCheck = Text.Substring(0, Text.Contains(Separator) ? Text.IndexOf(Separator) : Text.Length);
            if (wholeCheck.Length > WholeNumbers)
            {
                this.Text = _text; 
                return;
            }
        }                    
     }
  }
  else
  {
     //check that text fits for whole numbers with decimals
     if (WholeNumbers > 0 && !(Text.Length > 0 && Text.Last() == Separator.First()))
     {
         string wholeCheck = Text.Substring(0, Text.Contains(Separator) ? Text.IndexOf(Separator) : Text.Length);
         if (wholeCheck.Length > WholeNumbers)
         {
             this.Text = _text; 
             return;
         }
     }  

     //Reset IsPasted for next input
     IsPasted = true;
  }
  _text = this.Text;
}

Usage

To use it, add a reference at the top of the XAML page, to the library /namespace the control is in. Then, when you want to create one, use the following tag (or drag one on screen.

<UserControl x:Class="ExampleProject.ExampleUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    ...
    xmlns:mnd="clr-namespace:Bluechris.Controls.Phone;assembly=Bluechris.Controls.Phone"             
    ...
    d:DesignHeight="450" d:DesignWidth="480">

    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" TextWrapping="Wrap" x:Name="Instructions" Text="Enter a number here:"/>
        <mnd:NumberTextBox x:Name="Value" Grid.Row="1" Width="150"
                 DecimalPlaces="2" 
                 WholeNumbers="6" />          
    </Grid>
</UserControl>

So in this one, I’ve added a max on the number of digits, and the decimal places.  If I try to type more than that, it just doesn’t let me.  If I copy/paste in some text that isn’t valid, it does nothing. This is great, but may not be that helpful to the user.  In NumberTextBox, I have added a message option, and an event option that can fire when validation fails.  The validation failed event has a custom EventArgs which tells us why it failed, so a message can be displayed to the user e.g “Sorry, you can’t paste that here – you need a number with only 2 decimal places, and the number you pasted had 6”.

Option 1:

<mnd:NumberTextBox x:Name="Value" Grid.Row="1" Width="150"
                 DecimalPlaces="2" 
                 WholeNumbers="6"
                 ShowMessage="true"/>

Option 2 – XAML:

<mnd:NumberTextBox x:Name="Value" Grid.Row="1" Width="150"
                 DecimalPlaces="2" 
                 WholeNumbers="6"
                 ShowMessage="false"
                 TextValidationFailed="Value_ValidationFailed"/>

Option 2 – Code Behind:

private void Value_ValidationFailed(object sender, NumberTextBoxMessageEventArgs e)
{
    switch (e.ErrorType)
    {
        case NumberTextBoxError.DoubleDecimal:
	   MessageBox.Show("Only 1 decimal place is allowed");
	   break;
	case NumberTextBoxError.TooManyDecimalPlaces:
	   MessageBox.Show("The maximum number of decimal places is" + (sender as NumberTextBox).DecimalPlaces);
	   break;
       ....
    }
}

So there you go, a reusable number text box. Hope this has been helpful.

Advertisements

About bluechrism

I am a software developer with most professional experience in the Windows .Net realm and I'm currently a WPF developer with Starkey Labs. However, I have wanted for some time to start the mobile developer journey properly and being an N900 owner, this was to be in the realm of QT. Job hunting, moving to Minnesota and changing jobs put my plans on hold 6-12 months but things are starting to settle now, just as I'm getting sorted to start some things, Microsoft and Nokia merge. This blog is about my novice mobile development experiences and hopefully will end up complete with links to download some apps on various platforms, but obviously by the name, Sybian, Maemo/Meego and Windows Mobile. In other stuff, I am English, I support Everton FC, I have visited Glastonbury music festival 5 times and recommend it to anyone. I am married and my wife and i have a dog called Friday.
This entry was posted in Development, How To, Windows Phone and tagged , , , , , , . Bookmark the permalink.

One Response to Creating a Numbers only TextBox

  1. Pingback: Welcome in http://vbcafe.net/ » Creating a Number TextBox for Windows Phone

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s