Skip to content

Commit 026e046

Browse files
authored
HybridWebView: Invoke JS methods from .NET (#23769)
Fixes #22303
1 parent b9d711a commit 026e046

File tree

35 files changed

+968
-135
lines changed

35 files changed

+968
-135
lines changed

src/Controls/samples/Controls.Sample/Pages/Controls/HybridWebViewPage.xaml

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,39 @@
88

99
<Grid ColumnDefinitions="2*,1*" RowDefinitions="Auto,1*">
1010

11-
<Label
11+
<Editor
1212
Grid.Row="0"
1313
Grid.Column="0"
1414
Text="HybridWebView here"
15-
x:Name="statusLabel" />
15+
IsReadOnly="True"
16+
MinimumHeightRequest="200"
17+
x:Name="statusText" />
1618

17-
<Button
19+
<VerticalStackLayout
1820
Grid.Row="0"
19-
Grid.Column="1"
21+
Grid.Column="1">
22+
23+
<Button
24+
Margin="10"
2025
Text="Send message to JS"
2126
Clicked="SendMessageButton_Clicked" />
2227

23-
<HybridWebView
28+
<Button
29+
Margin="10"
30+
Text="Invoke JS"
31+
Clicked="InvokeJSMethodButton_Clicked" />
32+
33+
<Button
34+
Margin="10"
35+
Text="Invoke Async JS"
36+
Clicked="InvokeAsyncJSMethodButton_Clicked" />
37+
38+
</VerticalStackLayout>
39+
40+
<HybridWebView
2441
x:Name="hwv"
2542
Grid.Row="1"
26-
Grid.ColumnSpan="2"
43+
Grid.ColumnSpan="3"
2744
HybridRoot="HybridSamplePage"
2845
RawMessageReceived="hwv_RawMessageReceived"/>
2946

src/Controls/samples/Controls.Sample/Pages/Controls/HybridWebViewPage.xaml.cs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.Json.Serialization;
25
using Microsoft.Maui.Controls;
36

47
namespace Maui.Controls.Sample.Pages
@@ -10,14 +13,76 @@ public HybridWebViewPage()
1013
InitializeComponent();
1114
}
1215

16+
int count;
1317
private void SendMessageButton_Clicked(object sender, EventArgs e)
1418
{
15-
hwv.SendRawMessage("Hello from C#!");
19+
hwv.SendRawMessage($"Hello from C#! #{count++}");
20+
}
21+
22+
private async void InvokeJSMethodButton_Clicked(object sender, EventArgs e)
23+
{
24+
var statusResult = "";
25+
26+
var x = 123d;
27+
var y = 321d;
28+
var result = await hwv.InvokeJavaScriptAsync<ComputationResult>(
29+
"AddNumbers",
30+
SampleInvokeJsContext.Default.ComputationResult,
31+
new object?[] { x, null, y, null },
32+
new[] { SampleInvokeJsContext.Default.Double, null, SampleInvokeJsContext.Default.Double, null });
33+
34+
if (result is null)
35+
{
36+
statusResult += Environment.NewLine + $"Got no result for operation with {x} and {y} 😮";
37+
}
38+
else
39+
{
40+
statusResult += Environment.NewLine + $"Used operation {result.operationName} with numbers {x} and {y} to get {result.result}";
41+
}
42+
43+
Dispatcher.Dispatch(() => statusText.Text += statusResult);
44+
}
45+
46+
private async void InvokeAsyncJSMethodButton_Clicked(object sender, EventArgs e)
47+
{
48+
var statusResult = "";
49+
50+
var asyncFuncResult = await hwv.InvokeJavaScriptAsync<Dictionary<string,string>>(
51+
"EvaluateMeWithParamsAndAsyncReturn",
52+
SampleInvokeJsContext.Default.DictionaryStringString,
53+
new object?[] { "new_key", "new_value" },
54+
new[] { SampleInvokeJsContext.Default.String, SampleInvokeJsContext.Default.String });
55+
56+
if (asyncFuncResult == null)
57+
{
58+
statusResult += Environment.NewLine + $"Got no result from EvaluateMeWithParamsAndAsyncReturn 😮";
59+
}
60+
else
61+
{
62+
statusResult += Environment.NewLine + $"Got result from EvaluateMeWithParamsAndAsyncReturn: {string.Join(",", asyncFuncResult)}";
63+
}
64+
65+
Dispatcher.Dispatch(() => statusText.Text += statusResult);
1666
}
1767

1868
private void hwv_RawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
1969
{
20-
Dispatcher.Dispatch(() => statusLabel.Text += e.Message);
70+
Dispatcher.Dispatch(() => statusText.Text += Environment.NewLine + e.Message);
71+
}
72+
73+
public class ComputationResult
74+
{
75+
public double result { get; set; }
76+
public string? operationName { get; set; }
77+
}
78+
79+
[JsonSourceGenerationOptions(WriteIndented = true)]
80+
[JsonSerializable(typeof(ComputationResult))]
81+
[JsonSerializable(typeof(double))]
82+
[JsonSerializable(typeof(string))]
83+
[JsonSerializable(typeof(Dictionary<string, string>))]
84+
internal partial class SampleInvokeJsContext : JsonSerializerContext
85+
{
2186
}
2287
}
2388
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"key1": "value1",
3+
"key2": "value2"
4+
}

src/Controls/samples/Controls.Sample/Resources/Raw/HybridSamplePage/index.html

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<head>
55
<meta charset="utf-8" />
66
<title></title>
7+
<link rel="icon" href="data:,">
78
<link rel="stylesheet" href="styles/app.css">
89
<script src="scripts/HybridWebView.js"></script>
910
<script>
@@ -13,14 +14,39 @@
1314
var messageFromCSharp = document.getElementById("messageFromCSharp");
1415
messageFromCSharp.value += '\r\n' + e.detail.message;
1516
});
17+
18+
function AddNumbers(a, x, b, y) {
19+
var result = {
20+
"result": a + b,
21+
"operationName": "Addition"
22+
};
23+
return result;
24+
}
25+
26+
var count = 0;
27+
28+
async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
29+
const response = await fetch("/asyncdata.txt");
30+
if (!response.ok) {
31+
throw new Error(`HTTP error: ${response.status}`);
32+
}
33+
var jsonData = await response.json();
34+
35+
jsonData[s1] = s2;
36+
37+
const msg = 'JSON data is available: ' + JSON.stringify(jsonData);
38+
window.HybridWebView.SendRawMessage(msg)
39+
40+
return jsonData;
41+
}
1642
</script>
1743
</head>
1844
<body>
1945
<div>
2046
Hybrid sample!
2147
</div>
2248
<div>
23-
<button onclick="window.HybridWebView.SendRawMessage('Message from JS!')">Send message to C#</button>
49+
<button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button>
2450
</div>
2551
<div>
2652
Message from C#: <textarea readonly id="messageFromCSharp" style="width: 80%; height: 10em;"></textarea>
Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,78 @@
1-
function HybridWebViewInit() {
2-
3-
function DispatchHybridWebViewMessage(message) {
4-
const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } });
5-
window.dispatchEvent(event);
6-
}
1+
window.HybridWebView = {
2+
"Init": function () {
3+
function DispatchHybridWebViewMessage(message) {
4+
const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } });
5+
window.dispatchEvent(event);
6+
}
77

8-
if (window.chrome && window.chrome.webview) {
9-
// Windows WebView2
10-
window.chrome.webview.addEventListener('message', arg => {
11-
DispatchHybridWebViewMessage(arg.data);
12-
});
13-
}
14-
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
15-
// iOS and MacCatalyst WKWebView
16-
window.external = {
17-
"receiveMessage": message => {
18-
DispatchHybridWebViewMessage(message);
19-
}
20-
};
21-
}
22-
else {
23-
// Android WebView
24-
window.addEventListener('message', arg => {
25-
DispatchHybridWebViewMessage(arg.data);
26-
});
27-
}
28-
}
8+
if (window.chrome && window.chrome.webview) {
9+
// Windows WebView2
10+
window.chrome.webview.addEventListener('message', arg => {
11+
DispatchHybridWebViewMessage(arg.data);
12+
});
13+
}
14+
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
15+
// iOS and MacCatalyst WKWebView
16+
window.external = {
17+
"receiveMessage": message => {
18+
DispatchHybridWebViewMessage(message);
19+
}
20+
};
21+
}
22+
else {
23+
// Android WebView
24+
window.addEventListener('message', arg => {
25+
DispatchHybridWebViewMessage(arg.data);
26+
});
27+
}
28+
},
2929

30-
window.HybridWebView = {
3130
"SendRawMessage": function (message) {
31+
window.HybridWebView.__SendMessageInternal('RawMessage', message);
32+
},
33+
34+
"__SendMessageInternal": function (type, message) {
35+
36+
const messageToSend = type + '|' + message;
3237

3338
if (window.chrome && window.chrome.webview) {
3439
// Windows WebView2
35-
window.chrome.webview.postMessage(message);
40+
window.chrome.webview.postMessage(messageToSend);
3641
}
3742
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
3843
// iOS and MacCatalyst WKWebView
39-
window.webkit.messageHandlers.webwindowinterop.postMessage(message);
44+
window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend);
4045
}
4146
else {
4247
// Android WebView
43-
hybridWebViewHost.sendRawMessage(message);
48+
hybridWebViewHost.sendMessage(messageToSend);
4449
}
50+
},
51+
52+
"InvokeMethod": function (taskId, methodName, args) {
53+
if (methodName[Symbol.toStringTag] === 'AsyncFunction') {
54+
// For async methods, we need to call the method and then trigger the callback when it's done
55+
const asyncPromise = methodName(...args);
56+
asyncPromise
57+
.then(asyncResult => {
58+
window.HybridWebView.__TriggerAsyncCallback(taskId, asyncResult);
59+
})
60+
.catch(error => console.error(error));
61+
} else {
62+
// For sync methods, we can call the method and trigger the callback immediately
63+
const syncResult = methodName(...args);
64+
window.HybridWebView.__TriggerAsyncCallback(taskId, syncResult);
65+
}
66+
},
67+
68+
"__TriggerAsyncCallback": function (taskId, result) {
69+
// Make sure the result is a string
70+
if (result && typeof (result) !== 'string') {
71+
result = JSON.stringify(result);
72+
}
73+
74+
window.HybridWebView.__SendMessageInternal('InvokeMethodCompleted', taskId + '|' + result);
4575
}
4676
}
4777

48-
HybridWebViewInit();
78+
window.HybridWebView.Init();

0 commit comments

Comments
 (0)