mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 19:57:57 +01:00
CmdpPal: SearchBox visibility and async loading race (#42783)
## Summary of the Pull Request This PR introduces two related fixes to improve the stability and reliability of navigation and search UI behavior in the shell: - **Ensure search box visibility is correctly updated** - `ShellViewModel` previously set `IsSearchBoxVisible` after navigation to the page, but didn’t update it when the value changed. While the value isn’t expected to change dynamically, the property initialization is asynchronous, which could cause a race condition. - As a defensive measure, this also changes the default value of uninitialized property to make it visible by default. - **Cancel asynchronous focus placement if navigation changes** - Ensures that any pending asynchronous focus operation is cancelled when another navigation occurs before it completes. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #42782 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -48,7 +48,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
INotifyPropertyChanged
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
@@ -65,6 +66,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
|
||||
public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService<ShellViewModel>()!;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
@@ -488,6 +492,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
if (e.Content is Page element)
|
||||
{
|
||||
_lastNavigatedPageRef = new WeakReference<Page>(element);
|
||||
element.Loaded += FocusAfterLoaded;
|
||||
}
|
||||
}
|
||||
@@ -497,6 +502,18 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
var page = (Page)sender;
|
||||
page.Loaded -= FocusAfterLoaded;
|
||||
|
||||
// Only handle focus for the latest navigated page
|
||||
if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any previous pending focus work
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = new CancellationTokenSource();
|
||||
var token = _focusAfterLoadedCts.Token;
|
||||
|
||||
AnnounceNavigationToPage(page);
|
||||
|
||||
var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
@@ -509,34 +526,57 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await page.DispatcherQueue.EnqueueAsync(async () =>
|
||||
_ = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
// I hate this so much, but it can take a while for the page to be ready to accept focus;
|
||||
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
|
||||
for (var i = 0; i < 10; i++)
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||
{
|
||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||
if (set)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the search box visibility based on the current page:
|
||||
// - We do this here after navigation so the focus is not jumping around too much,
|
||||
// it messes with screen readers if we do it too early
|
||||
// - Since this should hide the search box on content pages, it's not a problem if we
|
||||
// wait for the code above to finish trying to focus the content
|
||||
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
});
|
||||
});
|
||||
try
|
||||
{
|
||||
await page.DispatcherQueue.EnqueueAsync(
|
||||
async () =>
|
||||
{
|
||||
// I hate this so much, but it can take a while for the page to be ready to accept focus;
|
||||
// focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement)
|
||||
{
|
||||
var set = frameworkElement.Focus(FocusState.Programmatic);
|
||||
if (set)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(100, token);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
// Update the search box visibility based on the current page:
|
||||
// - We do this here after navigation so the focus is not jumping around too much,
|
||||
// it messes with screen readers if we do it too early
|
||||
// - Since this should hide the search box on content pages, it's not a problem if we
|
||||
// wait for the code above to finish trying to focus the content
|
||||
ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false;
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Swallow cancellation - another FocusAfterLoaded invocation superseded this one
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error during FocusAfterLoaded async focus work", ex);
|
||||
}
|
||||
},
|
||||
token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,4 +705,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
Logger.LogError("Error handling mouse button press event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user