Repository navigation
[WPF] Fix BlazorWebView disposal hanging when cleanup awaits JS interop - #39497
Merged
kubaflo merged 1 commit intoOct 20, 2026
Merged
Conversation
Keep JS messaging available until component and scoped service disposal completes, then suppress messages before disposing the native WebView. Preserve the WPF UI context for native cleanup. Fixes dotnet#39471 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:30 — with
GitHub Actions
Inactive
Contributor
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 39497Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 39497" |
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:30 — with
GitHub Actions
Inactive
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:31 — with
GitHub Actions
Inactive
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:33 — with
GitHub Actions
Inactive
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:34 — with
GitHub Actions
Inactive
sheiksyedm
temporarily deployed
to
copilot-pat-pool
October 20, 2026 11:34 — with
GitHub Actions
Inactive
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The native WPF/WebView2 lifecycle change needs completed CI and human confirmation of the reported runtime verification.
No open issues 🎉
What changed in this PR
Fixes the WPF BlazorWebView disposal hang described in #39471 by keeping JavaScript interop available during component and scoped-service cleanup.
Changes:
- Delays message shutdown until manager disposal completes.
- Preserves the WPF UI context for native WebView disposal.
- Updates shared documentation to describe the disposal order; MAUI handler behavior is unchanged.
| File | Description |
|---|---|
| src/BlazorWebView/src/Wpf/BlazorWebView.cs | Reorders disposal and preserves the UI context. |
| src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs | Updates disposal-order documentation only. |
Review effort: Balanced
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
kubaflo
approved these changes
Oct 20, 2026
kubaflo
pushed a commit
that referenced
this pull request
Oct 21, 2026
…op (#39497) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue details On WPF, awaiting `BlazorWebView.DisposeAsync()` can hang when a Razor component or scoped service awaits JavaScript interop during its asynchronous disposal. For example, a scoped service implementing `IAsyncDisposable` can await `IJSRuntime.InvokeVoidAsync` during cleanup. The same problem affects components that perform asynchronous JS cleanup, such as disposing an `IJSObjectReference` module. The disposal-time JS invocation is silently dropped rather than completed or faulted. Consequently, the component or service cannot finish disposal, and the owning `BlazorWebView.DisposeAsync()` remains incomplete. Catching `JSDisconnectedException` does not help because no such exception is raised by this path. The reported real-world scenario involves closing a WPF dialog containing a DevExpress Blazor component whose scoped service releases JS modules during disposal. ### Root cause WPF's `BlazorWebView.DisposeAsync()` calls `_webviewManager.MarkAsDisposing()` before disposing the page through the WebView manager. That flag causes `WebView2WebViewManager.SendMessage()` to return without sending messages. However, page disposal still needs to dispose the Razor renderer and scoped services, and their cleanup code can await JS interop. This ordering blocks the JS invocation that disposal itself is waiting to complete: 1. The control marks the manager as disposing. 2. Page disposal invokes component or scoped-service cleanup. 3. Cleanup awaits a JS interop call. 4. `SendMessage()` drops the outgoing invocation because the manager is already marked as disposing. 5. Cleanup cannot finish, so control disposal cannot finish either. Additionally, the existing `ConfigureAwait(false)` can move native WebView cleanup off the WPF UI thread when manager disposal completes asynchronously. ### Fix description - Keep JS messaging available while awaiting WebView manager disposal, allowing component and scoped-service JS cleanup to complete. - Call `MarkAsDisposing()` after manager/page disposal completes and before disposing the native WebView. - Use `ConfigureAwait(true)` to preserve the WPF UI context for native WebView disposal. - Update the shared manager's documentation to describe the required disposal order. The existing late-message guards in `SendMessage()` remain in place. The control's `_isDisposed` guard is also retained. The functional change is limited to the WPF BlazorWebView implementation. No MAUI handler behavior is changed. ### Why no automated regression test is included No existing automated tests targeting the WPF BlazorWebView implementation were found in this repository. The existing BlazorWebView unit and device test projects target the MAUI implementation; the WPF application is a sample rather than an automated test suite. A representative regression test would require a WPF host running on an STA/UI dispatcher with a real WebView2 instance and JS interop. This PR does not introduce a new WPF test project or test infrastructure. Adding a MAUI test would not cover the affected WPF disposal path. Instead, the fix was verified using a separate executable WPF reproduction harness. That harness and the MAUI Sandbox comparison changes are not included in this PR. ### Verification Verified locally on Windows 11 with a real WebView2 runtime, using both the reported package version (`10.0.110`) and source-built WPF libraries. The WPF reproduction initializes Blazor, instantiates the scoped service, and successfully completes a JS preflight call before disposing the control on the WPF UI thread. A bounded 10-second observation distinguishes completed disposal from the reported hang. | Scenario | Before fix | After fix | | --- | --- | --- | | Scoped service awaits JS during disposal | Service entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, service disposal, and control disposal completed | | Razor component awaits JS during disposal | Component entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, component disposal, and control disposal completed | | Scoped service disposes without calling JS | Disposal completed | Disposal completed | All three scenarios completed successfully in three repeated runs each against the patched source-built WPF library. Subsequent runs of the local verification script also passed. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #39471 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Oct 25, 2026
…op (#39497) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue details On WPF, awaiting `BlazorWebView.DisposeAsync()` can hang when a Razor component or scoped service awaits JavaScript interop during its asynchronous disposal. For example, a scoped service implementing `IAsyncDisposable` can await `IJSRuntime.InvokeVoidAsync` during cleanup. The same problem affects components that perform asynchronous JS cleanup, such as disposing an `IJSObjectReference` module. The disposal-time JS invocation is silently dropped rather than completed or faulted. Consequently, the component or service cannot finish disposal, and the owning `BlazorWebView.DisposeAsync()` remains incomplete. Catching `JSDisconnectedException` does not help because no such exception is raised by this path. The reported real-world scenario involves closing a WPF dialog containing a DevExpress Blazor component whose scoped service releases JS modules during disposal. ### Root cause WPF's `BlazorWebView.DisposeAsync()` calls `_webviewManager.MarkAsDisposing()` before disposing the page through the WebView manager. That flag causes `WebView2WebViewManager.SendMessage()` to return without sending messages. However, page disposal still needs to dispose the Razor renderer and scoped services, and their cleanup code can await JS interop. This ordering blocks the JS invocation that disposal itself is waiting to complete: 1. The control marks the manager as disposing. 2. Page disposal invokes component or scoped-service cleanup. 3. Cleanup awaits a JS interop call. 4. `SendMessage()` drops the outgoing invocation because the manager is already marked as disposing. 5. Cleanup cannot finish, so control disposal cannot finish either. Additionally, the existing `ConfigureAwait(false)` can move native WebView cleanup off the WPF UI thread when manager disposal completes asynchronously. ### Fix description - Keep JS messaging available while awaiting WebView manager disposal, allowing component and scoped-service JS cleanup to complete. - Call `MarkAsDisposing()` after manager/page disposal completes and before disposing the native WebView. - Use `ConfigureAwait(true)` to preserve the WPF UI context for native WebView disposal. - Update the shared manager's documentation to describe the required disposal order. The existing late-message guards in `SendMessage()` remain in place. The control's `_isDisposed` guard is also retained. The functional change is limited to the WPF BlazorWebView implementation. No MAUI handler behavior is changed. ### Why no automated regression test is included No existing automated tests targeting the WPF BlazorWebView implementation were found in this repository. The existing BlazorWebView unit and device test projects target the MAUI implementation; the WPF application is a sample rather than an automated test suite. A representative regression test would require a WPF host running on an STA/UI dispatcher with a real WebView2 instance and JS interop. This PR does not introduce a new WPF test project or test infrastructure. Adding a MAUI test would not cover the affected WPF disposal path. Instead, the fix was verified using a separate executable WPF reproduction harness. That harness and the MAUI Sandbox comparison changes are not included in this PR. ### Verification Verified locally on Windows 11 with a real WebView2 runtime, using both the reported package version (`10.0.110`) and source-built WPF libraries. The WPF reproduction initializes Blazor, instantiates the scoped service, and successfully completes a JS preflight call before disposing the control on the WPF UI thread. A bounded 10-second observation distinguishes completed disposal from the reported hang. | Scenario | Before fix | After fix | | --- | --- | --- | | Scoped service awaits JS during disposal | Service entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, service disposal, and control disposal completed | | Razor component awaits JS during disposal | Component entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, component disposal, and control disposal completed | | Scoped service disposes without calling JS | Disposal completed | Disposal completed | All three scenarios completed successfully in three repeated runs each against the patched source-built WPF library. Subsequent runs of the local verification script also passed. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #39471 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
kubaflo
pushed a commit
that referenced
this pull request
Oct 26, 2026
…op (#39497) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue details On WPF, awaiting `BlazorWebView.DisposeAsync()` can hang when a Razor component or scoped service awaits JavaScript interop during its asynchronous disposal. For example, a scoped service implementing `IAsyncDisposable` can await `IJSRuntime.InvokeVoidAsync` during cleanup. The same problem affects components that perform asynchronous JS cleanup, such as disposing an `IJSObjectReference` module. The disposal-time JS invocation is silently dropped rather than completed or faulted. Consequently, the component or service cannot finish disposal, and the owning `BlazorWebView.DisposeAsync()` remains incomplete. Catching `JSDisconnectedException` does not help because no such exception is raised by this path. The reported real-world scenario involves closing a WPF dialog containing a DevExpress Blazor component whose scoped service releases JS modules during disposal. ### Root cause WPF's `BlazorWebView.DisposeAsync()` calls `_webviewManager.MarkAsDisposing()` before disposing the page through the WebView manager. That flag causes `WebView2WebViewManager.SendMessage()` to return without sending messages. However, page disposal still needs to dispose the Razor renderer and scoped services, and their cleanup code can await JS interop. This ordering blocks the JS invocation that disposal itself is waiting to complete: 1. The control marks the manager as disposing. 2. Page disposal invokes component or scoped-service cleanup. 3. Cleanup awaits a JS interop call. 4. `SendMessage()` drops the outgoing invocation because the manager is already marked as disposing. 5. Cleanup cannot finish, so control disposal cannot finish either. Additionally, the existing `ConfigureAwait(false)` can move native WebView cleanup off the WPF UI thread when manager disposal completes asynchronously. ### Fix description - Keep JS messaging available while awaiting WebView manager disposal, allowing component and scoped-service JS cleanup to complete. - Call `MarkAsDisposing()` after manager/page disposal completes and before disposing the native WebView. - Use `ConfigureAwait(true)` to preserve the WPF UI context for native WebView disposal. - Update the shared manager's documentation to describe the required disposal order. The existing late-message guards in `SendMessage()` remain in place. The control's `_isDisposed` guard is also retained. The functional change is limited to the WPF BlazorWebView implementation. No MAUI handler behavior is changed. ### Why no automated regression test is included No existing automated tests targeting the WPF BlazorWebView implementation were found in this repository. The existing BlazorWebView unit and device test projects target the MAUI implementation; the WPF application is a sample rather than an automated test suite. A representative regression test would require a WPF host running on an STA/UI dispatcher with a real WebView2 instance and JS interop. This PR does not introduce a new WPF test project or test infrastructure. Adding a MAUI test would not cover the affected WPF disposal path. Instead, the fix was verified using a separate executable WPF reproduction harness. That harness and the MAUI Sandbox comparison changes are not included in this PR. ### Verification Verified locally on Windows 11 with a real WebView2 runtime, using both the reported package version (`10.0.110`) and source-built WPF libraries. The WPF reproduction initializes Blazor, instantiates the scoped service, and successfully completes a JS preflight call before disposing the control on the WPF UI thread. A bounded 10-second observation distinguishes completed disposal from the reported hang. | Scenario | Before fix | After fix | | --- | --- | --- | | Scoped service awaits JS during disposal | Service entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, service disposal, and control disposal completed | | Razor component awaits JS during disposal | Component entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, component disposal, and control disposal completed | | Scoped service disposes without calling JS | Disposal completed | Disposal completed | All three scenarios completed successfully in three repeated runs each against the patched source-built WPF library. Subsequent runs of the local verification script also passed. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #39471 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 28, 2026
…op (#39497) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue details On WPF, awaiting `BlazorWebView.DisposeAsync()` can hang when a Razor component or scoped service awaits JavaScript interop during its asynchronous disposal. For example, a scoped service implementing `IAsyncDisposable` can await `IJSRuntime.InvokeVoidAsync` during cleanup. The same problem affects components that perform asynchronous JS cleanup, such as disposing an `IJSObjectReference` module. The disposal-time JS invocation is silently dropped rather than completed or faulted. Consequently, the component or service cannot finish disposal, and the owning `BlazorWebView.DisposeAsync()` remains incomplete. Catching `JSDisconnectedException` does not help because no such exception is raised by this path. The reported real-world scenario involves closing a WPF dialog containing a DevExpress Blazor component whose scoped service releases JS modules during disposal. ### Root cause WPF's `BlazorWebView.DisposeAsync()` calls `_webviewManager.MarkAsDisposing()` before disposing the page through the WebView manager. That flag causes `WebView2WebViewManager.SendMessage()` to return without sending messages. However, page disposal still needs to dispose the Razor renderer and scoped services, and their cleanup code can await JS interop. This ordering blocks the JS invocation that disposal itself is waiting to complete: 1. The control marks the manager as disposing. 2. Page disposal invokes component or scoped-service cleanup. 3. Cleanup awaits a JS interop call. 4. `SendMessage()` drops the outgoing invocation because the manager is already marked as disposing. 5. Cleanup cannot finish, so control disposal cannot finish either. Additionally, the existing `ConfigureAwait(false)` can move native WebView cleanup off the WPF UI thread when manager disposal completes asynchronously. ### Fix description - Keep JS messaging available while awaiting WebView manager disposal, allowing component and scoped-service JS cleanup to complete. - Call `MarkAsDisposing()` after manager/page disposal completes and before disposing the native WebView. - Use `ConfigureAwait(true)` to preserve the WPF UI context for native WebView disposal. - Update the shared manager's documentation to describe the required disposal order. The existing late-message guards in `SendMessage()` remain in place. The control's `_isDisposed` guard is also retained. The functional change is limited to the WPF BlazorWebView implementation. No MAUI handler behavior is changed. ### Why no automated regression test is included No existing automated tests targeting the WPF BlazorWebView implementation were found in this repository. The existing BlazorWebView unit and device test projects target the MAUI implementation; the WPF application is a sample rather than an automated test suite. A representative regression test would require a WPF host running on an STA/UI dispatcher with a real WebView2 instance and JS interop. This PR does not introduce a new WPF test project or test infrastructure. Adding a MAUI test would not cover the affected WPF disposal path. Instead, the fix was verified using a separate executable WPF reproduction harness. That harness and the MAUI Sandbox comparison changes are not included in this PR. ### Verification Verified locally on Windows 11 with a real WebView2 runtime, using both the reported package version (`10.0.110`) and source-built WPF libraries. The WPF reproduction initializes Blazor, instantiates the scoped service, and successfully completes a JS preflight call before disposing the control on the WPF UI thread. A bounded 10-second observation distinguishes completed disposal from the reported hang. | Scenario | Before fix | After fix | | --- | --- | --- | | Scoped service awaits JS during disposal | Service entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, service disposal, and control disposal completed | | Razor component awaits JS during disposal | Component entered disposal, but its JS call and control disposal remained incomplete after 10 seconds | JS call, component disposal, and control disposal completed | | Scoped service disposes without calling JS | Disposal completed | Disposal completed | All three scenarios completed successfully in three repeated runs each against the patched source-built WPF library. Subsequent runs of the local verification script also passed. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #39471 <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. --> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue details
On WPF, awaiting
BlazorWebView.DisposeAsync()can hang when a Razor component or scoped service awaits JavaScript interop during its asynchronous disposal.For example, a scoped service implementing
IAsyncDisposablecan awaitIJSRuntime.InvokeVoidAsyncduring cleanup. The same problem affects components that perform asynchronous JS cleanup, such as disposing anIJSObjectReferencemodule.The disposal-time JS invocation is silently dropped rather than completed or faulted. Consequently, the component or service cannot finish disposal, and the owning
BlazorWebView.DisposeAsync()remains incomplete. CatchingJSDisconnectedExceptiondoes not help because no such exception is raised by this path.The reported real-world scenario involves closing a WPF dialog containing a DevExpress Blazor component whose scoped service releases JS modules during disposal.
Root cause
WPF's
BlazorWebView.DisposeAsync()calls_webviewManager.MarkAsDisposing()before disposing the page through the WebView manager.That flag causes
WebView2WebViewManager.SendMessage()to return without sending messages. However, page disposal still needs to dispose the Razor renderer and scoped services, and their cleanup code can await JS interop.This ordering blocks the JS invocation that disposal itself is waiting to complete:
SendMessage()drops the outgoing invocation because the manager is already marked as disposing.Additionally, the existing
ConfigureAwait(false)can move native WebView cleanup off the WPF UI thread when manager disposal completes asynchronously.Fix description
MarkAsDisposing()after manager/page disposal completes and before disposing the native WebView.ConfigureAwait(true)to preserve the WPF UI context for native WebView disposal.The existing late-message guards in
SendMessage()remain in place. The control's_isDisposedguard is also retained.The functional change is limited to the WPF BlazorWebView implementation. No MAUI handler behavior is changed.
Why no automated regression test is included
No existing automated tests targeting the WPF BlazorWebView implementation were found in this repository. The existing BlazorWebView unit and device test projects target the MAUI implementation; the WPF application is a sample rather than an automated test suite.
A representative regression test would require a WPF host running on an STA/UI dispatcher with a real WebView2 instance and JS interop. This PR does not introduce a new WPF test project or test infrastructure. Adding a MAUI test would not cover the affected WPF disposal path.
Instead, the fix was verified using a separate executable WPF reproduction harness. That harness and the MAUI Sandbox comparison changes are not included in this PR.
Verification
Verified locally on Windows 11 with a real WebView2 runtime, using both the reported package version (
10.0.110) and source-built WPF libraries.The WPF reproduction initializes Blazor, instantiates the scoped service, and successfully completes a JS preflight call before disposing the control on the WPF UI thread. A bounded 10-second observation distinguishes completed disposal from the reported hang.
All three scenarios completed successfully in three repeated runs each against the patched source-built WPF library. Subsequent runs of the local verification script also passed.
Issues Fixed
Fixes #39471