Click here to Skip to main content
15,886,593 members
Articles / Desktop Programming / WPF

Getting control of your numbers

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
1 Apr 2011CPOL1 min read 14.3K   6   5
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:

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

The full code to do this is shown below:

C#
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)


Written By
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.

Comments and Discussions

 
BugDoes not work with UpdateSourceTrigger=PropertyChanged Pin
d_weyermann24-Jan-12 21:51
d_weyermann24-Jan-12 21:51 
GeneralMy vote of 5 Pin
gardnerp3-May-11 3:40
gardnerp3-May-11 3:40 
GeneralRe: My vote of 5 Pin
Pete O'Hanlon10-May-11 10:16
mvePete O'Hanlon10-May-11 10:16 
QuestionPRE tags language attribute missing? Pin
Sandeep Mewara1-Apr-11 0:49
mveSandeep Mewara1-Apr-11 0:49 
AnswerRe: PRE tags language attribute missing? Pin
Pete O'Hanlon1-Apr-11 1:30
mvePete 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.