Skip to content

Commit

Permalink
Send JavaScript exceptions to .NET
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbritch committed Feb 18, 2025
1 parent 0018246 commit 0c4742f
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 28 deletions.
16 changes: 11 additions & 5 deletions docs/user-interface/controls/blazorwebview.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,13 @@ By default, <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> fi
> [!WARNING]
> This fire-and-forget default behavior means that disposal can return before all objects are disposed, which can cause behavioral changes in your app. The items that are disposed are partially Blazor's own internal types, but also app-defined types such as scoped services used within the <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> portion of your app.

To opt out of this behavior, you should configure your app to block on dispose via an <xref:System.AppContext> switch in the `CreateMauiApp` method in your `MauiProgram` class:
To opt out of this behavior, you should configure your app to block on dispose via an <xref:System.AppContext> switch in your `MauiProgram` class:

```csharp
AppContext.SetSwitch("BlazorWebView.AndroidFireAndForgetAsync", false);
static MauiProgram()
{
AppContext.SetSwitch("BlazorWebView.AndroidFireAndForgetAsync", false);
}
```

If your app is configured to block on dispose via this switch, <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> performs async-over-sync disposal, which means that it blocks the thread until the async disposal is complete. However, this can cause deadlocks if the disposal needs to run code on the same thread (because the thread is blocked while waiting).
Expand All @@ -239,11 +242,14 @@ If your app is configured to block on dispose via this switch, <xref:Microsoft.A

The default behavior for hosting content in a <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> has changed to `0.0.0.1`. The internal `0.0.0.0` address used to host content no longer works and results in the <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> not loading any content and rendering as an empty rectangle.

To opt into using the `0.0.0.0` address, add the following code to the `CreateMauiApp` method in *MauiProgram.cs*:
To opt into using the `0.0.0.0` address, add the following code to your `MauiProgram` class:

```csharp
// Set this switch to use the LEGACY behavior of always using 0.0.0.0 to host BlazorWebView
AppContext.SetSwitch("BlazorWebView.AppHostAddressAlways0000", true);
static MauiProgram()
{
// Set this switch to use the LEGACY behavior of always using 0.0.0.0 to host BlazorWebView
AppContext.SetSwitch("BlazorWebView.AppHostAddressAlways0000", true);
}
```

::: moniker-end
73 changes: 55 additions & 18 deletions docs/user-interface/controls/hybridwebview.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: HybridWebView
description: Learn how to use a HybridWebView to host HTML/JS/CSS content in a WebView, and communicate between that content and .NET.
ms.topic: concept-article
ms.date: 11/14/2024
ms.date: 02/18/2024
monikerRange: ">=net-maui-9.0"

#customer intent: As a developer, I want to host HTML/JS/CSS content in a web view so that I can publish the web app as a mobile app.
Expand Down Expand Up @@ -239,29 +239,53 @@ To create a .NET MAUI app with a <xref:Microsoft.Maui.Controls.HybridWebView>:
}
},

"__InvokeJavaScript": function __InvokeJavaScript(taskId, methodName, args) {
if (methodName[Symbol.toStringTag] === 'AsyncFunction') {
// For async methods, we need to call the method and then trigger the callback when it's done
const asyncPromise = methodName(...args);
asyncPromise
.then(asyncResult => {
window.HybridWebView.__TriggerAsyncCallback(taskId, asyncResult);
})
.catch(error => console.error(error));
} else {
// For sync methods, we can call the method and trigger the callback immediately
const syncResult = methodName(...args);
window.HybridWebView.__TriggerAsyncCallback(taskId, syncResult);
"__InvokeJavaScript": async function __InvokeJavaScript(taskId, methodName, args) {
try {
var result = null;
if (methodName[Symbol.toStringTag] === 'AsyncFunction') {
result = await methodName(...args);
} else {
result = methodName(...args);
}
window.HybridWebView.__TriggerAsyncCallback(taskId, result);
} catch (ex) {
console.error(ex);
window.HybridWebView.__TriggerAsyncFailedCallback(taskId, ex);
}
},

"__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) {
// Make sure the result is a string
if (result && typeof (result) !== 'string') {
result = JSON.stringify(result);
const json = JSON.stringify(result);
window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + json);
},

"__TriggerAsyncFailedCallback": function __TriggerAsyncCallback(taskId, error) {

if (!error) {
json = {
Message: "Unknown error",
StackTrace: Error().stack
};
} else if (error instanceof Error) {
json = {
Name: error.name,
Message: error.message,
StackTrace: error.stack
};
} else if (typeof (error) === 'string') {
json = {
Message: error,
StackTrace: Error().stack
};
} else {
json = {
Message: JSON.stringify(error),
StackTrace: Error().stack
};
}

window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + result);
json = JSON.stringify(json);
window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptFailed', taskId + '|' + json);
}
}

