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 Jul 26, 2024
1 parent b521bfc commit 218ce12
Show file tree
Hide file tree
Showing 31 changed files with 444 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@

<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 Method"
Clicked="InvokeJSMethodButton_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
Expand Up @@ -10,14 +10,37 @@ 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 x = 123;
var y = 321;
var result = await hwv.InvokeJavaScriptAsync<ComputationResult>("AddNumbers", x, y);

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

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; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,28 @@
<script>
window.addEventListener(
"HybridWebViewMessageReceived",
function (e) {
function(e) {
var messageFromCSharp = document.getElementById("messageFromCSharp");
messageFromCSharp.value += '\r\n' + e.detail.message;
});

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

var count = 0;
</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
126 changes: 115 additions & 11 deletions src/Controls/src/Core/HybridWebView/HybridWebView.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Maui.Devices;

namespace Microsoft.Maui.Controls
{
/// <summary>
/// A <see cref="View"/> that presents local HTML content in a web view and allows JavaScript and C# code to interop using messages.
/// A <see cref="View"/> that presents local HTML content in a web view and allows JavaScript and C# code to
/// communicate by using messages and by invoking methods.
/// </summary>
public class HybridWebView : View, IHybridWebView
{
Expand All @@ -15,21 +19,14 @@ public class HybridWebView : View, IHybridWebView
BindableProperty.Create(nameof(HybridRoot), typeof(string), typeof(HybridWebView), defaultValue: "wwwroot");


/// <summary>
/// Specifies the file within the <see cref="HybridRoot"/> that should be served as the default file. The
/// default value is <c>index.html</c>.
/// </summary>
/// <inheritdoc/>
public string? DefaultFile
{
get { return (string)GetValue(DefaultFileProperty); }
set { SetValue(DefaultFileProperty, value); }
}

/// <summary>
/// The path within the app's "Raw" asset resources that contain the web app's contents. For example, if the
/// files are located in <c>[ProjectFolder]/Resources/Raw/hybrid_root</c>, then set this property to "hybrid_root".
/// The default value is <c>wwwroot</c>, which maps to <c>[ProjectFolder]/Resources/Raw/wwwroot</c>.
/// </summary>
/// <inheritdoc/>
public string? HybridRoot
{
get { return (string)GetValue(HybridRootProperty); }
Expand All @@ -46,9 +43,116 @@ void IHybridWebView.RawMessageReceived(string rawMessage)
/// </summary>
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;

/// <summary>
/// Sends a raw message to the code running in the web view. Raw messages have no additional processing.
/// </summary>
/// <param name="rawMessage"></param>
public void SendRawMessage(string rawMessage)
{
Handler?.Invoke(nameof(IHybridWebView.SendRawMessage), rawMessage);
Handler?.Invoke(
nameof(IHybridWebView.SendRawMessage),
new HybridWebViewRawMessage
{
Message = rawMessage,
});
}

private const string TrimmerJustification = "The caller of this method must ensure necessary types and members are preserved.";

/// <summary>
/// Invokes a JavaScript method named <paramref name="methodName"/> and optionally passes in the parameter values
/// specified by <paramref name="paramValues"/>.
/// </summary>
/// <param name="methodName">The name of the JavaScript method to invoke.</param>
/// <param name="paramValues">Optional array of objects to be passed to the JavaScript method.</param>
/// <returns>A string containing the return value of the called method.</returns>
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = TrimmerJustification)]
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = TrimmerJustification)]
public async Task<string?> InvokeJavaScriptAsync(string methodName, params object[] paramValues)
{
#if !WINDOWS && !ANDROID && !IOS && !MACCATALYST
await Task.Delay(0);
throw new NotImplementedException();
#else
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentException($"The method name cannot be null or empty.", nameof(methodName));
}

var js = $"{methodName}({(paramValues == null ? string.Empty : string.Join(", ", paramValues.Select(v => System.Text.Json.JsonSerializer.Serialize(v))))})";
return await EvaluateJavaScriptAsync(js);
#endif
}

/// <summary>
/// Invokes a JavaScript method named <paramref name="methodName"/> and optionally passes in the parameter values specified
/// by <paramref name="paramValues"/> by JSON-encoding each one.
/// </summary>
/// <typeparam name="TReturnType">The type of the return value to deserialize from JSON.</typeparam>
/// <param name="methodName">The name of the JavaScript method to invoke.</param>
/// <param name="paramValues">Optional array of objects to be passed to the JavaScript method by JSON-encoding each one.</param>
/// <returns>An object of type <typeparamref name="TReturnType"/> containing the return value of the called method.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = TrimmerJustification)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = TrimmerJustification)]
public async Task<TReturnType?> InvokeJavaScriptAsync<TReturnType>(string methodName, params object[] paramValues)
{
#if !WINDOWS && !ANDROID && !IOS && !MACCATALYST
await Task.Delay(0);
throw new NotImplementedException();
#else
var stringResult = await InvokeJavaScriptAsync(methodName, paramValues);

if (stringResult is null)
{
return default;
}
return System.Text.Json.JsonSerializer.Deserialize<TReturnType>(stringResult);
#endif
}

/// <inheritdoc/>
public async Task<string?> EvaluateJavaScriptAsync(string script)
{
if (script == null)
{
return null;
}

// Make all the platforms mimic Android's implementation, which is by far the most complete.
if (DeviceInfo.Platform != DevicePlatform.Android)
{
script = WebView.EscapeJsString(script);

if (DeviceInfo.Platform != DevicePlatform.WinUI)
{
// Use JSON.stringify() method to converts a JavaScript value to a JSON string
script = "try{JSON.stringify(eval('" + script + "'))}catch(e){'null'};";
}
else
{
script = "try{eval('" + script + "')}catch(e){'null'};";
}
}

string? result;

// Use the handler command to evaluate the JS
result = await Handler!.InvokeAsync(nameof(IHybridWebView.EvaluateJavaScriptAsync),
new EvaluateJavaScriptAsyncRequest(script));

//if the js function errored or returned null/undefined treat it as null
if (result == "null")
{
result = null;
}
//JSON.stringify wraps the result in literal quotes, we just want the actual returned result
//note that if the js function returns the string "null" we will get here and not above
else if (result != null)
{
result = result.Trim('"');
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ Microsoft.Maui.Controls.Handlers.Items.CarouselViewHandler.~CarouselViewHandler(
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ Microsoft.Maui.Controls.HandlerProperties
Microsoft.Maui.Controls.HybridWebView
Microsoft.Maui.Controls.HybridWebView.DefaultFile.get -> string?
Microsoft.Maui.Controls.HybridWebView.DefaultFile.set -> void
Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, params object![]! paramValues) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/WebView/WebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public IPlatformElementConfiguration<T, WebView> On<T>() where T : IConfigPlatfo
return _platformConfigurationRegistry.Value.On<T>();
}

static string EscapeJsString(string js)
internal static string EscapeJsString(string js)
{
if (js == null)
return null;
Expand Down
Loading

0 comments on commit 218ce12

Please sign in to comment.