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;
public partial class NumericTextBoxBehavior : Behavior<TextBox>
{
private bool _allowDecimal = true;
private int _decimalLimit = 0;
private bool _allowNegative = true;
private string _pattern = string.Empty;
public NumericTextBoxBehavior()
{
AllowDecimal = true;
AllowNegatives = true;
DecimalLimit = 0;
}
public bool AllowDecimal
{
get
{
return _allowDecimal;
}
set
{
if (_allowDecimal == value) return;
_allowDecimal = value;
SetText();
}
}
public int DecimalLimit
{
get
{
return _decimalLimit;
}
set
{
if (_decimalLimit == value) return;
_decimalLimit = value;
SetText();
}
}
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
private void OnClipboardPaste(object sender, DataObjectPastingEventArgs dopea)
{
string text = dopea.SourceDataObject.GetData(dopea.FormatToApply).ToString();
if (!string.IsNullOrWhiteSpace(text) && !Validate(text))
dopea.CancelCommand();
}
#endif
void AssociatedObject_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !Validate(e.Text);
}
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 (AllowNegatives)
{
signPattern = string.Format(signPattern, "-");
}
else
{
signPattern = string.Format(signPattern, string.Empty);
}
if (!AllowDecimal)
{
return string.Format(@"^({0}?)(\d*)$", signPattern);
}
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.