CmdPal: Fix dock popup XamlRoot handling on DockControl (#46305)

## Summary of the Pull Request

This PR handles situations when app can crash because a popup control is
being touched before XamlRoot is set.

- Registers message handlers in DockControl only while controls are
loaded.
- Guards the edit-mode TeachingTip until the dock is rooted.
- Sets XamlRoot for dock flyouts and tooltips before showing.
- Ensures that tooltips in DockItemControl are set only after XamlRoot
is explicitly set.
- Unregisteres messages and CenterItems_CollectionChanged when unloaded.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46228
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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:
Jiří Polášek
2026-03-24 19:18:59 +01:00
committed by GitHub
parent 93f80f5f61
commit 735ea01a93
2 changed files with 108 additions and 9 deletions

View File

@@ -67,15 +67,52 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
_viewModel = viewModel;
InitializeComponent();
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
ViewModel.CenterItems.CollectionChanged += CenterItems_CollectionChanged;
Loaded += DockControl_Loaded;
Unloaded += DockControl_Unloaded;
// Start with edit mode disabled - normal click behavior
UpdateEditMode(false);
}
private void DockControl_Loaded(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.UnregisterAll(this);
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
ViewModel.CenterItems.CollectionChanged += CenterItems_CollectionChanged;
UpdateEditModeTeachingTip();
}
private void DockControl_Unloaded(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.UnregisterAll(this);
ViewModel.CenterItems.CollectionChanged -= CenterItems_CollectionChanged;
if (EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = false;
}
if (ContextMenuFlyout.IsOpen)
{
ContextMenuFlyout.Hide();
}
if (AddBandFlyout.IsOpen)
{
AddBandFlyout.Hide();
}
if (EditModeContextMenu.IsOpen)
{
EditModeContextMenu.Hide();
}
}
private void CenterItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateCenterVisibility();
@@ -125,7 +162,38 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
};
}
EditButtonsTeachingTip.IsOpen = isEditMode;
UpdateEditModeTeachingTip();
}
private void UpdateEditModeTeachingTip()
{
if (XamlRoot is null || ContentGrid.XamlRoot is null || EditButtonsTeachingTip.Parent is null)
{
return;
}
if (!IsEditMode)
{
if (EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = false;
}
return;
}
if (!EditButtonsTeachingTip.IsOpen)
{
EditButtonsTeachingTip.IsOpen = true;
}
}
private static void PreparePopupForShow(FlyoutBase popup, FrameworkElement placementTarget)
{
if (placementTarget.XamlRoot is not null && popup.XamlRoot != placementTarget.XamlRoot)
{
popup.XamlRoot = placementTarget.XamlRoot;
}
}
internal void EnterEditMode()
@@ -214,6 +282,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
PreparePopupForShow(EditModeContextMenu, dockItem);
EditModeContextMenu.ShowAt(
dockItem,
new FlyoutShowOptions()
@@ -232,6 +301,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = true;
PreparePopupForShow(ContextMenuFlyout, dockItem);
ContextMenuFlyout.ShowAt(
dockItem,
new FlyoutShowOptions()
@@ -320,6 +390,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
ContextControl.ViewModel.SelectedItem = item;
ContextControl.ShowFilterBox = false;
PreparePopupForShow(ContextMenuFlyout, RootGrid);
ContextMenuFlyout.ShowAt(
this.RootGrid,
new FlyoutShowOptions()
@@ -516,6 +587,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
// Show the flyout
PreparePopupForShow(AddBandFlyout, button);
AddBandFlyout.ShowAt(button);
}
}

View File

@@ -35,10 +35,7 @@ public sealed partial class DockItemControl : Control
{
if (d is DockItemControl control)
{
// Collapse the tooltip when the string is null or empty so an
// empty tooltip bubble doesn't appear on hover.
var text = e.NewValue as string;
ToolTipService.SetToolTip(control, string.IsNullOrEmpty(text) ? null : text);
control.UpdateToolTip();
}
}
@@ -91,6 +88,7 @@ public sealed partial class DockItemControl : Control
private FrameworkElement? _iconPresenter;
private DockControl? _parentDock;
private ToolTip? _toolTip;
private long _dockSideCallbackToken = -1;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
@@ -184,9 +182,33 @@ public sealed partial class DockItemControl : Control
{
UpdateTextVisibility();
UpdateIconVisibility();
UpdateToolTip();
UpdateAlignment();
}
private void UpdateToolTip()
{
var text = ToolTip;
if (string.IsNullOrEmpty(text))
{
ToolTipService.SetToolTip(this, null);
_toolTip = null;
return;
}
// Wait until the control is connected to a XamlRoot before creating
// the tooltip popup; dock items are materialized very early in startup.
if (XamlRoot is null)
{
return;
}
_toolTip ??= new ToolTip();
_toolTip.Content = text;
_toolTip.XamlRoot = XamlRoot;
ToolTipService.SetToolTip(this, _toolTip);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
@@ -232,6 +254,8 @@ public sealed partial class DockItemControl : Control
DockControl.DockSideProperty,
OnParentDockSideChanged);
}
UpdateToolTip();
}
private void DockItemControl_ActualThemeChanged(FrameworkElement sender, object args)
@@ -250,6 +274,9 @@ public sealed partial class DockItemControl : Control
_dockSideCallbackToken = -1;
_parentDock = null;
}
ToolTipService.SetToolTip(this, null);
_toolTip = null;
}
private void OnParentDockSideChanged(DependencyObject sender, DependencyProperty dp)