Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HybridWebView: Invoke JS methods from .NET #23769

Merged
merged 1 commit into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading