From 02e2cf58ea9a6ef661e456b8b4fd42fe6f20bdcf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:33:59 +0000 Subject: [PATCH 1/8] Initial plan From c21eb67bcfc4d362de8f251187b6402cb7b90eff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:41:18 +0000 Subject: [PATCH 2/8] Fix keyboard dismiss and value update issue on Android for SfNumericEntry Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index 32aaf8df..e890ac60 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2772,6 +2772,7 @@ void AndroidEntryHandler(object? sender) androidEntry.AfterTextChanged += AndroidEntry_AfterTextChanged; androidEntry.KeyListener = global::Android.Text.Method.DigitsKeyListener.GetInstance(KEYS); androidEntry.Click += AndroidEntry_Click; + androidEntry.FocusChange += AndroidEntry_FocusChange; // Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text. androidEntry.EmojiCompatEnabled = false; } @@ -2877,6 +2878,41 @@ void AndroidEntry_Click(object? sender, EventArgs e) } } } + + /// + /// Handles the focus change event for an Android Entry control. + /// Ensures proper value update and formatting when focus is lost. + /// This fixes the issue where clicking a button doesn't update the value + /// or dismiss the keyboard because the entry doesn't properly lose focus. + /// + /// The object that triggered the event, typically an Android Entry component. + /// The focus change event arguments. + void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEventArgs e) + { + // When focus is lost (HasFocus is false), ensure proper cleanup + // This is crucial for Android where tapping outside (e.g., on a button) + // doesn't always trigger the proper unfocus sequence in MAUI + if (!e.HasFocus && _textBox != null) + { + // The native Android entry has lost focus + // Ensure the value is updated if ValueChangeMode is OnLostFocus (default) + // or if there are pending changes that need to be committed + MainThread.BeginInvokeOnMainThread(() => + { + // Only process if the text box still exists and lost focus + if (_textBox != null) + { + // Update the value to ensure it's synchronized with the bound property + // This handles the case where a button is clicked before the value was updated + if (ValueChangeMode == ValueChangeMode.OnLostFocus) + { + UpdateValue(); + FormatValue(); + } + } + }); + } + } #endif /// From 51bcd5b5e07779132e4eb7b21ad3efe584d81251 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:42:02 +0000 Subject: [PATCH 3/8] Refine Android focus change handler to avoid duplicate processing Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index e890ac60..71d19646 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2889,26 +2889,22 @@ void AndroidEntry_Click(object? sender, EventArgs e) /// The focus change event arguments. void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEventArgs e) { - // When focus is lost (HasFocus is false), ensure proper cleanup + // When focus is lost (HasFocus is false), ensure the MAUI layer is notified // This is crucial for Android where tapping outside (e.g., on a button) - // doesn't always trigger the proper unfocus sequence in MAUI + // doesn't always properly sync the native focus state with MAUI's focus state if (!e.HasFocus && _textBox != null) { // The native Android entry has lost focus - // Ensure the value is updated if ValueChangeMode is OnLostFocus (default) - // or if there are pending changes that need to be committed + // Check if MAUI's Entry is still reporting as focused + // If so, we need to trigger the unfocus to ensure proper event handling MainThread.BeginInvokeOnMainThread(() => { - // Only process if the text box still exists and lost focus - if (_textBox != null) + if (_textBox != null && _textBox.IsFocused) { - // Update the value to ensure it's synchronized with the bound property - // This handles the case where a button is clicked before the value was updated - if (ValueChangeMode == ValueChangeMode.OnLostFocus) - { - UpdateValue(); - FormatValue(); - } + // MAUI still thinks the entry is focused, but the native control has lost focus + // This can happen when clicking outside (e.g., on a button) + // Calling Unfocus() will trigger TextBoxOnLostFocus -> OnLostFocus() -> UpdateValue() -> FormatValue() + _textBox.Unfocus(); } }); } From fa2285461c3aaf2522091afda72c3dc05f570da5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:44:31 +0000 Subject: [PATCH 4/8] Add proper cleanup for Android event handlers to prevent memory leaks Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 37 ++++++++++++++----- maui/src/NumericEntry/SfNumericEntry.cs | 5 +++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index 71d19646..188c7e95 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2764,17 +2764,34 @@ void WindowEntryHandler(object? sender) void AndroidEntryHandler(object? sender) { #if ANDROID - if ((sender is SfEntryView textBox) && textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry) + if (sender is SfEntryView textBox) { - androidEntry.EditorAction += AndroidEntry_EditorAction; - androidEntry.TextChanged += OnTextBoxTextChanged; - androidEntry.BeforeTextChanged += AndroidEntry_BeforeTextChanged; - androidEntry.AfterTextChanged += AndroidEntry_AfterTextChanged; - androidEntry.KeyListener = global::Android.Text.Method.DigitsKeyListener.GetInstance(KEYS); - androidEntry.Click += AndroidEntry_Click; - androidEntry.FocusChange += AndroidEntry_FocusChange; - // Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text. - androidEntry.EmojiCompatEnabled = false; + if (textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry) + { + // Store reference to the Android entry for cleanup + _androidEntry = androidEntry; + + androidEntry.EditorAction += AndroidEntry_EditorAction; + androidEntry.TextChanged += OnTextBoxTextChanged; + androidEntry.BeforeTextChanged += AndroidEntry_BeforeTextChanged; + androidEntry.AfterTextChanged += AndroidEntry_AfterTextChanged; + androidEntry.KeyListener = global::Android.Text.Method.DigitsKeyListener.GetInstance(KEYS); + androidEntry.Click += AndroidEntry_Click; + androidEntry.FocusChange += AndroidEntry_FocusChange; + // Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text. + androidEntry.EmojiCompatEnabled = false; + } + else if (textBox.Handler == null && _androidEntry != null) + { + // Clean up event handlers when handler is removed + _androidEntry.EditorAction -= AndroidEntry_EditorAction; + _androidEntry.TextChanged -= OnTextBoxTextChanged; + _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; + _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; + _androidEntry.Click -= AndroidEntry_Click; + _androidEntry.FocusChange -= AndroidEntry_FocusChange; + _androidEntry = null; + } } #endif } diff --git a/maui/src/NumericEntry/SfNumericEntry.cs b/maui/src/NumericEntry/SfNumericEntry.cs index e4451e22..47e49af0 100644 --- a/maui/src/NumericEntry/SfNumericEntry.cs +++ b/maui/src/NumericEntry/SfNumericEntry.cs @@ -300,6 +300,11 @@ public partial class SfNumericEntry : SfView, ITextElement, ITouchListener, IKey /// bool _isFirstFocus = true; + /// + /// Represents the underlying Android AppCompatEditText used in the control. + /// + AndroidX.AppCompat.Widget.AppCompatEditText? _androidEntry; + #endif #if !WINDOWS From 4f38ea111b0e0a84a71b7e96213e620d7305b96b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:45:51 +0000 Subject: [PATCH 5/8] Address code review feedback: improve cleanup logic and thread safety Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index 188c7e95..235b4956 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2766,6 +2766,19 @@ void AndroidEntryHandler(object? sender) #if ANDROID if (sender is SfEntryView textBox) { + // Clean up existing event handlers if we have a previous Android entry + if (_androidEntry != null) + { + _androidEntry.EditorAction -= AndroidEntry_EditorAction; + _androidEntry.TextChanged -= OnTextBoxTextChanged; + _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; + _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; + _androidEntry.Click -= AndroidEntry_Click; + _androidEntry.FocusChange -= AndroidEntry_FocusChange; + _androidEntry = null; + } + + // Subscribe to new handler's platform view if available if (textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry) { // Store reference to the Android entry for cleanup @@ -2781,17 +2794,6 @@ void AndroidEntryHandler(object? sender) // Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text. androidEntry.EmojiCompatEnabled = false; } - else if (textBox.Handler == null && _androidEntry != null) - { - // Clean up event handlers when handler is removed - _androidEntry.EditorAction -= AndroidEntry_EditorAction; - _androidEntry.TextChanged -= OnTextBoxTextChanged; - _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; - _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; - _androidEntry.Click -= AndroidEntry_Click; - _androidEntry.FocusChange -= AndroidEntry_FocusChange; - _androidEntry = null; - } } #endif } @@ -2909,13 +2911,15 @@ void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEven // When focus is lost (HasFocus is false), ensure the MAUI layer is notified // This is crucial for Android where tapping outside (e.g., on a button) // doesn't always properly sync the native focus state with MAUI's focus state - if (!e.HasFocus && _textBox != null) + if (!e.HasFocus) { // The native Android entry has lost focus // Check if MAUI's Entry is still reporting as focused // If so, we need to trigger the unfocus to ensure proper event handling MainThread.BeginInvokeOnMainThread(() => { + // Re-check _textBox for null inside MainThread callback + // as it could be set to null on another thread if (_textBox != null && _textBox.IsFocused) { // MAUI still thinks the entry is focused, but the native control has lost focus From 74789686aec94ad14237b0a9c7dc1439f61a099e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:47:27 +0000 Subject: [PATCH 6/8] Add comprehensive null checks and cleanup to UnHookEvents Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index 235b4956..24e8decb 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2908,6 +2908,12 @@ void AndroidEntry_Click(object? sender, EventArgs e) /// The focus change event arguments. void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEventArgs e) { + // Verify sender is valid before processing + if (sender == null || _androidEntry == null) + { + return; + } + // When focus is lost (HasFocus is false), ensure the MAUI layer is notified // This is crucial for Android where tapping outside (e.g., on a button) // doesn't always properly sync the native focus state with MAUI's focus state @@ -2918,9 +2924,9 @@ void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEven // If so, we need to trigger the unfocus to ensure proper event handling MainThread.BeginInvokeOnMainThread(() => { - // Re-check _textBox for null inside MainThread callback - // as it could be set to null on another thread - if (_textBox != null && _textBox.IsFocused) + // Re-check all references for null inside MainThread callback + // as they could be set to null on another thread during cleanup + if (_textBox != null && _androidEntry != null && _textBox.IsFocused) { // MAUI still thinks the entry is focused, but the native control has lost focus // This can happen when clicking outside (e.g., on a button) @@ -2948,6 +2954,20 @@ void UnHookEvents() _textBox.Unfocused -= TextBoxOnLostFocus; } + +#if ANDROID + // Clean up Android-specific event handlers + if (_androidEntry != null) + { + _androidEntry.EditorAction -= AndroidEntry_EditorAction; + _androidEntry.TextChanged -= OnTextBoxTextChanged; + _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; + _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; + _androidEntry.Click -= AndroidEntry_Click; + _androidEntry.FocusChange -= AndroidEntry_FocusChange; + _androidEntry = null; + } +#endif } #endregion From 21de4b5de7d3456d124984ecc7319f1486ef9c29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:49:22 +0000 Subject: [PATCH 7/8] Extract cleanup method and fix race condition with local references Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index 24e8decb..b97dde6a 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2756,6 +2756,25 @@ void WindowEntryHandler(object? sender) } } + /// + /// Cleans up Android entry event handlers to prevent memory leaks. + /// + void CleanupAndroidEntryEvents() + { +#if ANDROID + if (_androidEntry != null) + { + _androidEntry.EditorAction -= AndroidEntry_EditorAction; + _androidEntry.TextChanged -= OnTextBoxTextChanged; + _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; + _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; + _androidEntry.Click -= AndroidEntry_Click; + _androidEntry.FocusChange -= AndroidEntry_FocusChange; + _androidEntry = null; + } +#endif + } + /// /// Configures platform-specific behavior for Android Entry controls. /// This method is called when the Entry's handler changes. @@ -2767,16 +2786,7 @@ void AndroidEntryHandler(object? sender) if (sender is SfEntryView textBox) { // Clean up existing event handlers if we have a previous Android entry - if (_androidEntry != null) - { - _androidEntry.EditorAction -= AndroidEntry_EditorAction; - _androidEntry.TextChanged -= OnTextBoxTextChanged; - _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; - _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; - _androidEntry.Click -= AndroidEntry_Click; - _androidEntry.FocusChange -= AndroidEntry_FocusChange; - _androidEntry = null; - } + CleanupAndroidEntryEvents(); // Subscribe to new handler's platform view if available if (textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry) @@ -2908,8 +2918,13 @@ void AndroidEntry_Click(object? sender, EventArgs e) /// The focus change event arguments. void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEventArgs e) { - // Verify sender is valid before processing - if (sender == null || _androidEntry == null) + // Capture local references to avoid race conditions + // The _androidEntry and _textBox could be set to null on another thread + var androidEntry = _androidEntry; + var textBox = _textBox; + + // Verify references are valid before processing + if (sender == null || androidEntry == null || textBox == null) { return; } @@ -2924,14 +2939,14 @@ void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEven // If so, we need to trigger the unfocus to ensure proper event handling MainThread.BeginInvokeOnMainThread(() => { - // Re-check all references for null inside MainThread callback - // as they could be set to null on another thread during cleanup - if (_textBox != null && _androidEntry != null && _textBox.IsFocused) + // Use the local captured references to avoid race conditions + // Only proceed if the textBox is still focused + if (textBox.IsFocused) { // MAUI still thinks the entry is focused, but the native control has lost focus // This can happen when clicking outside (e.g., on a button) // Calling Unfocus() will trigger TextBoxOnLostFocus -> OnLostFocus() -> UpdateValue() -> FormatValue() - _textBox.Unfocus(); + textBox.Unfocus(); } }); } @@ -2955,19 +2970,8 @@ void UnHookEvents() } -#if ANDROID // Clean up Android-specific event handlers - if (_androidEntry != null) - { - _androidEntry.EditorAction -= AndroidEntry_EditorAction; - _androidEntry.TextChanged -= OnTextBoxTextChanged; - _androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged; - _androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged; - _androidEntry.Click -= AndroidEntry_Click; - _androidEntry.FocusChange -= AndroidEntry_FocusChange; - _androidEntry = null; - } -#endif + CleanupAndroidEntryEvents(); } #endregion From ccb4f5e3269659b9765a76804dc1f1018bb0b31e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 15:50:28 +0000 Subject: [PATCH 8/8] Add exception handling for disposed control edge case Co-authored-by: PaulAndersonS <42271912+PaulAndersonS@users.noreply.github.com> --- .../NumericEntry/SfNumericEntry.Methods.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/maui/src/NumericEntry/SfNumericEntry.Methods.cs b/maui/src/NumericEntry/SfNumericEntry.Methods.cs index b97dde6a..9c4652f1 100644 --- a/maui/src/NumericEntry/SfNumericEntry.Methods.cs +++ b/maui/src/NumericEntry/SfNumericEntry.Methods.cs @@ -2939,14 +2939,22 @@ void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEven // If so, we need to trigger the unfocus to ensure proper event handling MainThread.BeginInvokeOnMainThread(() => { - // Use the local captured references to avoid race conditions - // Only proceed if the textBox is still focused - if (textBox.IsFocused) + try { - // MAUI still thinks the entry is focused, but the native control has lost focus - // This can happen when clicking outside (e.g., on a button) - // Calling Unfocus() will trigger TextBoxOnLostFocus -> OnLostFocus() -> UpdateValue() -> FormatValue() - textBox.Unfocus(); + // Use the local captured references to avoid race conditions + // Only proceed if the textBox is still focused + if (textBox.IsFocused) + { + // MAUI still thinks the entry is focused, but the native control has lost focus + // This can happen when clicking outside (e.g., on a button) + // Calling Unfocus() will trigger TextBoxOnLostFocus -> OnLostFocus() -> UpdateValue() -> FormatValue() + textBox.Unfocus(); + } + } + catch (ObjectDisposedException) + { + // The control was disposed between capturing the reference and using it + // This is safe to ignore as the control no longer needs to be unfocused } }); }