From 0346de81a4867f3fd3f0fbb156ee9d8d3bce703d Mon Sep 17 00:00:00 2001 From: David Britch Date: Thu, 14 Nov 2024 15:58:37 +0000 Subject: [PATCH] Invoke C# from JavaScript (#2632) * Invoke C# from JavaScript. * Edits. * Edit. * Edit. --- docs/deployment/includes/feature-switches.md | 16 +- .../includes/trimming-incompatibilities.md | 3 +- docs/user-interface/controls/hybridwebview.md | 221 ++++++++++++++++-- 3 files changed, 219 insertions(+), 21 deletions(-) diff --git a/docs/deployment/includes/feature-switches.md b/docs/deployment/includes/feature-switches.md index 707dc398d..ea0195354 100644 --- a/docs/deployment/includes/feature-switches.md +++ b/docs/deployment/includes/feature-switches.md @@ -1,18 +1,19 @@ --- ms.topic: include -ms.date: 10/28/2024 +ms.date: 11/14/2024 --- -.NET MAUI has trimmer directives, known as feature switches, that make it possible to preserve the code for features that aren't trim safe. These trimmer directives can be used when the `$(TrimMode)` build property is set to `full`, as well as for NativeAOT: +.NET MAUI has trimmer directives, known as feature switches, that make it possible to preserve the code for features that aren't trim safe. These trimmer directives can be used when the `$(TrimMode)` build property is set to `full`, as well as for Native AOT: | MSBuild property | Description | | ---------------- | ----------- | -| `MauiEnableVisualAssemblyScanning` | When set to `true`, .NET MAUI will scan assemblies for types implementing `IVisual` and for `[assembly:Visual(...)]` attributes, and will register these types. By default, this build property is set to `false`. | -| `MauiShellSearchResultsRendererDisplayMemberNameSupported` | When set to `false`, the value of `SearchHandler.DisplayMemberName` will be ignored. Instead, you should provide an to define the appearance of results. By default, this build property is set to `true`.| -| `MauiQueryPropertyAttributeSupport` | When set to `false`, `[QueryProperty(...)]` attributes won't be used to set property values when navigating. Instead, you should implement the interface to accept query parameters. By default, this build property is set to `true`. | -| `MauiImplicitCastOperatorsUsageViaReflectionSupport` | When set to `false`, .NET MAUI won't look for implicit conversion operators when converting values from one type to another. This can affect bindings between properties with different types, and setting a property value of a bindable object with a value of a different type. Instead, you should define a for your type and attach it to the type using the attribute. By default, this build property is set to `true`.| +| `MauiEnableVisualAssemblyScanning` | When set to `true`, .NET MAUI will scan assemblies for types implementing `IVisual` and for `[assembly:Visual(...)]` attributes, and will register these types. By default, this build property is set to `false` when full trimming is enabled. | +| `MauiShellSearchResultsRendererDisplayMemberNameSupported` | When set to `false`, the value of `SearchHandler.DisplayMemberName` will be ignored. Instead, you should provide an to define the appearance of results. By default, this build property is set to `false` when full trimming or Native AOT is enabled.| +| `MauiQueryPropertyAttributeSupport` | When set to `false`, `[QueryProperty(...)]` attributes won't be used to set property values when navigating. Instead, you should implement the interface to accept query parameters. By default, this build property is set to `false` when full trimming or Native AOT is enabled. | +| `MauiImplicitCastOperatorsUsageViaReflectionSupport` | When set to `false`, .NET MAUI won't look for implicit conversion operators when converting values from one type to another. This can affect bindings between properties with different types, and setting a property value of a bindable object with a value of a different type. Instead, you should define a for your type and attach it to the type using the attribute. By default, this build property is set to `false` when full trimming or Native AOT is enabled. | | `_MauiBindingInterceptorsSupport` | When set to `false`, .NET MAUI won't intercept any calls to the `SetBinding` methods and won't try to compile them. By default, this build property is set to `true`. | -| `MauiEnableXamlCBindingWithSourceCompilation` | When set to `true`, .NET MAUI will compile all bindings, including those where the `Source` property is used. If you enable this feature ensure that all bindings have the correct `x:DataType` so that they compile, or clear the data type with `x:Data={x:Null}}` if the binding shouldn't be compiled. By default, this build property is only set to `true` when full trimming or Native AOT deployment is enabled. | +| `MauiEnableXamlCBindingWithSourceCompilation` | When set to `true`, .NET MAUI will compile all bindings, including those where the `Source` property is used. If you enable this feature ensure that all bindings have the correct `x:DataType` so that they compile, or clear the data type with `x:Data={x:Null}}` if the binding shouldn't be compiled. By default, this build property is set to `true` when full trimming or Native AOT is enabled. | +| `MauiHybridWebViewSupported` | When set to `false`, the control won't be available. By default, this build property is set to `false` when full trimming or Native AOT is enabled. | These MSBuild properties also have equivalent switches: @@ -22,5 +23,6 @@ These MSBuild properties also have equivalent switches: - The `MauiImplicitCastOperatorsUsageViaReflectionSupport` MSBuild property has an equivalent switch named `Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported`. - The `_MauiBindingInterceptorsSupport` MSBuild property has an equivalent switch named `Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported`. - The `MauiEnableXamlCBindingWithSourceCompilation` MSBuild property has an equivalent switch named `Microsoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabled`. +- The `MauiHybridWebViewSupported` MSBuild property has an equivalent switch named `Microsoft.Maui.RuntimeFeature.IsHybridWebViewSupported`. The easiest way to consume a feature switch is by putting the corresponding MSBuild property into your app's project file (*.csproj), which causes the related code to be trimmed from the .NET MAUI assemblies. diff --git a/docs/deployment/includes/trimming-incompatibilities.md b/docs/deployment/includes/trimming-incompatibilities.md index 16e4dcf94..0054005a4 100644 --- a/docs/deployment/includes/trimming-incompatibilities.md +++ b/docs/deployment/includes/trimming-incompatibilities.md @@ -1,6 +1,6 @@ --- ms.topic: include -ms.date: 10/23/2024 +ms.date: 11/14/2024 monikerRange: ">=net-maui-9.0" --- @@ -11,3 +11,4 @@ The following .NET MAUI features are incompatible with full trimming and will be - Loading XAML at runtime with the extension method. This XAML can be made trim safe by annotating all types that could be loaded at runtime with the [`DynamicallyAccessedMembers`](xref:System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute) attribute or the [`DynamicDependency`](xref:System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute) attribute. However, this is very error prone and isn't recommended. - Receiving navigation data using the . Instead, you should implement the interface on types that need to accept query parameters. For more information, see [Process navigation data using a single method](~/fundamentals/shell/navigation.md#process-navigation-data-using-a-single-method). - The `SearchHandler.DisplayMemberName` property. Instead, you should provide an to define the appearance of results. For more information, see [Define search results item appearance](~/fundamentals/shell/search.md#define-search-results-item-appearance). +- The control, due to its use of dynamic `System.Text.Json` serialization features. diff --git a/docs/user-interface/controls/hybridwebview.md b/docs/user-interface/controls/hybridwebview.md index a1ff389f0..1b2379e8a 100644 --- a/docs/user-interface/controls/hybridwebview.md +++ b/docs/user-interface/controls/hybridwebview.md @@ -21,7 +21,7 @@ The .NET Multi-platform App UI (.NET MAUI) defines a event that's raised when a raw message is received. The object that accompanies the event defines a property that contains the message. -Your app's C# code can invoke synchronous and asynchronous JavaScript methods within the with the and methods. For more information, see [Invoke JavaScript from C#](#invoke-javascript-from-c). +Your app's C# code can invoke synchronous and asynchronous JavaScript methods within the with the and methods. Your app's JavaScript code can also synchronously invoke C# methods. For more information, see [Invoke JavaScript from C#](#invoke-javascript-from-c) and [Invoke C# from JavaScript](#invoke-c-from-javascript). To create a .NET MAUI app with you need: @@ -31,6 +31,9 @@ To create a .NET MAUI app with you The entire app, including the web content, is packaged and runs locally on a device, and can be published to applicable app stores. The web content is hosted within a native web view control and runs within the context of the app. Any part of the app can access external web services, but isn't required to. +> [!IMPORTANT] +> By default, the control won't be available when full trimming or Native AOT is enabled. To change this behavior, see [Trimming feature switches](~/deployment/trimming.md#trimming-feature-switches). + ## Create a .NET MAUI HybridWebView app To create a .NET MAUI app with a : @@ -52,23 +55,89 @@ To create a .NET MAUI app with a : + -

