Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/3.1] Context Menus are sometimes not shown in High-DPI applications #2098

Merged
2 commits merged into from Oct 29, 2019

Conversation

vatsan-madhavan
Copy link
Member

Addresses #2088
.NET 5 PR: #2097

Description (Summary)

As part of a previous fix (that shipped originally as part of .NET 4.8 in), a change was made to Popup that involved the destruction and recreation of the underlying HWND. This was done to ensure that the HWND was always created with the correct monitor (~=DPI) affinity.

This previous fix depended upon a private helper-method (Popup.DestroyWindow) that had side-effects that were not accounted for in the original fix. One of the side effects is that the ContextMenu is not sown consistently.

The solution modifies (refactors) the private method to extract the useful portion and uses it to improve the previous fix.

Customer Impact

This is a forward-port from from .NET 4.8. This was reported by a customer, and has also been discovered by Visual Studio internally.

Regression

Regression introduced by .NET 4.8. .NET Core 3.0 shipped with this bug.

Risk

Low - this has been well tested internally and validated by multiple customers. The fix is well understood and small/scoped.

Details

As part of a previous fix (that shipped originally as part of .NET 4.8 in), a change was made to Popup that involved the destruction and recreation of the underlying HWND. This was done to ensure that the HWND was always created with the correct monitor (~=DPI) affinity.

In order to acheive this, DestroyWindow() and BuildWindow() - private methods in Popup - were used.

We are finding that DestroyWindow() has side-effects that can lead to incorrect behavior. The incorrect beahvior works as follows:

  • The call into Popup.CreateWindow is usually a consequence Popup.IsOpenChanged (false -> true)
  • Within Popup.CreateWindow, DestroyWindow() is called (when high-dpi mode is detected).
  • DestroyWindow() destroys the underlying HWND, releases the capture, raises the OnClosed event and clears the placement-target.
    • At the end of DestroyWindow(), we get IsOpen == false.
  • After DestroyWindow(), we call into BuildWindow() and CreateNewPopupRoot() etc., which go on to build the Popup again (and also instnatiate a new HWND).
    • Unfortunately, there is no mechanism here for resetting IsOpen back to true (without also leading to an undesirable infinite-recursion that calls back into CreateWindow).

If these calls to show the context-menu arise from ContextMenu.OnIsOpenChanged, and flow through ContextMenu.HookupParentPopup -> Popup.CreateRootPopup, then IsOpen gets reset (there is a direct call into SetBinding(IsOpenProperty), in Popup.CreateRootPopupInternal) and the popup is shown correctly. Until then, the context-menu is "stuck" not being able to be shown.

The solution is to stop using DestroyWindow() as-is, which does more than what we need for it to accomplish. Our original intent in calling DestroyWindow() was simply destroy and recreate the HWND. This fix refactors DestroyWindow() to suit this need and uses the newly introduced DestroyWindowImpl() to destroy the HWND, and then recreate just that. The rest of the state is retained intact, and the Popup/ContextMenu continues to function well as before.

…8 in), a change was made to `Popup` that involved the destruction and recreation of the underlying `HWND`. This was done to ensure that the `HWND` was always created with the correct monitor (~=DPI) affinity.

In order to acheive this, `DestroyWindow()` and `BuildWindow()` - private methods in `Popup` - were used.

We are finding that `DestroyWindow()` has side-effects that can lead to incorrect behavior. The incorrect beahvior works as follows:

  - The call into `Popup.CreateWindow` is usually a consequence `Popup.IsOpenChanged` (`false -> true`)
  - Within `Popup.CreateWindow`, `DestroyWindow()` is called (when high-dpi mode is detected).
  - `DestroyWindow()` destroys the underlying `HWND`, releases the capture, raises the *OnClosed* event and clears the placement-target.
    - At the end of `DestroyWindow()`, we get `IsOpen == false`.
  - After `DestroyWindow()`, we call into `BuildWindow()` and `CreateNewPopupRoot()` etc., which go on to build the `Popup` again (and also instnatiate a new `HWND`).
    - Unfortunately, there is no mechanism here for resetting `IsOpen` back to `true` (without also leading to an undesirable infinite-recursion that calls back into `CreateWindow`).

If these calls to show the context-menu arise from `ContextMenu.OnIsOpenChanged`, and flow through `ContextMenu.HookupParentPopup -> Popup.CreateRootPopup`, then `IsOpen` gets reset (there is a direct call into `SetBinding(IsOpenProperty)`, in `Popup.CreateRootPopupInternal`) and the popup is shown correctly. Until then, the context-menu is "stuck" not being able to be shown.

The solution is to stop using `DestroyWindow()` as-is, which does more than what we need for it to accomplish. Our original intent in calling `DestroyWindow()` was simply destroy and recreate the `HWND`. This fix refactors `DestroyWindow()` to suit this need and uses the newly introduced `DestroyWindowImpl()` to destroy the `HWND`, and then recreate just that. The rest of the state is retained intact, and the `Popup/ContextMenu` continues to function well as before.
@ghost ghost requested a review from rladuca October 23, 2019 22:16
@ghost ghost added the PR metadata: Label to tag PRs, to facilitate with triage label Oct 23, 2019
@ghost ghost requested a review from SamBent October 23, 2019 22:16
@vatsan-madhavan vatsan-madhavan self-assigned this Oct 23, 2019
@vatsan-madhavan vatsan-madhavan added this to the 3.1 milestone Oct 23, 2019
@vatsan-madhavan vatsan-madhavan added * NO MERGE * metadata: The PR is not ready for merge yet (see discussion for detailed reasons) issue-type-netfx-port Ports from .NET Framework regression status: This issue is a regression from a previous build or release labels Oct 23, 2019
@arpitmathur arpitmathur self-requested a review October 24, 2019 19:58
@vatsan-madhavan vatsan-madhavan added auto_merge bot-command and removed * NO MERGE * metadata: The PR is not ready for merge yet (see discussion for detailed reasons) labels Oct 29, 2019
@ghost
Copy link

ghost commented Oct 29, 2019

Hello @vatsan-madhavan!

Because this pull request has the auto_merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost ghost merged commit 066f116 into dotnet:release/3.1 Oct 29, 2019
@vatsan-madhavan vatsan-madhavan deleted the contextmenu_fixup-rel31 branch October 29, 2019 19:02
@fabriceleal
Copy link

Is there any way to mitigate this until the next version of .NET Framework?

@dotnet dotnet locked as resolved and limited conversation to collaborators Apr 14, 2022
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
auto_merge bot-command issue-type-netfx-port Ports from .NET Framework PR metadata: Label to tag PRs, to facilitate with triage regression status: This issue is a regression from a previous build or release Servicing-approved
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants