dotnet / dotnet/maui

BlazorWebView: Improve `WebView2WebViewManager` disposal handling

Open
#34,855 11 comments 5 reactions 0 assignees View on GitHub
area-blazor platform/windows s/triaged s/verified t/bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.