diff --git a/src/MahApps.Metro/Controls/NumericUpDown.cs b/src/MahApps.Metro/Controls/NumericUpDown.cs
index 84a3cc497..0312160a7 100644
--- a/src/MahApps.Metro/Controls/NumericUpDown.cs
+++ b/src/MahApps.Metro/Controls/NumericUpDown.cs
@@ -998,6 +998,19 @@ public override void OnApplyTemplate()
{
base.OnApplyTemplate();
+ // Clean up old event handlers to prevent memory leaks
+ if (this.repeatUp != null)
+ {
+ this.repeatUp.Click -= this.OnRepeatUpClick;
+ this.repeatUp.PreviewMouseUp -= this.OnRepeatButtonMouseUp;
+ }
+
+ if (this.repeatDown != null)
+ {
+ this.repeatDown.Click -= this.OnRepeatDownClick;
+ this.repeatDown.PreviewMouseUp -= this.OnRepeatButtonMouseUp;
+ }
+
this.repeatUp = this.GetTemplateChild(PART_NumericUp) as RepeatButton;
this.repeatDown = this.GetTemplateChild(PART_NumericDown) as RepeatButton;
@@ -1010,17 +1023,33 @@ public override void OnApplyTemplate()
this.ToggleReadOnlyMode(this.IsReadOnly);
- this.repeatUp.Click += (_, _) => { this.ChangeValueWithSpeedUp(true); };
- this.repeatDown.Click += (_, _) => { this.ChangeValueWithSpeedUp(false); };
+ // Use named methods for event handlers so they can be properly removed
+ this.repeatUp.Click += this.OnRepeatUpClick;
+ this.repeatDown.Click += this.OnRepeatDownClick;
- this.repeatUp.PreviewMouseUp += (_, _) => this.ResetInternal();
- this.repeatDown.PreviewMouseUp += (_, _) => this.ResetInternal();
+ this.repeatUp.PreviewMouseUp += this.OnRepeatButtonMouseUp;
+ this.repeatDown.PreviewMouseUp += this.OnRepeatButtonMouseUp;
this.OnValueChanged(this.Value, this.Value);
this.scrollViewer = null;
}
+ private void OnRepeatUpClick(object sender, RoutedEventArgs e)
+ {
+ this.ChangeValueWithSpeedUp(true);
+ }
+
+ private void OnRepeatDownClick(object sender, RoutedEventArgs e)
+ {
+ this.ChangeValueWithSpeedUp(false);
+ }
+
+ private void OnRepeatButtonMouseUp(object sender, MouseButtonEventArgs e)
+ {
+ this.ResetInternal();
+ }
+
///
/// Creates AutomationPeer ()
///
@@ -1170,9 +1199,10 @@ protected void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
var textBox = (TextBox)sender;
var fullText = textBox.Text.Remove(textBox.SelectionStart, textBox.SelectionLength).Insert(textBox.CaretIndex, e.Text);
var textIsValid = this.ValidateText(fullText, out var convertedValue);
- // Value must be valid and not coerced
- var coerceValue = CoerceValue(this, convertedValue as double?);
- e.Handled = !textIsValid || !coerceValue.isValid;
+
+ // Allow typing intermediate values (e.g., typing "5" when minimum is 50)
+ // Only block input if the text format is invalid, not if it would be coerced
+ e.Handled = !textIsValid;
this.manualChange = !e.Handled;
}
@@ -1338,16 +1368,25 @@ private static bool TryFormatHexadecimal(double newValue, string format, Culture
var match = RegexStringFormatHexadecimal.Match(format);
if (match.Success)
{
+ // Validate value is within int range and has no decimal part
+ if (newValue < int.MinValue || newValue > int.MaxValue || Math.Abs(newValue % 1) > double.Epsilon)
+ {
+ output = null;
+ return false;
+ }
+
+ var intValue = (int)newValue;
+
if (match.Groups["simpleHEX"].Success)
{
// HEX DOES SUPPORT INT ONLY.
- output = ((int)newValue).ToString(match.Groups["simpleHEX"].Value, culture);
+ output = intValue.ToString(match.Groups["simpleHEX"].Value, culture);
return true;
}
if (match.Groups["complexHEX"].Success)
{
- output = string.Format(culture, match.Groups["complexHEX"].Value, (int)newValue);
+ output = string.Format(culture, match.Groups["complexHEX"].Value, intValue);
return true;
}
}
@@ -1450,7 +1489,9 @@ private void SetValueTo(double newValue)
value = this.Minimum;
}
- this.SetCurrentValue(ValueProperty, CoerceValue(this, value).value);
+ // Remove manual coercion - the dependency property system will handle it automatically
+ // via the CoerceValueCallback registered on ValueProperty
+ this.SetCurrentValue(ValueProperty, value);
}
private void EnableDisableUpDown()
@@ -1494,10 +1535,7 @@ private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
/// The TextBox which will be used for the correction
/// The decimal correction mode.
/// The culture with the decimal-point information.
- ///
- /// Typical "async-void" pattern as "fire-and-forget" behavior.
- ///
- private static async void SimulateDecimalPointKeyPress(TextBoxBase textBox, DecimalPointCorrectionMode mode, CultureInfo culture)
+ private static void SimulateDecimalPointKeyPress(TextBoxBase textBox, DecimalPointCorrectionMode mode, CultureInfo culture)
{
// Select the proper decimal-point string upon the context
string? replace;
@@ -1527,8 +1565,6 @@ private static async void SimulateDecimalPointKeyPress(TextBoxBase textBox, Deci
TextCompositionManager.StartComposition(tc);
}
-
- await Task.FromResult(true);
}
private void OnTextBoxLostFocus(object? sender, RoutedEventArgs e)
@@ -1590,7 +1626,9 @@ private void ChangeValueFromTextInput(string text)
}
}
- this.OnValueChanged(oldValue, this.Value);
+ // REMOVED: this.OnValueChanged(oldValue, this.Value);
+ // SetValueTo already triggers ValueProperty change which calls OnValueChanged
+ // Calling it again causes double event firing
this.manualChange = false;
}
diff --git a/src/MahApps.Metro/Controls/TimePicker/DateTimePicker.cs b/src/MahApps.Metro/Controls/TimePicker/DateTimePicker.cs
index d3243957e..b2b2e7615 100644
--- a/src/MahApps.Metro/Controls/TimePicker/DateTimePicker.cs
+++ b/src/MahApps.Metro/Controls/TimePicker/DateTimePicker.cs
@@ -340,7 +340,8 @@ protected override void SetSelectedDateTime()
return;
}
- if (DateTime.TryParse(this.textBox.Text, this.SpecificCultureInfo, System.Globalization.DateTimeStyles.None, out var dateTime))
+ // Use AssumeLocal to ensure consistent DateTimeKind.Local
+ if (DateTime.TryParse(this.textBox.Text, this.SpecificCultureInfo, System.Globalization.DateTimeStyles.AssumeLocal, out var dateTime))
{
this.SetCurrentValue(SelectedDateTimeProperty, dateTime);
this.SetCurrentValue(DisplayDateProperty, dateTime);