From f6d588f3fc74d7e515731978b60cb28c5aae9acf Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 31 Jan 2023 18:17:24 +0000 Subject: [PATCH 1/4] [Windows] Fix issue with caching the page that holds unfocused control --- .../DeviceTests/Elements/Entry/EntryTests.cs | 46 +++++++++++++++++++ .../src/Platform/Windows/ViewExtensions.cs | 43 ++++++++--------- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs index f5128a000a5e..1290b1bffba7 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs @@ -1,7 +1,9 @@ using System; using System.Threading.Tasks; using Microsoft.Maui.Controls; +using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; using Xunit; namespace Microsoft.Maui.DeviceTests @@ -52,6 +54,50 @@ await InvokeOnMainThreadAsync(() => Assert.True(platformControl.IsReadOnly); }); } + + + [Fact(DisplayName = "Unfocus will work when page is shown a 2nd time")] + public async Task UnFocusOnEntryAfterPagePop() + { + int unfocused = 0; + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(typeof(Toolbar), typeof(ToolbarHandler)); + handlers.AddHandler(typeof(NavigationPage), typeof(NavigationViewHandler)); + handlers.AddHandler(); + handlers.AddHandler(typeof(Window), typeof(WindowHandlerStub)); + handlers.AddHandler(typeof(Entry), typeof(EntryHandler)); + + }); + }); + + var entry = new Entry(); + entry.Unfocused += (s, e) => + { + if(!e.IsFocused) + { + unfocused++; + } + + }; + var navPage = new NavigationPage(new ContentPage { Content = entry }); + var window = new Window(navPage); + + await CreateHandlerAndAddToWindow(window, async (handler) => + { + entry.Focus(); + await Task.Delay(500); + entry.Unfocus(); + await navPage.PushAsync(new ContentPage()); + await navPage.PopAsync(); + entry.Focus(); + await Task.Delay(500); + entry.Unfocus(); + Assert.True(unfocused == 2); + }); + } #endif [Theory(DisplayName = "CursorPosition Initializes Correctly")] diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 2120a2c944e9..150d48adc3c2 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -19,8 +19,6 @@ namespace Microsoft.Maui.Platform { public static partial class ViewExtensions { - internal static Page? ContainingPage; // Cache of containing page used for unfocusing - public static void TryMoveFocus(this FrameworkElement platformView, FocusNavigationDirection direction) { if (platformView?.XamlRoot?.Content is UIElement elem) @@ -368,34 +366,33 @@ internal static void UnfocusControl(Control control) // hack. What we *can* do is set the focus to the Page which contains Control; // this will cause Control to lose focus without shifting focus to, say, the next Entry - if (ContainingPage == null) - { - // Work our way up the tree to find the containing Page - DependencyObject parent = control; - - while (parent != null && parent is not Page) - { - parent = VisualTreeHelper.GetParent(parent); - } + // Work our way up the tree to find the containing Page + // We can't cache this parent because this Page can be disposed + // or the Control can belong to another page + DependencyObject parent = control; - ContainingPage = parent as Page; + while (parent != null && parent is not Page) + { + parent = VisualTreeHelper.GetParent(parent); } - if (ContainingPage != null) - { - // Cache the tabstop setting - var wasTabStop = ContainingPage.IsTabStop; + if (parent is not Page containingPage) + return; - // Controls can only get focus if they're a tabstop - ContainingPage.IsTabStop = true; - ContainingPage.Focus(FocusState.Programmatic); + // Cache the tabstop setting + var wasTabStop = containingPage.IsTabStop; - // Restore the tabstop setting; that may cause the Page to lose focus, - // but it won't restore the focus to Control - ContainingPage.IsTabStop = wasTabStop; - } + // Controls can only get focus if they're a tabstop + containingPage.IsTabStop = true; + containingPage.Focus(FocusState.Programmatic); + + // Restore the tabstop setting; that may cause the Page to lose focus, + // but it won't restore the focus to Control + containingPage.IsTabStop = wasTabStop; } + + internal static IWindow? GetHostedWindow(this IView? view) => GetHostedWindow(view?.Handler?.PlatformView as FrameworkElement); From 159b8250d9225a0bc2a72d707a9e7f9a31230c34 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 7 Feb 2023 14:35:26 +0000 Subject: [PATCH 2/4] Simplify condition --- src/Core/src/Platform/Windows/ViewExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 150d48adc3c2..3aa1f2211ad2 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -371,7 +371,7 @@ internal static void UnfocusControl(Control control) // or the Control can belong to another page DependencyObject parent = control; - while (parent != null && parent is not Page) + while (parent is not Page) { parent = VisualTreeHelper.GetParent(parent); } From 157a03d9173f98fba902d138fcde5db55f4ba4e1 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 7 Feb 2023 15:43:16 +0000 Subject: [PATCH 3/4] [test] Use AutoResetEvent for entry focus test --- .../DeviceTests/Elements/Entry/EntryTests.cs | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs index 1290b1bffba7..c9bad6489962 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Maui.Controls; using Microsoft.Maui.DeviceTests.Stubs; @@ -55,7 +56,7 @@ await InvokeOnMainThreadAsync(() => }); } - + [Fact(DisplayName = "Unfocus will work when page is shown a 2nd time")] public async Task UnFocusOnEntryAfterPagePop() { @@ -72,30 +73,47 @@ public async Task UnFocusOnEntryAfterPagePop() }); }); - + AutoResetEvent _focused = new AutoResetEvent(false); + AutoResetEvent _unFocused = new AutoResetEvent(false); var entry = new Entry(); entry.Unfocused += (s, e) => { - if(!e.IsFocused) + if (!e.IsFocused) { unfocused++; } - + _unFocused.Set(); }; - var navPage = new NavigationPage(new ContentPage { Content = entry }); + var navPage = new NavigationPage(new ContentPage { Content = entry }); var window = new Window(navPage); await CreateHandlerAndAddToWindow(window, async (handler) => { - entry.Focus(); - await Task.Delay(500); - entry.Unfocus(); - await navPage.PushAsync(new ContentPage()); - await navPage.PopAsync(); - entry.Focus(); - await Task.Delay(500); - entry.Unfocus(); - Assert.True(unfocused == 2); + await Task.Run(() => + { + InvokeOnMainThreadAsync(() => + { + entry.Focused += (s, e) => _focused.Set(); + entry.Focus(); + }); + _focused.WaitOne(); + _focused.Reset(); + InvokeOnMainThreadAsync(async () => + { + entry.Unfocus(); + await navPage.PushAsync(new ContentPage()); + await navPage.PopAsync(); + entry.Focus(); + }); + _focused.WaitOne(); + _unFocused.Reset(); + InvokeOnMainThreadAsync(() => + { + entry.Unfocus(); + }); + _unFocused.WaitOne(); + Assert.True(unfocused == 2); + }); }); } #endif From 5a8faaed5295329c3ef04f60ffa641296e3d5fcf Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 8 Feb 2023 12:19:34 +0000 Subject: [PATCH 4/4] Use tabStop to unfocus --- .../src/Platform/Windows/ViewExtensions.cs | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/src/Core/src/Platform/Windows/ViewExtensions.cs b/src/Core/src/Platform/Windows/ViewExtensions.cs index 3aa1f2211ad2..5b71fba725c4 100644 --- a/src/Core/src/Platform/Windows/ViewExtensions.cs +++ b/src/Core/src/Platform/Windows/ViewExtensions.cs @@ -359,40 +359,13 @@ internal static void UnfocusControl(Control control) if (control == null || !control.IsEnabled) return; - // "Unfocusing" doesn't really make sense on Windows; for accessibility reasons, - // something always has focus. So forcing the unfocusing of a control would normally - // just move focus to the next control, or leave it on the current control if no other - // focus targets are available. This is what happens if you use the "disable/enable" - // hack. What we *can* do is set the focus to the Page which contains Control; - // this will cause Control to lose focus without shifting focus to, say, the next Entry - - // Work our way up the tree to find the containing Page - // We can't cache this parent because this Page can be disposed - // or the Control can belong to another page - DependencyObject parent = control; - - while (parent is not Page) - { - parent = VisualTreeHelper.GetParent(parent); - } - - if (parent is not Page containingPage) - return; - - // Cache the tabstop setting - var wasTabStop = containingPage.IsTabStop; - - // Controls can only get focus if they're a tabstop - containingPage.IsTabStop = true; - containingPage.Focus(FocusState.Programmatic); - - // Restore the tabstop setting; that may cause the Page to lose focus, - // but it won't restore the focus to Control - containingPage.IsTabStop = wasTabStop; + var isTabStop = control.IsTabStop; + control.IsTabStop = false; + control.IsEnabled = false; + control.IsEnabled = true; + control.IsTabStop = isTabStop; } - - internal static IWindow? GetHostedWindow(this IView? view) => GetHostedWindow(view?.Handler?.PlatformView as FrameworkElement);