Skip to content

Commit

Permalink
HybridWebView: Invoke JS methods from .NET
Browse files Browse the repository at this point in the history
Fixes #22303
  • Loading branch information
Eilon committed Aug 6, 2024
1 parent 50e9fe0 commit 9b1087f
Show file tree
Hide file tree
Showing 34 changed files with 774 additions and 103 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,79 @@ 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 evalResult = await hwv.EvaluateJavaScriptAsync("EvaluateMeWithParamsAndAsyncReturn('abc', 'def')");
//statusResult += Environment.NewLine + $"Got result from EvaluateMeWithParamsAndAsyncReturn: {evalResult}";

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 9b1087f

Please sign in to comment.