Expand Down Expand Up @@ -421,6 +445,19 @@ internal partial class HybridSampleJSContext : JsonSerializerContext
> [!IMPORTANT]
> The `HybridSampleJsContext` class must be `partial` so that code generation can provide the implementation when the project is compiled. If the type is nested into another type, then that type must also be `partial`.
### Send JavaScript exceptions to .NET

By default, invocation of JavaScript methods in a <xref:Microsoft.Maui.Controls.HybridWebView> can hide exceptions thrown by your JavaScript code. To opt into JavaScript exceptions being sent to .NET, where they're re-thrown as .NET exceptions, add the following code to your `MauiProgram` class:

```csharp
static MauiProgram()
{
AppContext.SetSwitch("HybridWebView.InvokeJavaScriptThrowsExceptions", true);
}
```

This enables scenarios where if your C# code calls JavaScript code, and the JavaScript code fails, the JavaScript failure can be sent to .NET where it's re-thrown as a .NET exception that can be caught and handled.

## Invoke C\# from JavaScript

Your app's JavaScript code within the <xref:Microsoft.Maui.Controls.HybridWebView> can synchronously invoke C# methods, with optional parameters and an optional return value. This can be achieved by:
Expand Down
16 changes: 11 additions & 5 deletions docs/whats-new/dotnet-9.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,22 +147,28 @@ The binding mode for `IsVisible` and `IsEnabled` on a <xref:Microsoft.Maui.Contr

The default behavior for hosting content in a <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> has changed to `0.0.0.1`. The internal `0.0.0.0` address used to host content no longer works and results in the <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> not loading any content and rendering as an empty rectangle.

To opt into using the `0.0.0.0` address, add the following code to the `CreateMauiApp` method in *MauiProgram.cs*:
To opt into using the `0.0.0.0` address, add the following code to your `MauiProgram.cs` class:

```csharp
// Set this switch to use the LEGACY behavior of always using 0.0.0.0 to host BlazorWebView
AppContext.SetSwitch("BlazorWebView.AppHostAddressAlways0000", true);
static MauiProgram()
{
// Set this switch to use the LEGACY behavior of always using 0.0.0.0 to host BlazorWebView
AppContext.SetSwitch("BlazorWebView.AppHostAddressAlways0000", true);
}
```

By default, <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> now fires and forgets the async disposal of the underlying `WebViewManager`. This reduces the potential for disposal deadlocks to occur on Android.

> [!WARNING]
> This fire-and-forget default behavior means that disposal can return before all objects are disposed, which can cause behavioral changes in your app. The items that are disposed are partially Blazor's own internal types, but also app-defined types such as scoped services used within the <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> portion of your app.
To opt out of this behavior, you should configure your app to block on dispose via an <xref:System.AppContext> switch in the `CreateMauiApp` method in your `MauiProgram` class:
To opt out of this behavior, you should configure your app to block on dispose via an <xref:System.AppContext> switch in your `MauiProgram` class:

```csharp
AppContext.SetSwitch("BlazorWebView.AndroidFireAndForgetAsync", false);
static MauiProgram()
{
AppContext.SetSwitch("BlazorWebView.AndroidFireAndForgetAsync", false);
}
```

If your app is configured to block on dispose via this switch, <xref:Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebView> performs async-over-sync disposal, which means that it blocks the thread until the async disposal is complete. However, this can cause deadlocks if the disposal needs to run code on the same thread (because the thread is blocked while waiting).
Expand Down

0 comments on commit 0c4742f

Please sign in to comment.