BlazorWebView: Improve `WebView2WebViewManager` disposal handling
- Dominant language
- C#
- Stars
- 23.3k
- Forks
- 2k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 290
Description
### Description
There appears to be a disposal timing issue in `WebView2WebViewManager` when using `BlazorWebView`. If background work is still running and the host WebView is closed, an exception can be thrown due to attempts to interact with a disposed WebView.
---
### Steps to Reproduce
The issue is easy to reproduce with a simple background task:
```razor
@code {
private bool _loading;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_ = BackgroundWork();
}
private async Task BackgroundWork()
{
_loading = true;
await InvokeAsync(StateHasChanged);
try
{
await Task.Delay(5000);
/*
* Exception behavior depends on:
* 1. Whether an exception is thrown here
* 2. Whether DisposeAsync() is called for BlazorWebView.DisposeAsync
*
* Scenarios:
*
* A) Exception thrown + DispatchExceptionAsync used:
* - DisposeAsync() NOT called:
* -> "WebView2 control is Disposed" exception
* - DisposeAsync() called:
* -> "Renderer does not have a component with ID XX"
*
* B) NO exception thrown:
* - DisposeAsync() NOT called:
* -> "WebView2 control is Disposed" exception
* - DisposeAsync() called:
* -> No exception (BUT rare race condition observed occasionally, hard to repro reliably)
*/
throw new Exception("Exception"); // comment me depending on scenario
}
catch (Exception exception)
{
// MainLayout needs to be wrapped in ErrorBoundary
await DispatchExceptionAsync(exception);
}
finally
{
_loading = false;
await InvokeAsync(StateHasChanged);
}
}
}
```
Steps:
1. Run the app (get from reproduction repository) below.
2. Close the host window while the background task is still running.
3. Wait for exception when Blazor background task is finished
4. Keep in mind you should not exit the whole application when window is closed (for example, in wpf that can be achieved with `ShutdownMode="OnExplicitShutdown"`)
---
### Actual Behavior
An exception is thrown when the background task attempts to call `InvokeAsync` after the WebView has been disposed:
```
at Microsoft.Web.WebView2.Core.CoreWebView2.PostWebMessageAsString(String webMessageAsString)
at Microsoft.AspNetCore.Components.WebView.WebView2.WebView2WebViewManager.SendMessage(String message)
at Microsoft.AspNetCore.Components.WebView.IpcSender.<>c__DisplayClass13_0.b__0()
at Microsoft.AspNetCore.Components.WebView.Wpf.WpfDispatcher.d__4.MoveNext()
at Microsoft.AspNetCore.Components.WebView.IpcSender.<>c__DisplayClass14_0.<g__AwaitAndNotify|0>d.MoveNext()
at Microsoft.AspNetCore.Components.WebView.IpcSender.<>c__DisplayClass12_0.b__1()
at Microsoft.AspNetCore.Components.WebView.Wpf.WpfDispatcher.d__4.MoveNext()
at Microsoft.AspNetCore.Components.WebView.Wpf.WpfDispatcher.<>c.<.cctor>b__8_0(Exception exception)
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
```
Additional stack traces indicate the issue originates from `WebView2WebViewManager.SendMessage` attempting to post messages to a disposed `CoreWebView2`.
---
### Expected Behavior
Closing the host should not result in exceptions, even if there are in-flight IPC messages or background operations still running.
---
### Analysis
* This is fundamentally a race condition between WebView disposal and pending UI updates / IPC messages (`InvokeAsync`, `StateHasChanged`)
* Even with careful handling (cancellation tokens, guards, try/catch), it is not always possible to fully prevent this:
* Not all work is cancellable
* Third-party libraries (e.g., MudBlazor, Radzen) may trigger UI updates internally
* Rapid open/close cycles can still trigger the issue even without explicit background work
Some very rare exception I got during open/close even without background logic:
```
---> System.ObjectDisposedException: Cannot access a disposed object.
at Microsoft.Web.WebView2.Wpf.WebView2Base.VerifyNotDisposed()
at Microsoft.Web.WebView2.Wpf.WebView2Base.get_CoreWebView2()
at Microsoft.Web.WebView2.Wpf.WebView2CompositionControl.get_CoreWebView2()
at Microsoft.AspNetCore.Components.WebView.WebView2.WebView2WebViewManager.SendMessage(String message)
at Microsoft.AspNetCore.Components.WebView.IpcSender.<>c__DisplayClass13_0.b__0()
at System.Windows.Threading.DispatcherOperation.InvokeDelegateCore()
at System.Windows.Threading.DispatcherOperation.InvokeImpl()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Components.WebView.Wpf.WpfDispatcher.InvokeAsync(Action workItem)
at Microsoft.AspNetCore.Components.WebView.IpcSender.<>c__DisplayClass14_0.<g__AwaitAndNotify|0>d.MoveNext()
--- End of inner exception stack trace ---
at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at System.Delegate.DynamicInvokeImpl(Object[] args)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
```
---
### Did you find any workaround? (not sufficient)
* Using shared state / cancellation tokens
* Guarding `InvokeAsync` / `DispatchExceptionAsync` calls
* Delaying window closing (`OnClosing`)
* Adding extensive defensive checks
These approaches introduce significant boilerplate and still do not guarantee safety in all edge cases.
---
### Suggested Fix
Consider adding a safe no-op behavior inside `WebView2WebViewManager.SendMessage`:
https://github.com/dotnet/maui/blob/fdd42f79cc3a524b192f8b27dfa794242b883437/src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs#L198
to prevent sending messages when the WebView is already disposed:
P.S. Also consider fixing this threading issue as well: https://github.com/dotnet/maui/issues/26746
### Additional Info
I am a core maintainer of MudBlazor. In our codebase, we use batching, timers, throttling, and debouncing mechanisms that introduce delayed state changes in Blazor.
In WinForms or WPF applications with multiple windows, this issue can occur quite easily. Specifically, if a WebView host window is closed without shutting down the entire application, it may trigger the exception mentioned above.
### Link to public reproduction project repository
Repo: https://github.com/ScarletKuro/WpfIssueThreading
### Version with bug
Microsoft.AspNetCore.Components.WebView.Wpf 10.0.51
### Is this a regression from previous behavior?
No
### Last version that worked well
None
### Affected platforms
Windows
But could be all platforms, since none of them noops `SendMessage` on dispose.
Contributor guide
Research direction
Start with src/BlazorWebView/src/SharedSource/WebView2WebViewManager.cs at SendMessage, then run the WpfIssueThreading reproduction to observe disposal while background work is still running. Trace how pending IPC messages reach the disposed CoreWebView2 and verify that closing the host produces no exception, including with in-flight updates.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100