Skip to content

Instantly share code, notes, and snippets.

@jrgcubano
Created July 30, 2014 10:16
Show Gist options
  • Save jrgcubano/f5ffa1e49106298dd10d to your computer and use it in GitHub Desktop.
Save jrgcubano/f5ffa1e49106298dd10d to your computer and use it in GitHub Desktop.
WPF TextBox TextBoxMaskBehavior (by smarter DB and me)
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace PMS_UI.CommonControls.Behaviours
{
public enum ValueTypes
{
NoNumeric,
Integer,
Double
}
public class TextBoxMaskBehavior
{
#region MinimumValue Property
public static double GetMinimumValue(DependencyObject obj)
{
return (double)obj.GetValue(MinimumValueProperty);
}
public static void SetMinimumValue(DependencyObject obj, double value)
{
obj.SetValue(MinimumValueProperty, value);
}
public static readonly DependencyProperty MinimumValueProperty =
DependencyProperty.RegisterAttached(
"MinimumValue",
typeof(double),
typeof(TextBoxMaskBehavior),
new FrameworkPropertyMetadata(double.NaN, MinimumValueChangedCallback)
);
private static void MinimumValueChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBox _this = (d as TextBox);
ValueTypes vt = GetValueType(_this);
double min = GetMinimumValue(_this);
if (!min.Equals(double.NaN))
{
switch (vt)
{
case ValueTypes.Integer:
if (min < Convert.ToDouble(Int32.MinValue))
throw new ArgumentOutOfRangeException("Overflow, minimum: " + Int32.MinValue.ToString());
//SetMinimumValue(_this, Convert.ToDouble(Int32.MinValue));
break;
case ValueTypes.Double:
//Egy karakterrel előbb megállunk, hogy ne okozzon exception-t.
if (min < (Double.MinValue / 100))
throw new ArgumentOutOfRangeException("Overflow, minimum: " + (Double.MinValue / 100).ToString());
//SetMinimumValue(_this, (Double.MinValue / 100));
break;
}
}
ValidateTextBox(_this);
}
#endregion
#region MaximumValue Property
public static double GetMaximumValue(DependencyObject obj)
{
return (double)obj.GetValue(MaximumValueProperty);
}
public static void SetMaximumValue(DependencyObject obj, double value)
{
obj.SetValue(MaximumValueProperty, value);
}
public static readonly DependencyProperty MaximumValueProperty =
DependencyProperty.RegisterAttached(
"MaximumValue",
typeof(double),
typeof(TextBoxMaskBehavior),
new FrameworkPropertyMetadata(double.NaN, MaximumValueChangedCallback)
);
private static void MaximumValueChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBox _this = (d as TextBox);
ValueTypes vt = GetValueType(_this);
double max = GetMaximumValue(_this);
if (!max.Equals(double.NaN))
{
switch (vt)
{
case ValueTypes.Integer:
if (max > Convert.ToDouble(Int32.MaxValue))
throw new ArgumentOutOfRangeException("Overflow, maximum: " + Int32.MaxValue.ToString());
//SetMinimumValue(_this, Convert.ToDouble(Int32.MinValue));
break;
case ValueTypes.Double:
//We stop two characters ahead, so as not to cause an exception.
if (max > (Double.MaxValue / 100))
throw new ArgumentOutOfRangeException("Overflow, maximum: " + (Double.MaxValue / 100).ToString());
//SetMinimumValue(_this, (Double.MinValue / 100));
break;
}
}
ValidateTextBox(_this);
}
#endregion
#region Mask Property
public static string GetMask(DependencyObject obj)
{
return (string)obj.GetValue(MaskProperty);
}
public static void SetMask(DependencyObject obj, string value)
{
obj.SetValue(MaskProperty, value);
}
public static readonly DependencyProperty MaskProperty =
DependencyProperty.RegisterAttached(
"Mask",
typeof(string),
typeof(TextBoxMaskBehavior),
new FrameworkPropertyMetadata(MaskChangedCallback)
);
private static void MaskChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is TextBox)
{
(e.OldValue as TextBox).PreviewTextInput -= TextBox_PreviewTextInput;
(e.OldValue as TextBox).TextChanged -= TextBox_TextChanged;
(e.OldValue as TextBox).PreviewKeyDown -= TextBox_PreviewKeyDown;
(e.OldValue as TextBox).GotKeyboardFocus -= _TextBox_GotKeyboardFocus;
DataObject.RemovePastingHandler((e.OldValue as TextBox), (DataObjectPastingEventHandler)TextBoxPastingEventHandler);
}
TextBox _this = (d as TextBox);
if (_this == null)
return;
if (string.Empty != (string)e.NewValue)
{
_this.PreviewTextInput += TextBox_PreviewTextInput;
_this.TextChanged += TextBox_TextChanged;
_this.PreviewKeyDown += TextBox_PreviewKeyDown;
_this.GotKeyboardFocus += _TextBox_GotKeyboardFocus;
DataObject.AddPastingHandler(_this, (DataObjectPastingEventHandler)TextBoxPastingEventHandler);
}
ValidateTextBox(_this);
}
#endregion
#region ValueType Property
public static ValueTypes GetValueType(DependencyObject obj)
{
return (ValueTypes)obj.GetValue(ValueTypeProperty);
}
public static void SetValueType(DependencyObject obj, ValueTypes value)
{
obj.SetValue(ValueTypeProperty, value);
}
public static readonly DependencyProperty ValueTypeProperty =
DependencyProperty.RegisterAttached(
"ValueType",
typeof(ValueTypes),
typeof(TextBoxMaskBehavior),
new FrameworkPropertyMetadata(ValueTypeChangedCallback)
);
private static void ValueTypeChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBox _this = (d as TextBox);
ValidateTextBox(_this);
}
#endregion
#region Static Methods
static void _TextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
TextBox _this = sender as TextBox;
//A tizedesvessző baloldalára áll kezdéskor
if (_this.Text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
{
_this.CaretIndex = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
}
else
{
_this.CaretIndex = _this.Text.Length;
}
}
static void TextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if ((ValueTypes.NoNumeric != GetValueType(sender as TextBox)) && (Key.Space == e.Key))
{
//Space is not allowed at number type entry.
e.Handled = true;
return;
}
if (Key.Back == e.Key)
{
//Backspace
TextBox _this = sender as TextBox;
if ((0 == _this.SelectionLength) &&
(0 < _this.CaretIndex))
{
//If nothing is selected, the cursor is not at the very beginning.
if (NumberFormatInfo.CurrentInfo.NumberDecimalSeparator == _this.Text.Substring(_this.CaretIndex - 1, 1))
{
//This does not have to be carried out if we want to delete the separator
_this.CaretIndex -= 1;
e.Handled = true;
return;
}
if ((true == _this.Text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)) &&
(_this.CaretIndex > _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator) + 1))
{
//If the cursor is at the decimal value and we delete backwards.
int caret = _this.CaretIndex;
_this.Text = _this.Text.Substring(0, _this.CaretIndex - 1) + _this.Text.Substring(_this.CaretIndex) + "0";
_this.CaretIndex = caret - 1;
e.Handled = true;
return;
}
}
if (0 < _this.CaretIndex)
{
if (0 < _this.SelectionLength)
{
//If we delete the highlighted part of text.
int caret = _this.SelectionStart;
int rcaret = _this.Text.Length - caret - _this.SelectionLength;
string txtWS = _this.Text.Substring(0, caret);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string txtSWS = _this.Text.Substring(caret, _this.SelectionLength);
string txtSWOS = txtSWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string text = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
text = text.Substring(0, caret - (txtWS.Length - txtWOS.Length)) +
//If the highlighted part contains the decimal separator, we put it back after deleting.
(txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator) ? NumberFormatInfo.CurrentInfo.NumberDecimalSeparator : String.Empty) +
text.Substring(caret - (txtWS.Length - txtWOS.Length) + _this.SelectionLength - (txtSWS.Length - txtSWOS.Length));
_this.Text = text;
if (txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
{
//If the decimal separator was also selected, then the cursor is put in front of the decimal separator.
caret = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
}
else
{
caret = _this.Text.Length - rcaret;
}
if (caret < 0)
caret = 0;
_this.CaretIndex = caret;
e.Handled = true;
return;
}
else
{
//One item is deleted from the left.
int caret = _this.CaretIndex;
int rcaret = _this.Text.Length - caret;
string txtWS = _this.Text.Substring(0, caret);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string text = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
text = text.Substring(0, caret - (txtWS.Length - txtWOS.Length) - 1) +
text.Substring(caret - (txtWS.Length - txtWOS.Length));
_this.Text = text;
caret = _this.Text.Length - rcaret;
if (caret < 0)
caret = 0;
_this.CaretIndex = caret;
e.Handled = true;
return;
}
}
}
if (Key.Delete == e.Key)
{
//Del
TextBox _this = sender as TextBox;
if ((0 == _this.SelectionLength) && (_this.CaretIndex < _this.Text.Length))
{
//If nothing is selected, the cursor is not at the very end.
if (NumberFormatInfo.CurrentInfo.NumberDecimalSeparator == _this.Text.Substring(_this.CaretIndex, 1))
{
//This does not have to be carried out if we want to delete the separator
_this.CaretIndex += 1;
e.Handled = true;
return;
}
if ((true == _this.Text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)) &&
(_this.CaretIndex > _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)))
{
//If the cursor is at the decimal value and we delete.
int caret = _this.CaretIndex;
int ind = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
_this.Text = _this.Text.Substring(0, caret) + _this.Text.Substring(caret + 1) + "0";
_this.CaretIndex = caret;
e.Handled = true;
return;
}
}
if (0 < _this.SelectionLength)
{
//If we delete the highlighted part of text.
int caret = _this.SelectionStart;
int rcaret = _this.Text.Length - caret - _this.SelectionLength;
string txtWS = _this.Text.Substring(0, caret);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string txtSWS = _this.Text.Substring(caret, _this.SelectionLength);
string txtSWOS = txtSWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string text = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
text = text.Substring(0, caret - (txtWS.Length - txtWOS.Length)) +
//If the highlighted part contains the decimal separator, we put it back after deleting.
(txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator) ? NumberFormatInfo.CurrentInfo.NumberDecimalSeparator : String.Empty) +
text.Substring(caret - (txtWS.Length - txtWOS.Length) + _this.SelectionLength - (txtSWS.Length - txtSWOS.Length));
//If there is only one decimal separator, it will be deleted.
text = (NumberFormatInfo.CurrentInfo.NumberDecimalSeparator == text ? String.Empty : text);
_this.Text = text;
if (txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
{
caret = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
}
else
{
caret = _this.Text.Length - rcaret;
}
if (caret < 0)
caret = 0;
_this.CaretIndex = caret;
e.Handled = true;
return;
}
else
{
if (_this.CaretIndex < _this.Text.Length)
{
//One item is deleted from the right.
int caret = _this.CaretIndex;
int rcaret = _this.Text.Length - caret - 1;
if (NumberFormatInfo.CurrentInfo.NumberGroupSeparator == _this.Text.Substring(caret, 1))
rcaret -= 1;
string txtWS = _this.Text.Substring(0, caret);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string text = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
text = text.Substring(0, caret - (txtWS.Length - txtWOS.Length)) +
text.Substring(caret - (txtWS.Length - txtWOS.Length) + 1);
_this.Text = text;
caret = _this.Text.Length - rcaret;
if (caret < 0)
caret = 0;
_this.CaretIndex = caret;
e.Handled = true;
return;
}
}
}
e.Handled = false;
}
#endregion
#region Private Static Methods
private static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox _this = sender as TextBox;
string text = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string mask = GetMask(_this);
ValueTypes vt = GetValueType(_this);
if (0 != mask.Length)
{
// text length > 0 && not a single negative sign char
if (0 < _this.Text.Length && !IsOnlyNegativeSign(_this.Text))
{
if (vt.Equals(ValueTypes.Integer))
{
//todo: TryParse/try-catch
_this.Text = String.Format("{" + mask + "}", Int32.Parse(text)); // Int64?
e.Handled = true;
}
else
{
_this.Text = String.Format("{" + mask + "}", Double.Parse(text));
e.Handled = true;
}
}
else
{
_this.Text = "0"; // TODO (text = Max(0, Minimum))
//_this.Text = "";
e.Handled = true;
}
}
}
private static bool IsOnlyNegativeSign(string text)
{
if (text.Length == 1 && text.Contains(NumberFormatInfo.CurrentInfo.NegativeSign))
return true;
return false;
}
private static void ValidateTextBox(TextBox _this)
{
if (string.Empty != GetMask(_this))
{
_this.Text = ValidateValue(GetMask(_this),
GetValueType(_this),
_this.Text,
GetMinimumValue(_this),
GetMaximumValue(_this));
}
}
private static void TextBoxPastingEventHandler(object sender, DataObjectPastingEventArgs e)
{
TextBox _this = (sender as TextBox);
string clipboard = e.DataObject.GetData(typeof(string)) as string;
clipboard = ValidateValue(GetMask(_this), GetValueType(_this), clipboard, GetMinimumValue(_this), GetMaximumValue(_this));
if (!string.IsNullOrEmpty(clipboard))
{
_this.Text = clipboard;
}
e.CancelCommand();
e.Handled = true;
}
private static void TextBox_PreviewTextInput(object sender,
System.Windows.Input.TextCompositionEventArgs e)
{
TextBox _this = (sender as TextBox);
string etext = e.Text;
// TODO (mapping from invariant culture keys for decimal separators to current culture if possible
if (etext == NumberFormatInfo.InvariantInfo.NumberDecimalSeparator)
{
etext = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
}
bool isValid = IsSymbolValid(GetMask(_this), etext, GetValueType(_this));
bool textInserted = false;
bool toNDS = false;
if (isValid)
{
//Current content
string txtOld = _this.Text.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
//New content
string txtNew = String.Empty;
bool handled = false;
int caret = _this.CaretIndex;
int rcaret = 0;
if (etext == NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)
{
//If we entered a decimal separator.
int ind = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator) + 1;
rcaret = _this.Text.Length - ind;
//The text doesn't change.
txtNew = txtOld;
handled = true;
}
if ((!handled) && (etext == NumberFormatInfo.CurrentInfo.NegativeSign))
{
//We entered a negative symbol.
if (_this.Text.Contains(NumberFormatInfo.CurrentInfo.NegativeSign))
{
//A negative symbol is already in the text.
//As overriding the text initializes the cursor, the present position is remembered.
rcaret = _this.Text.Length - caret;
txtNew = txtOld.Replace(NumberFormatInfo.CurrentInfo.NegativeSign, string.Empty);
}
else
{
//There is no negative symbol in the text.
//As overriding the text initializes the cursor, the present position is remembered.
rcaret = _this.Text.Length - caret;
txtNew = NumberFormatInfo.CurrentInfo.NegativeSign + txtOld;
}
handled = true;
}
if (!handled)
{
textInserted = true;
if (0 < _this.SelectionLength)
{
//We delete the highlighted text and insert what we have just written.
int ind = _this.SelectionStart;
rcaret = _this.Text.Length - ind - _this.SelectionLength;
string txtWS = _this.Text.Substring(0, ind);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string txtSWS = _this.Text.Substring(ind, _this.SelectionLength);
string txtSWOS = txtSWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string txtNWS = etext;
string txtNWOS = txtNWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
txtNew = txtOld.Substring(0, ind - (txtWS.Length - txtWOS.Length)) + txtNWOS +
(txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator) ? NumberFormatInfo.CurrentInfo.NumberDecimalSeparator : String.Empty) +
txtOld.Substring(ind - (txtWS.Length - txtWOS.Length) + _this.SelectionLength - (txtSWS.Length - txtSWOS.Length));
if (txtSWOS.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
{
//If the decimal separator was also highlighted, then the cursor is put in front of the decimal separator.
toNDS = true;
}
}
else
{
//We insert the character to the right of the cursor.
int ind = _this.CaretIndex;
rcaret = _this.Text.Length - ind;
if ((0 < rcaret) &&
(NumberFormatInfo.CurrentInfo.NumberGroupSeparator == _this.Text.Substring(ind, 1)))
rcaret -= 1;
string txtWS = _this.Text.Substring(0, ind);
string txtWOS = txtWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
string txtNWS = etext;
string txtNWOS = txtNWS.Replace(NumberFormatInfo.CurrentInfo.NumberGroupSeparator, String.Empty);
txtNew = txtOld.Substring(0, ind - (txtWS.Length - txtWOS.Length)) + txtNWOS +
txtOld.Substring(ind - (txtWS.Length - txtWOS.Length));
}
}
// Timelines (fix to gain the focus)
bool initFocus = false;
if (!txtNew.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)) // !txtNew.Contains(","))
initFocus = true;
try
{
double val = Double.Parse(txtNew);
double newVal = ValidateLimits(GetMinimumValue(_this), GetMaximumValue(_this), val, GetValueType(_this));
if (val != newVal)
{
txtNew = newVal.ToString();
}
else if (val == 0)
{
if (!txtNew.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
{
txtNew = "0";
}
}
}
catch
{
txtNew = "0";
}
_this.Text = txtNew;
if ((true == _this.Text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)) &&
(caret > _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)))
{
//If the cursor is at the decimal value, then it moves to the right of the decimal separator, if possible.
if (caret < _this.Text.Length)
{
if (textInserted)
{
caret += 1;
rcaret = _this.Text.Length - caret;
}
}
else
{
//We are at the very end; it's not possible to enter more characters.
if (textInserted)
_this.Text = txtOld;
}
}
caret = _this.Text.Length - rcaret;
if (caret < 0)
caret = 0;
if (toNDS)
{
_this.CaretIndex = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
}
else
{
_this.CaretIndex = caret;
}
// Timelines (continue fix to gain the focus)
if (initFocus && _this.Text.Contains(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator))
_this.CaretIndex = _this.Text.IndexOf(NumberFormatInfo.CurrentInfo.NumberDecimalSeparator);
}
e.Handled = true;
}
private static string ValidateValue(string mask, ValueTypes vt, string value, double min, double max)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
value = value.Trim();
switch (vt)
{
case ValueTypes.Integer:
try
{
value = ValidateLimits(min, max, Int32.Parse(value), vt).ToString();
return value;
}
catch { }
return string.Empty;
case ValueTypes.Double:
try
{
value = ValidateLimits(min, max, Double.Parse(value), vt).ToString();
return value;
}
catch { }
return string.Empty;
}
return string.Empty;
}
private static double ValidateLimits(double min, double max, double value, ValueTypes vt)
{
if (!min.Equals(double.NaN))
{
if (value < min)
return min;
}
else
{
switch (vt)
{
case ValueTypes.Integer:
if (value < Convert.ToDouble(Int32.MinValue))
return Convert.ToDouble(Int32.MinValue);
break;
case ValueTypes.Double:
//Két karakterrel előbb megállunk, hogy ne okozzon exception-t.
if (value < (Double.MinValue / 100))
return (Double.MinValue / 100);
break;
}
}
if (!max.Equals(double.NaN))
{
if (value > max)
return max;
}
else
{
switch (vt)
{
case ValueTypes.Integer:
if (value > Convert.ToDouble(Int32.MaxValue))
return Convert.ToDouble(Int32.MaxValue);
break;
case ValueTypes.Double:
//We stop two characters ahead, so as not to cause an exception.
if (value > (Double.MaxValue / 100))
return (Double.MaxValue / 100);
break;
}
}
return value;
}
private static bool IsSymbolValid(string mask, string str, ValueTypes typ)
{
switch (typ)
{
case ValueTypes.NoNumeric:
return true;
case ValueTypes.Integer:
if (str == NumberFormatInfo.CurrentInfo.NegativeSign)
return true;
break;
case ValueTypes.Double:
if (str == NumberFormatInfo.CurrentInfo.NumberDecimalSeparator ||
str == NumberFormatInfo.CurrentInfo.NegativeSign)
return true;
break;
}
if (typ.Equals(ValueTypes.Integer) || typ.Equals(ValueTypes.Double))
{
foreach (char ch in str)
{
if (!Char.IsDigit(ch))
return false;
}
return true;
}
return false;
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment