using System; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; namespace Gumming { [TemplatePart(Name = "PART_TextBox", Type = typeof (TextBox))] [TemplatePart(Name = "PART_IncreaseButton", Type = typeof (RepeatButton))] [TemplatePart(Name = "PART_DecreaseButton", Type = typeof (RepeatButton))] public class NumericUpDown : Control { #region Properties #region Value public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof (Decimal), typeof (NumericUpDown), new PropertyMetadata(0m, OnValueChanged, CoerceValue)); public Decimal Value { get { return (Decimal) GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } private static void OnValueChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; if (control.TextBox != null) { control.TextBox.UndoLimit = 0; control.TextBox.UndoLimit = 1; } } private static object CoerceValue(DependencyObject element, object baseValue) { var control = (NumericUpDown) element; var value = (Decimal) baseValue; control.CoerceValueToBounds(ref value); // Get the text representation of Value var valueString = value.ToString(control.Culture); // Count all decimal places var decimalPlaces = control.GetDecimalPlacesCount(valueString); if (decimalPlaces > control.DecimalPlaces) { if (control.IsDecimalPointDynamic) { // Assigning DecimalPlaces will coerce the number control.DecimalPlaces = decimalPlaces; // If the specified number of decimal places is still too much if (decimalPlaces > control.DecimalPlaces) { value = control.TruncateValue(valueString, control.DecimalPlaces); } } else { // Remove all overflowing decimal places value = control.TruncateValue(valueString, decimalPlaces); } } else if (control.IsDecimalPointDynamic) { control.DecimalPlaces = decimalPlaces; } if (control.IsThousandSeparatorVisible) { if (control.TextBox != null) { control.TextBox.Text = value.ToString("N", control.Culture); } } else { if (control.TextBox != null) { control.TextBox.Text = value.ToString("F", control.Culture); } } return value; } #endregion #region MaxValue public static readonly DependencyProperty MaxValueProperty = DependencyProperty.Register("MaxValue", typeof (Decimal), typeof (NumericUpDown), new PropertyMetadata(100000000m, OnMaxValueChanged, CoerceMaxValue)); public Decimal MaxValue { get { return (Decimal) GetValue(MaxValueProperty); } set { SetValue(MaxValueProperty, value); } } private static void OnMaxValueChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; var maxValue = (Decimal) e.NewValue; // If maxValue steps over MinValue, shift it if (maxValue < control.MinValue) { control.MinValue = maxValue; } if (maxValue <= control.Value) { control.Value = maxValue; } } private static object CoerceMaxValue(DependencyObject element, Object baseValue) { var maxValue = (Decimal) baseValue; if (maxValue == Decimal.MaxValue) { return DependencyProperty.UnsetValue; } return maxValue; } #endregion #region MinValue public static readonly DependencyProperty MinValueProperty = DependencyProperty.Register("MinValue", typeof (Decimal), typeof (NumericUpDown), new PropertyMetadata(0m, OnMinValueChanged, CoerceMinValue)); public Decimal MinValue { get { return (Decimal) GetValue(MinValueProperty); } set { SetValue(MinValueProperty, value); } } private static void OnMinValueChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; var minValue = (Decimal) e.NewValue; // If minValue steps over MaxValue, shift it if (minValue > control.MaxValue) { control.MaxValue = minValue; } if (minValue >= control.Value) { control.Value = minValue; } } private static object CoerceMinValue(DependencyObject element, Object baseValue) { var minValue = (Decimal) baseValue; if (minValue == Decimal.MinValue) { return DependencyProperty.UnsetValue; } return minValue; } #endregion #region DecimalPlaces public static readonly DependencyProperty DecimalPlacesProperty = DependencyProperty.Register("DecimalPlaces", typeof (Int32), typeof (NumericUpDown), new PropertyMetadata(0, OnDecimalPlacesChanged, CoerceDecimalPlaces)); public Int32 DecimalPlaces { get { return (Int32) GetValue(DecimalPlacesProperty); } set { SetValue(DecimalPlacesProperty, value); } } private static void OnDecimalPlacesChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; var decimalPlaces = (Int32) e.NewValue; control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces; if (control.IsDecimalPointDynamic) { control.IsDecimalPointDynamic = false; control.InvalidateProperty(ValueProperty); control.IsDecimalPointDynamic = true; } else { control.InvalidateProperty(ValueProperty); } } private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue) { var decimalPlaces = (Int32) baseValue; var control = (NumericUpDown) element; if (decimalPlaces < control.MinDecimalPlaces) { decimalPlaces = control.MinDecimalPlaces; } else if (decimalPlaces > control.MaxDecimalPlaces) { decimalPlaces = control.MaxDecimalPlaces; } return decimalPlaces; } #endregion #region MaxDecimalPlaces public static readonly DependencyProperty MaxDecimalPlacesProperty = DependencyProperty.Register("MaxDecimalPlaces", typeof (Int32), typeof (NumericUpDown), new PropertyMetadata(28, OnMaxDecimalPlacesChanged, CoerceMaxDecimalPlaces)); public Int32 MaxDecimalPlaces { get { return (Int32) GetValue(MaxDecimalPlacesProperty); } set { SetValue(MaxDecimalPlacesProperty, value); } } private static void OnMaxDecimalPlacesChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; control.InvalidateProperty(DecimalPlacesProperty); } private static object CoerceMaxDecimalPlaces(DependencyObject element, Object baseValue) { var maxDecimalPlaces = (Int32) baseValue; var control = (NumericUpDown) element; if (maxDecimalPlaces > 28) { maxDecimalPlaces = 28; } else if (maxDecimalPlaces < 0) { maxDecimalPlaces = 0; } else if (maxDecimalPlaces < control.MinDecimalPlaces) { control.MinDecimalPlaces = maxDecimalPlaces; } return maxDecimalPlaces; } #endregion #region MinDecimalPlaces public static readonly DependencyProperty MinDecimalPlacesProperty = DependencyProperty.Register("MinDecimalPlaces", typeof (Int32), typeof (NumericUpDown), new PropertyMetadata(0, OnMinDecimalPlacesChanged, CoerceMinDecimalPlaces)); public Int32 MinDecimalPlaces { get { return (Int32) GetValue(MinDecimalPlacesProperty); } set { SetValue(MinDecimalPlacesProperty, value); } } private static void OnMinDecimalPlacesChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; control.InvalidateProperty(DecimalPlacesProperty); } private static object CoerceMinDecimalPlaces(DependencyObject element, Object baseValue) { var minDecimalPlaces = (Int32) baseValue; var control = (NumericUpDown) element; if (minDecimalPlaces < 0) { minDecimalPlaces = 0; } else if (minDecimalPlaces > 28) { minDecimalPlaces = 28; } else if (minDecimalPlaces > control.MaxDecimalPlaces) { control.MaxDecimalPlaces = minDecimalPlaces; } return minDecimalPlaces; } #endregion #region IsDecimalPointDynamic public static readonly DependencyProperty IsDecimalPointDynamicProperty = DependencyProperty.Register("IsDecimalPointDynamic", typeof (Boolean), typeof (NumericUpDown), new PropertyMetadata(false)); public Boolean IsDecimalPointDynamic { get { return (Boolean) GetValue(IsDecimalPointDynamicProperty); } set { SetValue(IsDecimalPointDynamicProperty, value); } } #endregion #region MinorDelta public static readonly DependencyProperty MinorDeltaProperty = DependencyProperty.Register("MinorDelta", typeof (Decimal), typeof (NumericUpDown), new PropertyMetadata(1m, OnMinorDeltaChanged, CoerceMinorDelta)); public Decimal MinorDelta { get { return (Decimal) GetValue(MinorDeltaProperty); } set { SetValue(MinorDeltaProperty, value); } } private static void OnMinorDeltaChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var minorDelta = (Decimal) e.NewValue; var control = (NumericUpDown) element; if (minorDelta > control.MajorDelta) { control.MajorDelta = minorDelta; } } private static object CoerceMinorDelta(DependencyObject element, Object baseValue) { var minorDelta = (Decimal) baseValue; return minorDelta; } #endregion #region MajorDelta public static readonly DependencyProperty MajorDeltaProperty = DependencyProperty.Register("MajorDelta", typeof (Decimal), typeof (NumericUpDown), new PropertyMetadata(10m, OnMajorDeltaChanged, CoerceMajorDelta)); public Decimal MajorDelta { get { return (Decimal) GetValue(MajorDeltaProperty); } set { SetValue(MajorDeltaProperty, value); } } private static void OnMajorDeltaChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var majorDelta = (Decimal) e.NewValue; var control = (NumericUpDown) element; if (majorDelta < control.MinorDelta) { control.MinorDelta = majorDelta; } } private static object CoerceMajorDelta(DependencyObject element, Object baseValue) { var majorDelta = (Decimal) baseValue; return majorDelta; } #endregion #region IsThousandSeparatorVisible public static readonly DependencyProperty IsThousandSeparatorVisibleProperty = DependencyProperty.Register("IsThousandSeparatorVisible", typeof (Boolean), typeof (NumericUpDown), new PropertyMetadata(false, OnIsThousandSeparatorVisibleChanged)); public Boolean IsThousandSeparatorVisible { get { return (Boolean) GetValue(IsThousandSeparatorVisibleProperty); } set { SetValue(IsThousandSeparatorVisibleProperty, value); } } private static void OnIsThousandSeparatorVisibleChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) { var control = (NumericUpDown) element; control.InvalidateProperty(ValueProperty); } #endregion #region IsAutoSelectionActive public static readonly DependencyProperty IsAutoSelectionActiveProperty = DependencyProperty.Register("IsAutoSelectionActive", typeof (Boolean), typeof (NumericUpDown), new PropertyMetadata(false)); public Boolean IsAutoSelectionActive { get { return (Boolean) GetValue(IsAutoSelectionActiveProperty); } set { SetValue(IsAutoSelectionActiveProperty, value); } } #endregion #region IsValueWrapAllowed public static readonly DependencyProperty IsValueWrapAllowedProperty = DependencyProperty.Register("IsValueWrapAllowed", typeof (Boolean), typeof (NumericUpDown), new PropertyMetadata(false)); public Boolean IsValueWrapAllowed { get { return (Boolean) GetValue(IsValueWrapAllowedProperty); } set { SetValue(IsValueWrapAllowedProperty, value); } } #endregion #endregion #region Fields protected readonly CultureInfo Culture; protected RepeatButton DecreaseButton; protected RepeatButton IncreaseButton; protected TextBox TextBox; #endregion #region Commands private readonly RoutedUICommand _minorDecreaseValueCommand = new RoutedUICommand("MinorDecreaseValue", "MinorDecreaseValue", typeof (NumericUpDown)); private readonly RoutedUICommand _minorIncreaseValueCommand = new RoutedUICommand("MinorIncreaseValue", "MinorIncreaseValue", typeof (NumericUpDown)); private readonly RoutedUICommand _majorDecreaseValueCommand = new RoutedUICommand("MajorDecreaseValue", "MajorDecreaseValue", typeof (NumericUpDown)); private readonly RoutedUICommand _majorIncreaseValueCommand = new RoutedUICommand("MajorIncreaseValue", "MajorIncreaseValue", typeof (NumericUpDown)); private readonly RoutedUICommand _updateValueStringCommand = new RoutedUICommand("UpdateValueString", "UpdateValueString", typeof (NumericUpDown)); private readonly RoutedUICommand _cancelChangesCommand = new RoutedUICommand("CancelChanges", "CancelChanges", typeof (NumericUpDown)); #endregion #region Constructors static NumericUpDown() { DefaultStyleKeyProperty.OverrideMetadata(typeof (NumericUpDown), new FrameworkPropertyMetadata( typeof (NumericUpDown))); } public NumericUpDown() { Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone(); Culture.NumberFormat.NumberDecimalDigits = DecimalPlaces; Loaded += OnLoaded; } #endregion #region Event handlers public override void OnApplyTemplate() { base.OnApplyTemplate(); AttachToVisualTree(); AttachCommands(); } private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { UpdateValue(); } } private void TextBoxOnLostFocus(object sender, RoutedEventArgs routedEventArgs) { UpdateValue(); } private void TextBoxOnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs) { if (IsAutoSelectionActive) { TextBox.SelectAll(); } } private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { InvalidateProperty(ValueProperty); } private void ButtonOnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs) { Value = 0; } #endregion #region Utility Methods #region Attachment private void AttachToVisualTree() { AttachTextBox(); AttachIncreaseButton(); AttachDecreaseButton(); } private void AttachTextBox() { var textBox = GetTemplateChild("PART_TextBox") as TextBox; // A null check is advised if (textBox != null) { TextBox = textBox; TextBox.LostFocus += TextBoxOnLostFocus; TextBox.PreviewKeyDown += TextBox_PreviewKeyDown; TextBox.PreviewMouseLeftButtonUp += TextBoxOnPreviewMouseLeftButtonUp; TextBox.TextChanged += TextBox_TextChanged; TextBox.UndoLimit = 1; TextBox.IsUndoEnabled = true; } } private void TextBox_TextChanged(object sender, TextChangedEventArgs e) { TextBox textBox = sender as TextBox; TextChange[] change = new TextChange[e.Changes.Count]; e.Changes.CopyTo(change, 0); int offset = change[0].Offset; if (change[0].AddedLength > 0) { double num = 0; if (!Double.TryParse(textBox.Text, out num)) { textBox.Text = textBox.Text.Remove(offset, change[0].AddedLength); textBox.Select(offset, 0); } } } private void AttachIncreaseButton() { var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton; if (increaseButton != null) { IncreaseButton = increaseButton; IncreaseButton.Focusable = false; IncreaseButton.Command = _minorIncreaseValueCommand; IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus(); IncreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown; } } private void AttachDecreaseButton() { var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton; if (decreaseButton != null) { DecreaseButton = decreaseButton; DecreaseButton.Focusable = false; DecreaseButton.Command = _minorDecreaseValueCommand; DecreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus(); DecreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown; } } private void AttachCommands() { CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue(true))); CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue(true))); CommandBindings.Add(new CommandBinding(_majorIncreaseValueCommand, (a, b) => IncreaseValue(false))); CommandBindings.Add(new CommandBinding(_majorDecreaseValueCommand, (a, b) => DecreaseValue(false))); CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) => UpdateValue())); CommandBindings.Add(new CommandBinding(_cancelChangesCommand, (a, b) => CancelChanges())); CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up))); CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down))); CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_majorIncreaseValueCommand, new KeyGesture(Key.PageUp))); CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_majorDecreaseValueCommand, new KeyGesture(Key.PageDown))); /* will affect other textbox CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));*/ CommandManager.RegisterClassInputBinding(typeof (TextBox), new KeyBinding(_cancelChangesCommand, new KeyGesture(Key.Escape))); } #endregion #region Data retrieval and deposit private Decimal ParseStringToDecimal(String source) { Decimal value; Decimal.TryParse(source, out value); return value; } public Int32 GetDecimalPlacesCount(String valueString) { return valueString.SkipWhile(c => c.ToString(Culture) != Culture.NumberFormat.NumberDecimalSeparator).Skip(1).Count(); } private Decimal TruncateValue(String valueString, Int32 decimalPlaces) { var endPoint = valueString.Length - (decimalPlaces - DecimalPlaces); endPoint++; var tempValueString = valueString.Substring(0, endPoint); return Decimal.Parse(tempValueString, Culture); } #endregion #region SubCoercion private void CoerceValueToBounds(ref Decimal value) { if (value < MinValue) { value = MinValue; } else if (value > MaxValue) { value = MaxValue; } } #endregion #endregion #region Methods private void UpdateValue() { Value = ParseStringToDecimal(TextBox.Text); } private void CancelChanges() { TextBox.Undo(); } private void RemoveFocus() { // Passes focus here and then just deletes it Focusable = true; Focus(); Focusable = false; } private void IncreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value >= MinValue) { if (minor) { if (IsValueWrapAllowed && value + MinorDelta > MaxValue) { value = MinValue; } else { value += MinorDelta; } } else { if (IsValueWrapAllowed && value + MajorDelta > MaxValue) { value = MinValue; } else { value += MajorDelta; } } } Value = value; } private void DecreaseValue(Boolean minor) { // Get the value that's currently in the _textBox.Text decimal value = ParseStringToDecimal(TextBox.Text); // Coerce the value to min/max CoerceValueToBounds(ref value); // Only change the value if it has any meaning if (value <= MaxValue) { if (minor) { if (IsValueWrapAllowed && value - MinorDelta < MinValue) { value = MaxValue; } else { value -= MinorDelta; } } else { if (IsValueWrapAllowed && value - MajorDelta < MinValue) { value = MaxValue; } else { value -= MajorDelta; } } } Value = value; } #endregion } }