diff --git a/Directory.packages.props b/Directory.packages.props
index 10ca831304..b58226de08 100644
--- a/Directory.packages.props
+++ b/Directory.packages.props
@@ -26,9 +26,9 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs b/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs
index a884a5b24f..64ec7192c5 100644
--- a/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs
+++ b/src/MainDemo.Wpf/Domain/MainWindowViewModel.cs
@@ -475,7 +475,9 @@ private static IEnumerable GenerateDemoItems(ISnackbarMessageQueue sna
{
DocumentationLink.DemoPageLink(),
DocumentationLink.StyleLink(nameof(NumericUpDown)),
- DocumentationLink.ApiLink()
+ DocumentationLink.ApiLink(),
+ DocumentationLink.ApiLink(),
+ DocumentationLink.ApiLink()
});
}
diff --git a/src/MainDemo.Wpf/NumericUpDown.xaml b/src/MainDemo.Wpf/NumericUpDown.xaml
index 37347941bc..ec43eda214 100644
--- a/src/MainDemo.Wpf/NumericUpDown.xaml
+++ b/src/MainDemo.Wpf/NumericUpDown.xaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
diff --git a/src/MaterialDesignThemes.Wpf/DecimalUpDown.cs b/src/MaterialDesignThemes.Wpf/DecimalUpDown.cs
new file mode 100644
index 0000000000..01d2cf035a
--- /dev/null
+++ b/src/MaterialDesignThemes.Wpf/DecimalUpDown.cs
@@ -0,0 +1,42 @@
+#if !NET8_0_OR_GREATER
+using System.Globalization;
+#endif
+
+namespace MaterialDesignThemes.Wpf;
+
+public class DecimalUpDown
+#if NET8_0_OR_GREATER
+ : UpDownBase
+#else
+ : UpDownBase
+#endif
+{
+ static DecimalUpDown()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(DecimalUpDown), new FrameworkPropertyMetadata(typeof(DecimalUpDown)));
+ }
+}
+
+#if !NET8_0_OR_GREATER
+public class DecimalArithmetic : IArithmetic
+{
+ public decimal Add(decimal value1, decimal value2) => value1 + value2;
+
+ public decimal Subtract(decimal value1, decimal value2) => value1 - value2;
+
+ public int Compare(decimal value1, decimal value2) => value1.CompareTo(value2);
+
+ public decimal MinValue() => decimal.MinValue;
+
+ public decimal MaxValue() => decimal.MaxValue;
+
+ public decimal One() => 1m;
+
+ public decimal Max(decimal value1, decimal value2) => Math.Max(value1, value2);
+
+ public decimal Min(decimal value1, decimal value2) => Math.Min(value1, value2);
+
+ public bool TryParse(string text, IFormatProvider? formatProvider, out decimal value)
+ => decimal.TryParse(text, NumberStyles.Number, formatProvider, out value);
+}
+#endif
diff --git a/src/MaterialDesignThemes.Wpf/NumericUpDown.cs b/src/MaterialDesignThemes.Wpf/NumericUpDown.cs
index 4edae2bb87..e5e310a9b0 100644
--- a/src/MaterialDesignThemes.Wpf/NumericUpDown.cs
+++ b/src/MaterialDesignThemes.Wpf/NumericUpDown.cs
@@ -1,259 +1,41 @@
-using System.ComponentModel;
+#if !NET8_0_OR_GREATER
using System.Globalization;
+#endif
namespace MaterialDesignThemes.Wpf;
-[TemplatePart(Name = IncreaseButtonPartName, Type = typeof(RepeatButton))]
-[TemplatePart(Name = DecreaseButtonPartName, Type = typeof(RepeatButton))]
-[TemplatePart(Name = TextBoxPartName, Type = typeof(TextBox))]
-public class NumericUpDown : Control
+public class NumericUpDown
+#if NET8_0_OR_GREATER
+ : UpDownBase
+#else
+ : UpDownBase
+#endif
{
- public const string IncreaseButtonPartName = "PART_IncreaseButton";
- public const string DecreaseButtonPartName = "PART_DecreaseButton";
- public const string TextBoxPartName = "PART_TextBox";
-
- private TextBox? _textBoxField;
- private RepeatButton? _decreaseButton;
- private RepeatButton? _increaseButton;
-
static NumericUpDown()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown), new FrameworkPropertyMetadata(typeof(NumericUpDown)));
}
+}
- #region DependencyProperties
-
-
-
- public object? IncreaseContent
- {
- get => GetValue(IncreaseContentProperty);
- set => SetValue(IncreaseContentProperty, value);
- }
-
- // Using a DependencyProperty as the backing store for IncreaseContent. This enables animation, styling, binding, etc...
- public static readonly DependencyProperty IncreaseContentProperty =
- DependencyProperty.Register(nameof(IncreaseContent), typeof(object), typeof(NumericUpDown), new PropertyMetadata(null));
-
- public object? DecreaseContent
- {
- get => GetValue(DecreaseContentProperty);
- set => SetValue(DecreaseContentProperty, value);
- }
-
- // Using a DependencyProperty as the backing store for DecreaseContent. This enables animation, styling, binding, etc...
- public static readonly DependencyProperty DecreaseContentProperty =
- DependencyProperty.Register(nameof(DecreaseContent), typeof(object), typeof(NumericUpDown), new PropertyMetadata(null));
-
- #region DependencyProperty : MinimumProperty
- public int Minimum
- {
- get => (int)GetValue(MinimumProperty);
- set => SetValue(MinimumProperty, value);
- }
- public static readonly DependencyProperty MinimumProperty =
- DependencyProperty.Register(nameof(Minimum), typeof(int), typeof(NumericUpDown), new PropertyMetadata(int.MinValue, OnMinimumChanged));
-
- private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- NumericUpDown ctrl = (NumericUpDown)d;
- ctrl.CoerceValue(ValueProperty);
- ctrl.CoerceValue(MaximumProperty);
- }
-
- #endregion DependencyProperty : MinimumProperty
-
- #region DependencyProperty : MaximumProperty
-
- public int Maximum
- {
- get => (int)GetValue(MaximumProperty);
- set => SetValue(MaximumProperty, value);
- }
-
- public static readonly DependencyProperty MaximumProperty =
- DependencyProperty.Register(nameof(Maximum), typeof(int), typeof(NumericUpDown), new PropertyMetadata(int.MaxValue, OnMaximumChanged, CoerceMaximum));
-
- private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- NumericUpDown ctrl = (NumericUpDown)d;
- ctrl.CoerceValue(ValueProperty);
- }
-
- private static object? CoerceMaximum(DependencyObject d, object? value)
- {
- if (d is NumericUpDown numericUpDown &&
- value is int numericValue)
- {
- return Math.Max(numericUpDown.Minimum, numericValue);
- }
- return value;
- }
-
- #endregion DependencyProperty : MaximumProperty
-
- #region DependencyProperty : ValueProperty
- public int Value
- {
- get => (int)GetValue(ValueProperty);
- set => SetValue(ValueProperty, value);
- }
-
- public static readonly DependencyProperty ValueProperty =
- DependencyProperty.Register(nameof(Value), typeof(int), typeof(NumericUpDown), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnNumericValueChanged, CoerceNumericValue));
-
- private static void OnNumericValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- if (d is NumericUpDown numericUpDown)
- {
- var args = new RoutedPropertyChangedEventArgs((int)e.OldValue, (int)e.NewValue)
- {
- RoutedEvent = ValueChangedEvent
- };
- numericUpDown.RaiseEvent(args);
- if (numericUpDown._textBoxField is { } textBox)
- {
- textBox.Text = ((int)e.NewValue).ToString(CultureInfo.CurrentUICulture);
- }
-
- if (numericUpDown._increaseButton is { } increaseButton)
- {
- increaseButton.IsEnabled = numericUpDown.Value != numericUpDown.Maximum;
- }
-
- if (numericUpDown._decreaseButton is { } decreaseButton)
- {
- decreaseButton.IsEnabled = numericUpDown.Value != numericUpDown.Minimum;
- }
- }
- }
-
- private static object? CoerceNumericValue(DependencyObject d, object? value)
- {
- if (d is NumericUpDown numericUpDown &&
- value is int numericValue)
- {
- numericValue = Math.Min(numericUpDown.Maximum, numericValue);
- numericValue = Math.Max(numericUpDown.Minimum, numericValue);
- return numericValue;
- }
- return value;
- }
- #endregion ValueProperty
-
- #region DependencyProperty : AllowChangeOnScroll
-
- public bool AllowChangeOnScroll
- {
- get => (bool)GetValue(AllowChangeOnScrollProperty);
- set => SetValue(AllowChangeOnScrollProperty, value);
- }
-
- public static readonly DependencyProperty AllowChangeOnScrollProperty =
- DependencyProperty.Register(nameof(AllowChangeOnScroll), typeof(bool), typeof(NumericUpDown), new PropertyMetadata(false));
-
- #endregion
-
- #endregion DependencyProperties
-
- #region Event : ValueChangedEvent
- [Category("Behavior")]
- public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(nameof(ValueChanged), RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(NumericUpDown));
-
- public event RoutedPropertyChangedEventHandler ValueChanged
- {
- add => AddHandler(ValueChangedEvent, value);
- remove => RemoveHandler(ValueChangedEvent, value);
- }
- #endregion Event : ValueChangedEvent
-
- public override void OnApplyTemplate()
- {
- if (_increaseButton != null)
- _increaseButton.Click -= IncreaseButtonOnClick;
-
- if (_decreaseButton != null)
- _decreaseButton.Click -= DecreaseButtonOnClick;
- if (_textBoxField != null)
- _textBoxField.TextChanged -= OnTextBoxFocusLost;
-
- _increaseButton = GetTemplateChild(IncreaseButtonPartName) as RepeatButton;
- _decreaseButton = GetTemplateChild(DecreaseButtonPartName) as RepeatButton;
- _textBoxField = GetTemplateChild(TextBoxPartName) as TextBox;
-
- if (_increaseButton != null)
- _increaseButton.Click += IncreaseButtonOnClick;
-
- if (_decreaseButton != null)
- _decreaseButton.Click += DecreaseButtonOnClick;
-
- if (_textBoxField != null)
- {
- _textBoxField.LostFocus += OnTextBoxFocusLost;
- _textBoxField.Text = Value.ToString(CultureInfo.CurrentUICulture);
- }
-
- base.OnApplyTemplate();
- }
+#if !NET8_0_OR_GREATER
+public class IntArithmetic : IArithmetic
+{
+ public int Add(int value1, int value2) => value1 + value2;
- private void OnTextBoxFocusLost(object sender, EventArgs e)
- {
- if (_textBoxField is { } textBoxField)
- {
- if (int.TryParse(textBoxField.Text, NumberStyles.Integer, CultureInfo.CurrentUICulture, out int numericValue))
- {
- SetCurrentValue(ValueProperty, numericValue);
- }
- else
- {
- textBoxField.Text = Value.ToString(CultureInfo.CurrentUICulture);
- }
- }
- }
+ public int Subtract(int value1, int value2) => value1 - value2;
- private void IncreaseButtonOnClick(object sender, RoutedEventArgs e) => OnIncrease();
+ public int Compare(int value1, int value2) => value1.CompareTo(value2);
- private void DecreaseButtonOnClick(object sender, RoutedEventArgs e) => OnDecrease();
+ public int MinValue() => int.MinValue;
- private void OnIncrease()
- {
- SetCurrentValue(ValueProperty, Value + 1);
- }
+ public int MaxValue() => int.MaxValue;
+ public int One() => 1;
- private void OnDecrease()
- {
- SetCurrentValue(ValueProperty, Value - 1);
- }
+ public int Max(int value1, int value2) => Math.Max(value1, value2);
- protected override void OnPreviewKeyDown(KeyEventArgs e)
- {
- if (e.Key == Key.Up)
- {
- OnIncrease();
- e.Handled = true;
- }
- else if (e.Key == Key.Down)
- {
- OnDecrease();
- e.Handled = true;
- }
- base.OnPreviewKeyDown(e);
- }
+ public int Min(int value1, int value2) => Math.Min(value1, value2);
- protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
- {
- if (IsKeyboardFocusWithin && AllowChangeOnScroll)
- {
- if (e.Delta > 0)
- {
- OnIncrease();
- }
- else if (e.Delta < 0)
- {
- OnDecrease();
- }
- e.Handled = true;
- }
- base.OnPreviewMouseWheel(e);
- }
+ public bool TryParse(string text, IFormatProvider? formatProvider, out int value)
+ => int.TryParse(text, NumberStyles.Integer, formatProvider, out value);
}
+#endif
diff --git a/src/MaterialDesignThemes.Wpf/Themes/Generic.xaml b/src/MaterialDesignThemes.Wpf/Themes/Generic.xaml
index 4c74b820fd..4f6842614a 100644
--- a/src/MaterialDesignThemes.Wpf/Themes/Generic.xaml
+++ b/src/MaterialDesignThemes.Wpf/Themes/Generic.xaml
@@ -43,6 +43,7 @@
+
diff --git a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.NumericUpDown.xaml b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.NumericUpDown.xaml
index b705ad673a..1caecc2c13 100644
--- a/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.NumericUpDown.xaml
+++ b/src/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.NumericUpDown.xaml
@@ -1,106 +1,120 @@
+ xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf">
-
-
-
+
+
-
-
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/src/MaterialDesignThemes.Wpf/UpDownBase.cs b/src/MaterialDesignThemes.Wpf/UpDownBase.cs
new file mode 100644
index 0000000000..a99a7ccb2c
--- /dev/null
+++ b/src/MaterialDesignThemes.Wpf/UpDownBase.cs
@@ -0,0 +1,325 @@
+using System.ComponentModel;
+using System.Globalization;
+
+namespace MaterialDesignThemes.Wpf;
+
+
+#if NET8_0_OR_GREATER
+using System.Numerics;
+public class UpDownBase : UpDownBase
+ where T : INumber, IMinMaxValue
+{
+ private static readonly Type SelfType = typeof(UpDownBase);
+
+ private static UpDownBase ToUpDownBase(DependencyObject dependencyObject) => (UpDownBase)dependencyObject;
+
+ private static T MinValue => T.MinValue;
+ private static T MaxValue => T.MaxValue;
+ private static T One => T.One;
+ private static T Max(T value1, T value2) => T.Max(value1, value2);
+ private static T Clamp(T value, T min, T max) => T.Clamp(value, min, max);
+ private static T Add(T value1, T value2) => value1 + value2;
+ private static T Subtract(T value1, T value2) => value1 - value2;
+ private static bool TryParse(string text, IFormatProvider? formatProvider, out T? value)
+ => T.TryParse(text, formatProvider, out value);
+ private static int Compare(T value1, T value2) => value1.CompareTo(value2);
+#else
+public class UpDownBase : UpDownBase
+ where TArithmetic : IArithmetic, new()
+{
+ private static readonly Type SelfType = typeof(UpDownBase);
+ private static readonly TArithmetic _arithmetic = new();
+
+ private static UpDownBase ToUpDownBase(DependencyObject dependencyObject) => (UpDownBase)dependencyObject;
+
+ private static T MinValue => _arithmetic.MinValue();
+ private static T MaxValue => _arithmetic.MaxValue();
+ private static T One => _arithmetic.One();
+ private static T Max(T value1, T value2) => _arithmetic.Max(value1, value2);
+ private static T Clamp(T value, T min, T max) => _arithmetic.Max(_arithmetic.Min(value, max), min);
+ private static T Add(T value1, T value2) => _arithmetic.Add(value1, value2);
+ private static T Subtract(T value1, T value2) => _arithmetic.Subtract(value1, value2);
+ private static bool TryParse(string text, IFormatProvider? formatProvider, out T? value)
+ => _arithmetic.TryParse(text, formatProvider, out value);
+ private static int Compare(T value1, T value2) => _arithmetic.Compare(value1, value2);
+#endif
+
+ #region DependencyProperties
+
+ #region DependencyProperty : MinimumProperty
+
+ public virtual T Minimum
+ {
+ get => (T)GetValue(MinimumProperty);
+ set => SetValue(MinimumProperty, value);
+ }
+ public static readonly DependencyProperty MinimumProperty =
+ DependencyProperty.Register(nameof(Minimum), typeof(T), SelfType, new PropertyMetadata(MinValue, OnMinimumChanged));
+
+ private static void OnMinimumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var ctrl = ToUpDownBase(d);
+ ctrl.CoerceValue(MaximumProperty);
+ ctrl.CoerceValue(ValueProperty);
+ }
+
+ #endregion DependencyProperty : MinimumProperty
+
+ #region DependencyProperty : MaximumProperty
+
+ public T Maximum
+ {
+ get => (T)GetValue(MaximumProperty);
+ set => SetValue(MaximumProperty, value);
+ }
+
+ public static readonly DependencyProperty MaximumProperty =
+ DependencyProperty.Register(nameof(Maximum), typeof(T), SelfType, new PropertyMetadata(MaxValue, OnMaximumChanged, CoerceMaximum));
+
+ private static void OnMaximumChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var upDownBase = ToUpDownBase(d);
+ upDownBase.CoerceValue(ValueProperty);
+ }
+
+ private static object? CoerceMaximum(DependencyObject d, object? value)
+ {
+ if (value is T numericValue)
+ {
+ var upDownBase = ToUpDownBase(d);
+ return Max(upDownBase.Minimum, numericValue);
+ }
+ return value;
+ }
+
+ #endregion DependencyProperty : MaximumProperty
+
+ #region DependencyProperty : ValueProperty
+ public T Value
+ {
+ get => (T)GetValue(ValueProperty);
+ set => SetValue(ValueProperty, value);
+ }
+
+ public static readonly DependencyProperty ValueProperty =
+ DependencyProperty.Register(nameof(Value), typeof(T), SelfType, new FrameworkPropertyMetadata(default(T), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnNumericValueChanged, CoerceNumericValue));
+
+ private static void OnNumericValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var upDownBase = ToUpDownBase(d);
+ var args = new RoutedPropertyChangedEventArgs((T)e.OldValue, (T)e.NewValue)
+ {
+ RoutedEvent = ValueChangedEvent
+ };
+ upDownBase.RaiseEvent(args);
+
+ if (upDownBase._textBoxField is { } textBox)
+ {
+ textBox.Text = e.NewValue.ToString();
+ }
+
+ if (upDownBase._increaseButton is { } increaseButton)
+ {
+ increaseButton.IsEnabled = Compare(upDownBase.Value, upDownBase.Maximum) < 0;
+ }
+
+ if (upDownBase._decreaseButton is { } decreaseButton)
+ {
+ decreaseButton.IsEnabled = Compare(upDownBase.Value, upDownBase.Minimum) > 0;
+ }
+ }
+
+ private static object? CoerceNumericValue(DependencyObject d, object? value)
+ {
+ if (value is T numericValue)
+ {
+ var upDownBase = ToUpDownBase(d);
+ numericValue = Clamp(numericValue, upDownBase.Minimum, upDownBase.Maximum);
+ return numericValue;
+ }
+ return value;
+ }
+ #endregion ValueProperty
+
+ #region DependencyProperty : ValueStep
+ ///
+ /// The step of value for each increase or decrease
+ ///
+ public T ValueStep
+ {
+ get => (T)GetValue(ValueStepProperty);
+ set => SetValue(ValueStepProperty, value);
+ }
+
+ public static readonly DependencyProperty ValueStepProperty =
+ DependencyProperty.Register(nameof(ValueStep), typeof(T), SelfType, new PropertyMetadata(One));
+ #endregion
+
+ #region DependencyProperty : AllowChangeOnScroll
+
+ public bool AllowChangeOnScroll
+ {
+ get => (bool)GetValue(AllowChangeOnScrollProperty);
+ set => SetValue(AllowChangeOnScrollProperty, value);
+ }
+
+ public static readonly DependencyProperty AllowChangeOnScrollProperty =
+ DependencyProperty.Register(nameof(AllowChangeOnScroll), typeof(bool), SelfType, new PropertyMetadata(false));
+
+ #endregion
+
+ #endregion DependencyProperties
+
+ #region Event : ValueChangedEvent
+ [Category("Behavior")]
+ public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(nameof(ValueChanged), RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), SelfType);
+
+ public event RoutedPropertyChangedEventHandler ValueChanged
+ {
+ add => AddHandler(ValueChangedEvent, value);
+ remove => RemoveHandler(ValueChangedEvent, value);
+ }
+ #endregion Event : ValueChangedEvent
+
+ public override void OnApplyTemplate()
+ {
+ if (_increaseButton != null)
+ _increaseButton.Click -= IncreaseButtonOnClick;
+
+ if (_decreaseButton != null)
+ _decreaseButton.Click -= DecreaseButtonOnClick;
+ if (_textBoxField != null)
+ _textBoxField.TextChanged -= OnTextBoxFocusLost;
+
+ _increaseButton = GetTemplateChild(IncreaseButtonPartName) as RepeatButton;
+ _decreaseButton = GetTemplateChild(DecreaseButtonPartName) as RepeatButton;
+ _textBoxField = GetTemplateChild(TextBoxPartName) as TextBox;
+
+ if (_increaseButton != null)
+ _increaseButton.Click += IncreaseButtonOnClick;
+
+ if (_decreaseButton != null)
+ _decreaseButton.Click += DecreaseButtonOnClick;
+
+ if (_textBoxField != null)
+ {
+ _textBoxField.LostFocus += OnTextBoxFocusLost;
+ _textBoxField.Text = Value?.ToString();
+ }
+
+ base.OnApplyTemplate();
+ }
+
+ private void OnTextBoxFocusLost(object sender, EventArgs e)
+ {
+ if (_textBoxField is { } textBoxField)
+ {
+ if (TryParse(textBoxField.Text, CultureInfo.CurrentUICulture, out T? value))
+ {
+ SetCurrentValue(ValueProperty, value);
+ }
+ else
+ {
+ textBoxField.Text = Value?.ToString();
+ }
+ }
+ }
+
+ private void IncreaseButtonOnClick(object sender, RoutedEventArgs e) => OnIncrease();
+
+ private void DecreaseButtonOnClick(object sender, RoutedEventArgs e) => OnDecrease();
+
+ private void OnIncrease() => SetCurrentValue(ValueProperty, Clamp(Add(Value, ValueStep), Minimum, Maximum));
+
+ private void OnDecrease() => SetCurrentValue(ValueProperty, Clamp(Subtract(Value, ValueStep), Minimum, Maximum));
+
+ protected override void OnPreviewKeyDown(KeyEventArgs e)
+ {
+ if (e.Key == Key.Up)
+ {
+ OnIncrease();
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Down)
+ {
+ OnDecrease();
+ e.Handled = true;
+ }
+ base.OnPreviewKeyDown(e);
+ }
+
+ protected override void OnPreviewMouseWheel(MouseWheelEventArgs e)
+ {
+ if (IsKeyboardFocusWithin && AllowChangeOnScroll)
+ {
+ if (e.Delta > 0)
+ {
+ OnIncrease();
+ }
+ else if (e.Delta < 0)
+ {
+ OnDecrease();
+ }
+ e.Handled = true;
+ }
+ base.OnPreviewMouseWheel(e);
+ }
+}
+
+[TemplatePart(Name = IncreaseButtonPartName, Type = typeof(RepeatButton))]
+[TemplatePart(Name = DecreaseButtonPartName, Type = typeof(RepeatButton))]
+[TemplatePart(Name = TextBoxPartName, Type = typeof(TextBox))]
+public class UpDownBase : Control
+{
+ public const string IncreaseButtonPartName = "PART_IncreaseButton";
+ public const string DecreaseButtonPartName = "PART_DecreaseButton";
+ public const string TextBoxPartName = "PART_TextBox";
+
+ protected TextBox? _textBoxField;
+ protected RepeatButton? _decreaseButton;
+ protected RepeatButton? _increaseButton;
+
+ public object? IncreaseContent
+ {
+ get => GetValue(IncreaseContentProperty);
+ set => SetValue(IncreaseContentProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for IncreaseContent. This enables animation, styling, binding, etc...
+ public static readonly DependencyProperty IncreaseContentProperty =
+ DependencyProperty.Register(nameof(IncreaseContent), typeof(object), typeof(UpDownBase), new PropertyMetadata(null));
+
+ public object? DecreaseContent
+ {
+ get => GetValue(DecreaseContentProperty);
+ set => SetValue(DecreaseContentProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for DecreaseContent. This enables animation, styling, binding, etc...
+ public static readonly DependencyProperty DecreaseContentProperty =
+ DependencyProperty.Register(nameof(DecreaseContent), typeof(object), typeof(UpDownBase), new PropertyMetadata(null));
+
+}
+
+#if !NET8_0_OR_GREATER
+public interface IArithmetic
+{
+ T Add(T value1, T value2);
+
+ T Subtract(T value1, T value2);
+
+ int Compare(T value1, T value2);
+
+ T MinValue();
+
+ T MaxValue();
+
+ T One();
+
+ T Max(T value1, T value2);
+
+ T Min(T value1, T value2);
+
+ bool TryParse(string text, IFormatProvider? formatProvider, out T? value);
+}
+#endif
diff --git a/src/MaterialDesignThemes.Wpf/VisualStudioToolsManifest.xml b/src/MaterialDesignThemes.Wpf/VisualStudioToolsManifest.xml
index ca288c3f12..ee31e1fbc2 100644
--- a/src/MaterialDesignThemes.Wpf/VisualStudioToolsManifest.xml
+++ b/src/MaterialDesignThemes.Wpf/VisualStudioToolsManifest.xml
@@ -9,9 +9,11 @@
+
+
diff --git a/tests/MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj b/tests/MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj
index 7d2c4ebe2e..c75b048ec9 100644
--- a/tests/MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj
+++ b/tests/MaterialDesignThemes.UITests/MaterialDesignThemes.UITests.csproj
@@ -6,6 +6,7 @@
true
$(NoWarn);CA1707
true
+
diff --git a/tests/MaterialDesignThemes.UITests/TestBase.cs b/tests/MaterialDesignThemes.UITests/TestBase.cs
index 72cb417941..66615f971c 100644
--- a/tests/MaterialDesignThemes.UITests/TestBase.cs
+++ b/tests/MaterialDesignThemes.UITests/TestBase.cs
@@ -4,6 +4,7 @@
[assembly: CollectionBehavior(DisableTestParallelization = true)]
[assembly: GenerateHelpers(typeof(AutoSuggestBox))]
[assembly: GenerateHelpers(typeof(ColorPicker))]
+[assembly: GenerateHelpers(typeof(DecimalUpDown))]
[assembly: GenerateHelpers(typeof(DialogHost))]
[assembly: GenerateHelpers(typeof(DrawerHost))]
[assembly: GenerateHelpers(typeof(NumericUpDown))]
diff --git a/tests/MaterialDesignThemes.UITests/WPF/UpDownControls/DecimalUpDownTests.cs b/tests/MaterialDesignThemes.UITests/WPF/UpDownControls/DecimalUpDownTests.cs
new file mode 100644
index 0000000000..d4cad60086
--- /dev/null
+++ b/tests/MaterialDesignThemes.UITests/WPF/UpDownControls/DecimalUpDownTests.cs
@@ -0,0 +1,118 @@
+namespace MaterialDesignThemes.UITests.WPF.UpDownControls;
+
+public class DecimalUpDownTests(ITestOutputHelper output) : TestBase(output)
+{
+ [Fact]
+ public async Task NumericButtons_IncreaseAndDecreaseValue()
+ {
+ await using var recorder = new TestRecorder(App);
+
+ var numericUpDown = await LoadXaml("""
+
+ """);
+ var plusButton = await numericUpDown.GetElement("PART_IncreaseButton");
+ var minusButton = await numericUpDown.GetElement("PART_DecreaseButton");
+ var textBox = await numericUpDown.GetElement("PART_TextBox");
+
+ Assert.Equal("1", await textBox.GetText());
+ Assert.Equal(1, await numericUpDown.GetValue());
+
+ await plusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("2", await textBox.GetText());
+ Assert.Equal(2, await numericUpDown.GetValue());
+ });
+
+ await minusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("1", await textBox.GetText());
+ Assert.Equal(1, await numericUpDown.GetValue());
+ });
+ }
+
+ [Fact]
+ public async Task NumericButtons_WithMaximum_DisablesPlusButton()
+ {
+ await using var recorder = new TestRecorder(App);
+
+ var numericUpDown = await LoadXaml("""
+
+ """);
+ var plusButton = await numericUpDown.GetElement("PART_IncreaseButton");
+ var minusButton = await numericUpDown.GetElement("PART_DecreaseButton");
+ var textBox = await numericUpDown.GetElement("PART_TextBox");
+
+ await plusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("2", await textBox.GetText());
+ Assert.Equal(2, await numericUpDown.GetValue());
+ });
+
+ Assert.False(await plusButton.GetIsEnabled());
+
+ await minusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("1", await textBox.GetText());
+ Assert.Equal(1, await numericUpDown.GetValue());
+ });
+
+ Assert.True(await plusButton.GetIsEnabled());
+ }
+
+ [Fact]
+ public async Task NumericButtons_WithMinimum_DisablesMinusButton()
+ {
+ await using var recorder = new TestRecorder(App);
+
+ var numericUpDown = await LoadXaml("""
+
+ """);
+ var plusButton = await numericUpDown.GetElement("PART_IncreaseButton");
+ var minusButton = await numericUpDown.GetElement("PART_DecreaseButton");
+ var textBox = await numericUpDown.GetElement("PART_TextBox");
+
+ await minusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("1", await textBox.GetText());
+ Assert.Equal(1, await numericUpDown.GetValue());
+ });
+
+ Assert.False(await minusButton.GetIsEnabled());
+
+ await plusButton.LeftClick();
+ await Wait.For(async () =>
+ {
+ Assert.Equal("2", await textBox.GetText());
+ Assert.Equal(2, await numericUpDown.GetValue());
+ });
+
+ Assert.True(await minusButton.GetIsEnabled());
+ }
+
+ [Fact]
+ public async Task MaxAndMinAssignments_CoerceValueToBeInRange()
+ {
+ await using var recorder = new TestRecorder(App);
+
+ var numericUpDown = await LoadXaml("""
+
+ """);
+
+ await numericUpDown.SetMaximum(1);
+ Assert.Equal(1, await numericUpDown.GetValue());
+
+ await numericUpDown.SetMinimum(3);
+ Assert.Equal(3, await numericUpDown.GetValue());
+ Assert.Equal(3, await numericUpDown.GetMaximum());
+
+ await numericUpDown.SetMaximum(2);
+ Assert.Equal(3, await numericUpDown.GetValue());
+ Assert.Equal(3, await numericUpDown.GetMinimum());
+ Assert.Equal(3, await numericUpDown.GetMaximum());
+ }
+}
diff --git a/tests/MaterialDesignThemes.UITests/WPF/NumericUpDowns/NumericUpDownTests.cs b/tests/MaterialDesignThemes.UITests/WPF/UpDownControls/NumericUpDownTests.cs
similarity index 98%
rename from tests/MaterialDesignThemes.UITests/WPF/NumericUpDowns/NumericUpDownTests.cs
rename to tests/MaterialDesignThemes.UITests/WPF/UpDownControls/NumericUpDownTests.cs
index 1378801259..1f72221804 100644
--- a/tests/MaterialDesignThemes.UITests/WPF/NumericUpDowns/NumericUpDownTests.cs
+++ b/tests/MaterialDesignThemes.UITests/WPF/UpDownControls/NumericUpDownTests.cs
@@ -1,4 +1,5 @@
-namespace MaterialDesignThemes.UITests.WPF.NumericUpDowns;
+namespace MaterialDesignThemes.UITests.WPF.UpDownControls;
+
public class NumericUpDownTests(ITestOutputHelper output) : TestBase(output)
{