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.0] Context Menus are sometimes not shown in High-DPI applications #2099

Closed

Conversation

vatsan-madhavan
Copy link
Member

Addresses #2088
.NET 5 PR: #2097
.NET Core 3.1 PR: #2098

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:18
@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:18
@vatsan-madhavan vatsan-madhavan self-assigned this Oct 23, 2019
@vatsan-madhavan vatsan-madhavan added this to the 3.0 Servicing 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) Bug Product bug (most likely) 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:57
@vatsan-madhavan
Copy link
Member Author

We are not forward porting .NET Framework fixes to release/3.0 - this will go into release/3.1 only.

@vatsan-madhavan vatsan-madhavan deleted the contextmenu_fixup-rel30 branch October 29, 2019 18:44
@ghost ghost locked as resolved and limited conversation to collaborators Apr 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug Product bug (most likely) issue-type-netfx-port Ports from .NET Framework * NO MERGE * metadata: The PR is not ready for merge yet (see discussion for detailed reasons) PR metadata: Label to tag PRs, to facilitate with triage regression status: This issue is a regression from a previous build or release Servicing-rejected
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants