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);