Click here to Skip to main content
13,189,884 members (50,062 online)
Click here to Skip to main content
Add your own
alternative version

Stats

9.8K views
6 bookmarked
Posted 1 Apr 2011

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;

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

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

    /// <summary>
    /// Get or set whether the input allows decimal characters.
    /// </summary>
    public bool AllowDecimal
    {
      get
      {
        return _allowDecimal;
      }
      set
      {
        if (_allowDecimal == value) return;
        _allowDecimal = value;
        SetText();
      }
    }
    /// <summary>
    /// Get or set the maximum number of values to appear after
    /// the decimal.
    /// </summary>
    /// <remarks>
    /// If DecimalLimit is 0, then no limit is applied.
    /// </remarks>
    public int DecimalLimit
    {
      get
      {
        return _decimalLimit;
      }
      set
      {
        if (_decimalLimit == value) return;
        _decimalLimit = value;
        SetText();
      }
    }
    /// <summary>
    /// Get or set whether negative numbers are allowed.
    /// </summary>
    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
    /// <summary>
    /// Handle paste operations into the textbox to ensure that the behavior
    /// is consistent with directly typing into the TextBox.
    /// </summary>
    /// <param name="sender">The TextBox sender.</param>
    /// <param name="dopea">Paste event arguments.</param>
    /// <remarks>This operation is only available in WPF.</remarks>
    private void OnClipboardPaste(object sender, DataObjectPastingEventArgs dopea)
    {
      string text = dopea.SourceDataObject.GetData(dopea.FormatToApply).ToString();

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

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

    /// <summary>
    /// Validate the contents of the textbox with the new content to see if it is
    /// valid.
    /// </summary>
    /// <param name="value">The text to validate.</param>
    /// <returns>True if this is valid, false otherwise.</returns>
    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 > 0)
      {
        pattern = string.Format(@"^({2}?)(\d*)([{0}]?)(\d{{0,{1}}})$",
          NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator,
          DecimalLimit,
          signPattern);
      }
      else
      {
        pattern = string.Format(@"^({1}?)(\d*)([{0}]?)(\d*)$", 
                  NumberFormatInfo.CurrentInfo.CurrencyDecimalSeparator, signPattern);
      }

      return pattern;
    }
    #endregion
  }
}

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)

Share

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.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
BugDoes not work with UpdateSourceTrigger=PropertyChanged Pin
d_weyermann24-Jan-12 21:51
memberd_weyermann24-Jan-12 21:51 
GeneralMy vote of 5 Pin
gardnerp3-May-11 3:40
membergardnerp3-May-11 3:40 
GeneralRe: My vote of 5 Pin
Pete O'Hanlon10-May-11 10:16
mvpPete O'Hanlon10-May-11 10:16 
Sorry it's taken so long for me to reply. For some reason I'm not getting notified of messages.

Anyway, if you need a max/min value, feel free to add the range as a property - but, consider what happens if you have a range of 0 to 500 and the user types 60 then presses 0. As you'd expect, it shouldn't let them enter the value, but this could be a surprise to the user. It all depends on your users, and how they react to the range checking.

Forgive your enemies - it messes with their heads


My blog | My articles | MoXAML PowerToys | Mole 2010 - debugging made easier - my favourite utility


QuestionPRE tags language attribute missing? Pin
Sandeep Mewara1-Apr-11 0:49
mvpSandeep Mewara1-Apr-11 0:49 
AnswerRe: PRE tags language attribute missing? Pin
Pete O'Hanlon1-Apr-11 1:30
mvpPete O'Hanlon1-Apr-11 1:30 

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

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

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171016.2 | Last Updated 1 Apr 2011
Article Copyright 2011 by Pete O'Hanlon
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid