Skip to content

Commit

Permalink
HybridWebView: Invoke JS methods from .NET (#23769)
Browse files Browse the repository at this point in the history
Fixes #22303
  • Loading branch information
Eilon authored Aug 15, 2024
1 parent b9d711a commit 026e046
Show file tree
Hide file tree
Showing 35 changed files with 968 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,39 @@

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

<Label
<Editor
Grid.Row="0"
Grid.Column="0"
Text="HybridWebView here"
x:Name="statusLabel" />
IsReadOnly="True"
MinimumHeightRequest="200"
x:Name="statusText" />

<Button
<VerticalStackLayout
Grid.Row="0"
Grid.Column="1"
Grid.Column="1">

<Button
Margin="10"
Text="Send message to JS"
Clicked="SendMessageButton_Clicked" />

<HybridWebView
<Button
Margin="10"
Text="Invoke JS"
Clicked="InvokeJSMethodButton_Clicked" />

<Button
Margin="10"
Text="Invoke Async JS"
Clicked="InvokeAsyncJSMethodButton_Clicked" />

</VerticalStackLayout>

<HybridWebView
x:Name="hwv"
Grid.Row="1"
Grid.ColumnSpan="2"
Grid.ColumnSpan="3"
HybridRoot="HybridSamplePage"
RawMessageReceived="hwv_RawMessageReceived"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Pages
Expand All @@ -10,14 +13,76 @@ public HybridWebViewPage()
InitializeComponent();
}

int count;
private void SendMessageButton_Clicked(object sender, EventArgs e)
{
hwv.SendRawMessage("Hello from C#!");
hwv.SendRawMessage($"Hello from C#! #{count++}");
}

private async void InvokeJSMethodButton_Clicked(object sender, EventArgs e)
{
var statusResult = "";

var x = 123d;
var y = 321d;
var result = await hwv.InvokeJavaScriptAsync<ComputationResult>(
"AddNumbers",
SampleInvokeJsContext.Default.ComputationResult,
new object?[] { x, null, y, null },
new[] { SampleInvokeJsContext.Default.Double, null, SampleInvokeJsContext.Default.Double, null });

if (result is null)
{
statusResult += Environment.NewLine + $"Got no result for operation with {x} and {y} 😮";
}
else
{
statusResult += Environment.NewLine + $"Used operation {result.operationName} with numbers {x} and {y} to get {result.result}";
}

Dispatcher.Dispatch(() => statusText.Text += statusResult);
}

private async void InvokeAsyncJSMethodButton_Clicked(object sender, EventArgs e)
{
var statusResult = "";

var asyncFuncResult = await hwv.InvokeJavaScriptAsync<Dictionary<string,string>>(
"EvaluateMeWithParamsAndAsyncReturn",
SampleInvokeJsContext.Default.DictionaryStringString,
new object?[] { "new_key", "new_value" },
new[] { SampleInvokeJsContext.Default.String, SampleInvokeJsContext.Default.String });

if (asyncFuncResult == null)
{
statusResult += Environment.NewLine + $"Got no result from EvaluateMeWithParamsAndAsyncReturn 😮";
}
else
{
statusResult += Environment.NewLine + $"Got result from EvaluateMeWithParamsAndAsyncReturn: {string.Join(",", asyncFuncResult)}";
}

Dispatcher.Dispatch(() => statusText.Text += statusResult);
}

private void hwv_RawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e)
{
Dispatcher.Dispatch(() => statusLabel.Text += e.Message);
Dispatcher.Dispatch(() => statusText.Text += Environment.NewLine + e.Message);
}

public class ComputationResult
{
public double result { get; set; }
public string? operationName { get; set; }
}

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(ComputationResult))]
[JsonSerializable(typeof(double))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(Dictionary<string, string>))]
internal partial class SampleInvokeJsContext : JsonSerializerContext
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"key1": "value1",
"key2": "value2"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="styles/app.css">
<script src="scripts/HybridWebView.js"></script>
<script>
Expand All @@ -13,14 +14,39 @@
var messageFromCSharp = document.getElementById("messageFromCSharp");
messageFromCSharp.value += '\r\n' + e.detail.message;
});

function AddNumbers(a, x, b, y) {
var result = {
"result": a + b,
"operationName": "Addition"
};
return result;
}

var count = 0;

async function EvaluateMeWithParamsAndAsyncReturn(s1, s2) {
const response = await fetch("/asyncdata.txt");
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
var jsonData = await response.json();

jsonData[s1] = s2;

const msg = 'JSON data is available: ' + JSON.stringify(jsonData);
window.HybridWebView.SendRawMessage(msg)

return jsonData;
}
</script>
</head>
<body>
<div>
Hybrid sample!
</div>
<div>
<button onclick="window.HybridWebView.SendRawMessage('Message from JS!')">Send message to C#</button>
<button onclick="window.HybridWebView.SendRawMessage('Message from JS! ' + (count++))">Send message to C#</button>
</div>
<div>
Message from C#: <textarea readonly id="messageFromCSharp" style="width: 80%; height: 10em;"></textarea>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,78 @@
function HybridWebViewInit() {

function DispatchHybridWebViewMessage(message) {
const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } });
window.dispatchEvent(event);
}
window.HybridWebView = {
"Init": function () {
function DispatchHybridWebViewMessage(message) {
const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } });
window.dispatchEvent(event);
}

if (window.chrome && window.chrome.webview) {
// Windows WebView2
window.chrome.webview.addEventListener('message', arg => {
DispatchHybridWebViewMessage(arg.data);
});
}
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
// iOS and MacCatalyst WKWebView
window.external = {
"receiveMessage": message => {
DispatchHybridWebViewMessage(message);
}
};
}
else {
// Android WebView
window.addEventListener('message', arg => {
DispatchHybridWebViewMessage(arg.data);
});
}
}
if (window.chrome && window.chrome.webview) {
// Windows WebView2
window.chrome.webview.addEventListener('message', arg => {
DispatchHybridWebViewMessage(arg.data);
});
}
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
// iOS and MacCatalyst WKWebView
window.external = {
"receiveMessage": message => {
DispatchHybridWebViewMessage(message);
}
};
}
else {
// Android WebView
window.addEventListener('message', arg => {
DispatchHybridWebViewMessage(arg.data);
});
}
},

window.HybridWebView = {
"SendRawMessage": function (message) {
window.HybridWebView.__SendMessageInternal('RawMessage', message);
},

"__SendMessageInternal": function (type, message) {

const messageToSend = type + '|' + message;

if (window.chrome && window.chrome.webview) {
// Windows WebView2
window.chrome.webview.postMessage(message);
window.chrome.webview.postMessage(messageToSend);
}
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
// iOS and MacCatalyst WKWebView
window.webkit.messageHandlers.webwindowinterop.postMessage(message);
window.webkit.messageHandlers.webwindowinterop.postMessage(messageToSend);
}
else {
// Android WebView
hybridWebViewHost.sendRawMessage(message);
hybridWebViewHost.sendMessage(messageToSend);
}
},

"InvokeMethod": function (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);
}
},

"__TriggerAsyncCallback": function (taskId, result) {
// Make sure the result is a string
if (result && typeof (result) !== 'string') {
result = JSON.stringify(result);
}

window.HybridWebView.__SendMessageInternal('InvokeMethodCompleted', taskId + '|' + result);
}
}

HybridWebViewInit();
window.HybridWebView.Init();
Loading

0 comments on commit 026e046

Please sign in to comment.