HybridWebView app!

- + Hybrid sample! +
+
+
- Messages from C#: + + + + +
+
+ Log: +
+
+ Consider checking out this PDF: sample.pdf
@@ -78,7 +147,7 @@ To create a .NET MAUI app with a : ```js window.HybridWebView = { - "Init": function () { + "Init": function Init() { function DispatchHybridWebViewMessage(message) { const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); window.dispatchEvent(event); @@ -106,11 +175,53 @@ To create a .NET MAUI app with a : } }, - "SendRawMessage": function (message) { - window.HybridWebView.__SendMessageInternal('RawMessage', message); + "SendRawMessage": function SendRawMessage(message) { + window.HybridWebView.__SendMessageInternal('__RawMessage', message); + }, + + "InvokeDotNet": async function InvokeDotNetAsync(methodName, paramValues) { + const body = { + MethodName: methodName + }; + + if (typeof paramValues !== 'undefined') { + if (!Array.isArray(paramValues)) { + paramValues = [paramValues]; + } + + for (var i = 0; i < paramValues.length; i++) { + paramValues[i] = JSON.stringify(paramValues[i]); + } + + if (paramValues.length > 0) { + body.ParamValues = paramValues; + } + } + + const message = JSON.stringify(body); + + var requestUrl = `${window.location.origin}/__hwvInvokeDotNet?data=${encodeURIComponent(message)}`; + + const rawResponse = await fetch(requestUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + const response = await rawResponse.json(); + + if (response) { + if (response.IsJson) { + return JSON.parse(response.Result); + } + + return response.Result; + } + + return null; }, - "__SendMessageInternal": function (type, message) { + "__SendMessageInternal": function __SendMessageInternal(type, message) { const messageToSend = type + '|' + message; @@ -128,7 +239,7 @@ To create a .NET MAUI app with a : } }, - "InvokeMethod": function (taskId, methodName, args) { + "__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); @@ -144,13 +255,13 @@ To create a .NET MAUI app with a : } }, - "__TriggerAsyncCallback": function (taskId, result) { + "__TriggerAsyncCallback": function __TriggerAsyncCallback(taskId, result) { // Make sure the result is a string if (result && typeof (result) !== 'string') { result = JSON.stringify(result); } - window.HybridWebView.__SendMessageInternal('InvokeMethodCompleted', taskId + '|' + result); + window.HybridWebView.__SendMessageInternal('__InvokeJavaScriptCompleted', taskId + '|' + result); } } @@ -178,6 +289,8 @@ To create a .NET MAUI app with a : 1. Modify the `CreateMauiApp` method of your `MauiProgram` class to enable developer tools on the underlying WebView controls when your app is running in debug configuration. To do this, call the method on the object: ```csharp + using Microsoft.Extensions.Logging; + public static class MauiProgram { public static MauiApp CreateMauiApp() @@ -193,6 +306,7 @@ To create a .NET MAUI app with a : #if DEBUG builder.Services.AddHybridWebViewDeveloperTools(); + builder.Logging.AddDebug(); #endif // Register any app services on the IServiceCollection object @@ -306,3 +420,84 @@ 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`. + +## Invoke C\# from JavaScript + +Your app's JavaScript code within the can synchronously invoke C# methods, with optional parameters and an optional return value. This can be achieved by: + +- Defining public C# methods that will be invoked from JavaScript. +- Calling the method to set the object that will be the target of JavaScript calls from the . +- Calling the C# methods from JavaScript. + +> [!IMPORTANT] +> Asynchronously invoking C# methods from JavaScript isn't currently supported. + +The following example defines four public methods for invoking from JavaScript: + +```csharp +public partial class MainPage : ContentPage +{ + ... + + public void DoSyncWork() + { + Debug.WriteLine("DoSyncWork"); + } + + public void DoSyncWorkParams(int i, string s) + { + Debug.WriteLine($"DoSyncWorkParams: {i}, {s}"); + } + + public string DoSyncWorkReturn() + { + Debug.WriteLine("DoSyncWorkReturn"); + return "Hello from C#!"; + } + + public SyncReturn DoSyncWorkParamsReturn(int i, string s) + { + Debug.WriteLine($"DoSyncWorkParamReturn: {i}, {s}"); + return new SyncReturn + { + Message = "Hello from C#!" + s, + Value = i + }; + } + + public class SyncReturn + { + public string? Message { get; set; } + public int Value { get; set; } + } +} +``` + +You must then call the method to set the object that will be the target of JavaScript calls from the : + +```csharp +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + hybridWebView.SetInvokeJavaScriptTarget(this); + } + + ... +} +``` + +The public methods on the object set via the method can then be invoked from JavaScript with the `window.HybridWebView.InvokeDotNet` function: + +```js +await window.HybridWebView.InvokeDotNet('DoSyncWork'); +await window.HybridWebView.InvokeDotNet('DoSyncWorkParams', [123, 'hello']); +const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkReturn'); +const retValue = await window.HybridWebView.InvokeDotNet('DoSyncWorkParamsReturn', [123, 'hello']); +``` + +The `window.HybridWebView.InvokeDotNet` JavaScript function invokes a specified C# method, with optional parameters and an optional return value. + +> [!NOTE] +> Invoking the `window.HybridWebView.InvokeDotNet` JavaScript function requires your app to include the *HybridWebView.js* JavaScript library listed earlier in this article.