Introduction
EDIT:
-found a couple of small bugs. (will update code, when done checking)
-implemented DP for "Value
". (will update code, when done checking)
-known issue: "Value
" setter is fired more than once, when text is updated from UI.
-known issue: numbers string format is NOT according to region.
--------------------------------------------------------------------------------------------
My WPF dev. started with inheriting a one-man application, written in "WPF", using win-forms event driven approach, and NOT using any of the WPF provided technologies. Shouldn't even mention the architecture, which was further from MVVM, as we are from colonizing Mars.
One of the most annoying issues I've encountered was validation, restriction and formatting of numerical user input. Given the nature of the application, some fields are positive only, some are integer only, etc.
I needed a solution that will address both data-binding, AND using the UI element directly. Additionally, I wished for both the string
value, and the numerical to be readily available. Lastly, I required "fine-tuning" the values by keys/mouse wheel + different text foregrounds.
Below is my solution, which may be easily expanded and modified according to specific needs.
What can it do:
- restrict input to match Int/Double correct format, disable copy-paste
- IsPositiveOnly setter ("0" is considered "positive")
- Normal/Modified/Error text foreground setters
- display numerics with "thousands" commas (I'm dealing with large numbers, so it really helps)
- number of (double) fraction digits setter
- keys Up/Down/P-Up/P-Down/mouse wheel - increase/decrease the value by predefined %
Using the Code
- Usable as is.
Abstract
base class + implementations for Int
and Double
data-types. - If binding to the numeric "
Value
" is desired, a DP should be implemented.
Base Class
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace CustomControls
{
public abstract class SmartTextBox : TextBox
{
public string ParamName { get; set; }
public bool IsValid { get; protected set; }
public bool IsPosOnly { get; set; }
public bool IsSetModifiedFG { get; set; }
public int FinePercent { get; set; }
public int CoarsePercent { get; set; }
public int WheelPercent { get; set; }
private static readonly BrushConverter _BrushConverter = new BrushConverter();
private const string _DefaultBgColor = "#3CFFFFFF";
protected SmartTextBox()
{
IsPosOnly = true;
IsSetModifiedFG = true;
FinePercent = 1;
CoarsePercent = 10;
WheelPercent = 3;
DataObject.AddCopyingHandler(this,
(sender, args) => args.CancelCommand());
DataObject.AddPastingHandler(this,
(sender, args) => args.CancelCommand());
this.PreviewMouseRightButtonUp +=
(sender, args) => args.Handled = true;
this.PreviewKeyDown += OnPreviewKeyDown;
this.TextChanged += OnTextChanged;
this.MouseWheel += OnMouseWheel;
this.Background = (Brush)_BrushConverter.ConvertFromString(_DefaultBgColor);
}
protected abstract void ApplyField();
protected abstract void CancelField();
public void SetNormalForeground()
{
this.Foreground = Brushes.Black;
}
public void SetModifiedForeground()
{
this.Foreground = Brushes.RoyalBlue;
}
public void SetErrorForeground()
{
this.Foreground = Brushes.Red;
}
private void OnMouseWheel(object sender, MouseWheelEventArgs mouseWheelEventArgs)
{
if (mouseWheelEventArgs.Delta > 0)
IncrementByPercent(WheelPercent);
else
DecrementByPercent(WheelPercent);
}
protected virtual void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
{
IsValid = !(this.Text == "" ||
this.Text == "-" || this.Text == "Not supported");
}
protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs k)
{
if (k.Key == Key.Enter)
{
ApplyField();
}
else if (k.Key == Key.Escape)
{
CancelField();
}
else if (k.Key == Key.Up)
{
IncrementByPercent(FinePercent);
}
else if (k.Key == Key.Down)
{
DecrementByPercent(FinePercent);
}
else if (k.Key == Key.PageUp)
{
k.Handled = true;
IncrementByPercent(CoarsePercent);
}
else if (k.Key == Key.PageDown)
{
k.Handled = true;
DecrementByPercent(CoarsePercent);
}
else if (!IsPosOnly && (k.Key == Key.Subtract || k.Key == Key.OemMinus))
{
if (this.Text.StartsWith("-") || this.CaretIndex != 0)
k.Handled = true;
}
else
{
int keyInt = (int)k.Key;
if ((keyInt >= 34 && keyInt <= 43) ||
(keyInt >= 74 && keyInt <= 83))
{
if (this.CaretIndex == 0 &&
(this.Text.StartsWith("-") || this.Text.StartsWith(".")))
k.Handled = true;
}
else if (!(k.Key == Key.Back || k.Key == Key.Delete || k.Key == Key.Left ||
k.Key == Key.Right || k.Key == Key.Tab || k.Key == Key.Home || k.Key == Key.End))
{
k.Handled = true;
}
}
}
protected abstract void IncrementByPercent(int percent);
protected abstract void DecrementByPercent(int percent);
}
}
INT Handling Implementation
using System;
using System.Windows.Controls;
namespace CustomControls
{
public class SmartIntTextBox : SmartTextBox
{
public Action<smartinttextbox> ApplyFieldEvent;
public Action<smartinttextbox> CancelFieldEvent;
public Action<smartinttextbox> DataChangedEvent;
private int _Value;
public int Value
{
get { return _Value; }
protected set
{
_Value = value;
int ci = this.CaretIndex;
int len = this.Text.Length;
this.Text = _Value.ToString("N0");
this.CaretIndex = ci + (this.Text.Length - len);
if (DataChangedEvent != null)
DataChangedEvent(this);
}
}
protected override void ApplyField()
{
if (ApplyFieldEvent != null)
ApplyFieldEvent(this);
}
protected override void CancelField()
{
if (CancelFieldEvent != null)
CancelFieldEvent(this);
}
protected override void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
{
base.OnTextChanged(sender, textChangedEventArgs);
if(!IsValid)
return;
if (!int.TryParse(this.Text.Replace(",", ""), out _Value))
{
_Value = 0;
IsValid = false;
SetErrorForeground();
return;
}
Value = _Value;
IsValid = true;
if (IsSetModifiedFG)
SetModifiedForeground();
}
protected override void IncrementByPercent(int percent)
{
if (_Value == 0)
{
++Value;
return;
}
double ratio = ((double)percent + 100) / 100;
Value = _Value > 0 ? (int)Math.Ceiling(_Value * ratio) : (int)Math.Ceiling(_Value / ratio);
}
protected override void DecrementByPercent(int percent)
{
if (!IsPosOnly && _Value == 0)
{
--Value;
return;
}
double ratio = ((double)percent + 100) / 100;
Value = _Value > 0 ? (int)Math.Floor(_Value / ratio) : (int)Math.Floor(_Value * ratio);
}
}
}
Double Handling Implementation
using System;
using System.Windows.Controls;
using System.Windows.Input;
namespace CustomControls
{
public class SmartDoubleTextBox : SmartTextBox
{
public Action<smartdoubletextbox> ApplyFieldEvent;
public Action<smartdoubletextbox> CancelFieldEvent;
public Action<smartdoubletextbox> DataChangedEvent;
private string _Format = "{0:#,##0.##}";
private int _DecPlaces = 2;
public int DecPlaces
{
get { return _DecPlaces; }
set
{
_DecPlaces = value;
_Format = "{0:#,##0.";
for (int ii = 0; ii < _DecPlaces; ++ii)
_Format += "#";
_Format += "}";
this.Text = string.Format(_Format, _Value);
}
}
private double _Value;
public double Value
{
get { return _Value; }
protected set
{
_Value = value;
int ci = this.CaretIndex;
int len = this.Text.Length;
this.Text = string.Format(_Format, _Value);
this.CaretIndex = ci + (this.Text.Length - len);
if (DataChangedEvent != null)
DataChangedEvent(this);
}
}
protected override void ApplyField()
{
if (ApplyFieldEvent != null)
ApplyFieldEvent(this);
}
protected override void CancelField()
{
if (CancelFieldEvent != null)
CancelFieldEvent(this);
}
protected override void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
{
base.OnTextChanged(sender, textChangedEventArgs);
if (!IsValid)
return;
if (!double.TryParse(this.Text.Replace(",", ""), out _Value))
{
_Value = 0;
IsValid = false;
SetErrorForeground();
return;
}
if (!(this.Text.EndsWith(".") ||
(this.Text.Contains(".") && this.Text.EndsWith("0"))))
Value = _Value;
IsValid = true;
if (IsSetModifiedFG)
SetModifiedForeground();
}
protected override void OnPreviewKeyDown(object sender, KeyEventArgs k)
{
int decIdx = this.Text.IndexOf(".");
if (k.Key == Key.Decimal || k.Key == Key.OemPeriod)
{
if (decIdx != -1)
k.Handled = true;
return;
}
if (decIdx >= 0)
{
int keyInt = (int)k.Key;
if (this.CaretIndex > decIdx &&
this.Text.Length - decIdx - 1 >= _DecPlaces &&
((keyInt >= 34 && keyInt <= 43) ||
(keyInt >= 74 && keyInt <= 83)))
{
k.Handled = true;
return;
}
}
base.OnPreviewKeyDown(sender, k);
}
protected override void IncrementByPercent(int percent)
{
if (Math.Abs(_Value) < 0.00001)
{
++Value;
return;
}
double ratio = ((double)percent + 100) / 100;
Value = _Value > 0 ? _Value * ratio : _Value / ratio;
}
protected override void DecrementByPercent(int percent)
{
if (!IsPosOnly && Math.Abs(_Value) < 0.00001)
{
--Value;
return;
}
double ratio = ((double)percent + 100) / 100;
Value = _Value > 0 ? _Value / ratio : _Value * ratio;
}
}
}
History
First version, will probably be updated according to new findings and needs. : )