Click here to Skip to main content
Click here to Skip to main content

Getting control of your numbers

, 1 Apr 2011
Rate this:
Please Sign up or sign in to vote.
A simple mechanism to limit the input of data to a TextBox so that it only accepted the relevant numeric amount.

Today on Code Project, one of the regulars asked how to set up a textbox so that it only accepted a currency amount. He was concerned that there doesn’t seem to be a simple mechanism to limit the input of data so that it only accepted the relevant numeric amount. Well, this is a feature I recently added into Goldlight, so I thought I’d post it here, along with an explanation of how it works.

Basically, and this will come as no surprise to you, it’s an Attached Behavior that you associate to the TextBox. There are many numeric only behaviors out there, so this one goes a little bit further. First of all, if you want, you can limit it to integers by setting AllowDecimal to false. If you want to limit it to a set number of decimal places, set DecimalLimit to the number of decimal places. If you don’t want to allow the developer to use negative numbers, set AllowNegatives to false. It’s that simple, so the solution to the problem would be to add the behaviour to the TextBox like this:

<TextBox Text="{Binding Price}">
  <i:Interaction.Behaviors>
    <gl:NumericTextBoxBehavior AllowNegatives="False" />
  </i:Interaction.Behaviors>
</TextBox>

The full code to do this is shown below:

namespace Goldlight.Extensions.Behaviors
{
  using System.Windows.Controls;
  using System.Windows.Interactivity;
  using System.Windows.Input;
  using System.Text.RegularExpressions;
  using System.Windows;
  using System.Globalization;

  /// <span class="code-SummaryComment"><summary>
</span>  /// Apply this behavior to a TextBox to ensure that it only accepts numeric values.
  /// The property <span class="code-SummaryComment"><see cref="NumericTextBoxBehavior.AllowDecimal"/> controls whether or not
</span>  /// the input is an integer or not.
  /// <span class="code-SummaryComment"><para>
</span>  /// A common requirement is to constrain the number
  /// count that appears after the decimal place.
  /// Setting <span class="code-SummaryComment"><see cref="NumericTextBoxBehavior.DecimalLimit"/>
</span>  /// specifies how many numbers appear here.
  /// If this value is 0, no limit is applied.
  /// <span class="code-SummaryComment"></para>
</span>  /// <span class="code-SummaryComment"></summary>
</span>  /// <span class="code-SummaryComment"><remarks>
</span>  /// In the view, this behavior is attached in the following way:
  /// <span class="code-SummaryComment"><code>
</span>  /// <span class="code-SummaryComment"><TextBox Text="{Binding Price}">
</span>  ///   <span class="code-SummaryComment"><i:Interaction.Behaviors>
</span>  ///     <span class="code-SummaryComment"><gl:NumericTextBoxBehavior AllowDecimal="False" />
</span>  ///   <span class="code-SummaryComment"></i:Interaction.Behaviors>
</span>  /// <span class="code-SummaryComment"></TextBox>
</span>  /// <span class="code-SummaryComment"></code>
</span>  /// <span class="code-SummaryComment"><para>
</span>  /// Add references to System.Windows.Interactivity to the view to use
  /// this behavior.
  /// <span class="code-SummaryComment"></para>
</span>  /// <span class="code-SummaryComment"></remarks>
</span>  public partial class NumericTextBoxBehavior : Behavior<TextBox>
  {
    private bool _allowDecimal = true;
    private int _decimalLimit = 0;
    private bool _allowNegative = true;
    private string _pattern = string.Empty;

    /// <span class="code-SummaryComment"><summary>
</span>    /// Initialize a new instance of <span class="code-SummaryComment"><see cref="NumericTextBoxBehavior"/>.
</span>    /// <span class="code-SummaryComment"></summary>
</span>    public NumericTextBoxBehavior()
    {
      AllowDecimal = true;
      AllowNegatives = true;
      DecimalLimit = 0;
    }

    /// <span class="code-SummaryComment"><summary>
</span>    /// Get or set whether the input allows decimal characters.
    /// <span class="code-SummaryComment"></summary>
</span>    public bool AllowDecimal
    {
      get
      {
        return _allowDecimal;
      }
      set
      {
        if (_allowDecimal == value) return;
        _allowDecimal = value;
        SetText();
      }
    }
    /// <span class="code-SummaryComment"><summary>
</span>    /// Get or set the maximum number of values to appear after
    /// the decimal.
    /// <span class="code-SummaryComment"></summary>
</span>    /// <span class="code-SummaryComment"><remarks>
</span>    /// If DecimalLimit is 0, then no limit is applied.
    /// <span class="code-SummaryComment"></remarks>
</span>    public int DecimalLimit
    {
      get
      {
        return _decimalLimit;
      }
      set
      {
        if (_decimalLimit == value) return;
        _decimalLimit = value;
        SetText();
      }
    }
    /// <span class="code-SummaryComment"><summary>
</span>    /// Get or set whether negative numbers are allowed.
    /// <span class="code-SummaryComment"></summary>
</span>    public bool AllowNegatives
    {
      get
      {
        return _allowNegative;
      }
      set
      {
        if (_allowNegative == value) return;
        _allowNegative = value;
        SetText();
      }
    }

    #region Overrides
    protected override void OnAttached()
    {
      base.OnAttached();

      AssociatedObject.PreviewTextInput += 
        new TextCompositionEventHandler(AssociatedObject_PreviewTextInput);
#if !SILVERLIGHT
      DataObject.AddPastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }

    protected override void OnDetaching()
    {
      base.OnDetaching();
      AssociatedObject.PreviewTextInput -= 
        new TextCompositionEventHandler(AssociatedObject_PreviewTextInput);
#if !SILVERLIGHT
      DataObject.RemovePastingHandler(AssociatedObject, OnClipboardPaste);
#endif
    }
    #endregion

    #region Private methods
    private void SetText()
    {
      _pattern = string.Empty;
      GetRegularExpressionText();
    }

#if !SILVERLIGHT
    /// <span class="code-SummaryComment"><summary>
</span>    /// Handle paste operations into the textbox to ensure that the behavior
    /// is consistent with directly typing into the TextBox.
    /// <span class="code-SummaryComment"></summary>
</span>    /// <span class="code-SummaryComment"><param name="sender">The TextBox sender.</param>
</span>    /// <span class="code-SummaryComment"><param name="dopea">Paste event arguments.</param>
</span>    /// <span class="code-SummaryComment"><remarks>This operation is only available in WPF.</remarks>
</span>    private void OnClipboardPaste(object sender, DataObjectPastingEventArgs dopea)
    {
      string text = dopea.SourceDataObject.GetData(dopea.FormatToApply).ToString();

      if (!string.IsNullOrWhiteSpace(text) && !Validate(text))
        dopea.CancelCommand();
    }
#endif

    /// <span class="code-SummaryComment"><summary>
</span>    /// Preview the text input.
    /// <span class="code-SummaryComment"></summary>
</span>    /// <span class="code-SummaryComment"><param name="sender">The TextBox sender.</param>
</span>    /// <span class="code-SummaryComment"><param name="e">The composition event arguments.</param>
</span>    void AssociatedObject_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
      e.Handled = !Validate(e.Text);
    }

    /// <span class="code-SummaryComment"><summary>
</span>    /// Validate the contents of the textbox with the new content to see if it is
    /// valid.
    /// <span class="code-SummaryComment"></summary>
</span>    /// <span class="code-SummaryComment"><param name="value">The text to validate.</param>
</span>    /// <span class="code-SummaryComment"><returns>True if this is valid, false otherwise.</returns>
</span>    protected bool Validate(string value)
    {
      TextBox textBox = AssociatedObject;

      string pre = string.Empty;
      string post = string.Empty;

      if (!string.IsNullOrWhiteSpace(textBox.Text))
      {
        pre = textBox.Text.Substring(0, textBox.SelectionStart);
        post = textBox.Text.Substring(textBox.SelectionStart + 
               textBox.SelectionLength, textBox.Text.Length - 
               (textBox.SelectionStart + textBox.SelectionLength));
      }
      else
      {
        pre = textBox.Text.Substring(0, textBox.CaretIndex);
        post = textBox.Text.Substring(textBox.CaretIndex, 
               textBox.Text.Length - textBox.CaretIndex);
      }
      string test = string.Concat(pre, value, post);

      string pattern = GetRegularExpressionText();

      return new Regex(pattern).IsMatch(test);
    }

    private string GetRegularExpressionText()
    {
      if (!string.IsNullOrWhiteSpace(_pattern))
      {
        return _pattern;
      }
      _pattern = GetPatternText();
      return _pattern;
    }

    private string GetPatternText()
    {
      string pattern = string.Empty;
      string signPattern = "[{0}+]";

      // If the developer has chosen to allow negative numbers, the pattern will be [-+].
      // If the developer chooses not to allow negatives, the pattern is [+].
      if (AllowNegatives)
      {
        signPattern = string.Format(signPattern, "-");
      }
      else
      {
        signPattern = string.Format(signPattern, string.Empty);
      }

      // If the developer doesn't allow decimals, return the pattern.
      if (!AllowDecimal)
      {
        return string.Format(@"^({0}?)(\d*)$", signPattern);
      }

      // If the developer has chosen to apply a decimal limit, the pattern matches
      // on a
      if (DecimalLimit >

The clever thing is that this behavior doesn’t allow the user to paste an incorrect value in either – the paste operation is subject to the same rules as directly entering the value in the first place.

Anyway, I hope this behavior is as much use to you as it is to me.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Pete O'Hanlon
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.
 
I am not the Stig, but I do wish I had Lotus Tuned Suspension.
Follow on   Twitter   Google+

Comments and Discussions

 
BugDoes not work with UpdateSourceTrigger=PropertyChanged Pinmemberd_weyermann24-Jan-12 21:51 
GeneralMy vote of 5 Pinmembergardnerp3-May-11 3:40 
GeneralRe: My vote of 5 PinmvpPete O'Hanlon10-May-11 10:16 
QuestionPRE tags language attribute missing? PinmvpSandeep Mewara1-Apr-11 0:49 
AnswerRe: PRE tags language attribute missing? PinmvpPete O'Hanlon1-Apr-11 1:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 1 Apr 2011
Article Copyright 2011 by Pete O'Hanlon
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid