Last active
January 13, 2021 05:44
-
-
Save mqudsi/ceb4ecee76eb4c32238a438664783480 to your computer and use it in GitHub Desktop.
WebView2 Extension Methods
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright (c) Mahmoud Al-Qudsi, NeoSmart Technoogies. All rights reserved. | |
// Licensed under the MIT License. | |
namespace NeoSmart.WebExtensions | |
{ | |
static class WebView2Extensions | |
{ | |
private static readonly ILogger Logger = Serilog.Log.Logger; | |
public static void Navigate(this WebView2 webview, Uri url) | |
{ | |
webview.Source = url; | |
} | |
private enum PropertyAction | |
{ | |
Read = 0, | |
Write = 1, | |
} | |
private struct WebMessage | |
{ | |
public Guid Guid { get; set; } | |
} | |
private struct MethodWebMessage | |
{ | |
public string Id { get; set; } | |
public string Method { get; set; } | |
public string Args { get; set; } | |
} | |
private struct PropertyWebMessage | |
{ | |
public string Id { get; set; } | |
public string Property { get; set; } | |
public PropertyAction Action { get; set; } | |
public string Value { get; set; } | |
} | |
public static List<TypedEventHandler<WebView2, WebView2WebMessageReceivedEventArgs>> _handlers = new List<TypedEventHandler<WebView2, WebView2WebMessageReceivedEventArgs>>(); | |
public static async Task AddWebAllowedObject<T>(this WebView2 webview, string name, T @object) | |
{ | |
var sb = new StringBuilder(); | |
sb.AppendLine($"window.{name} = {{ "); | |
// Test webview for our sanity | |
await webview.ExecuteScriptAsync($@"console.log(""Sanity check from iMessage"");"); | |
var methodsGuid = Guid.NewGuid(); | |
var methodInfo = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance); | |
var methods = new Dictionary<string, MethodInfo>(methodInfo.Length); | |
foreach (var method in methodInfo) | |
{ | |
var functionName = $"{char.ToLower(method.Name[0])}{method.Name.Substring(1)}"; | |
sb.AppendLine($@"{functionName}: function() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{methodsGuid}"", id: this._callbackIndex++, method: ""{functionName}"", args: JSON.stringify([...arguments]) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); | |
methods.Add(functionName, method); | |
} | |
var propertiesGuid = Guid.NewGuid(); | |
var propertyInfo = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); | |
var properties = new Dictionary<string, PropertyInfo>(propertyInfo.Length); | |
//foreach (var property in propertyInfo) | |
//{ | |
// var propertyName = $"{char.ToLower(property.Name[0])}{property.Name.Substring(1)}"; | |
// if (property.CanRead) | |
// { | |
// sb.AppendLine($@"get {propertyName}() {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int) PropertyAction.Read}"" }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); | |
// } | |
// if (property.CanWrite) | |
// { | |
// sb.AppendLine($@"set {propertyName}(value) {{ window.chrome.webview.postMessage(JSON.stringify({{ guid: ""{propertiesGuid}"", id: this._callbackIndex++, property: ""{propertyName}"", action: ""{(int)PropertyAction.Write}"", value: JSON.stringify(value) }})); const promise = new Promise((accept, reject) => this._callbacks.set(this._callbackIndex, {{ accept: accept, reject: reject }})); return promise; }},"); | |
// } | |
// properties[propertyName] = property; | |
//} | |
// Add a map<int, (promiseAccept, promiseReject)> to the object used to resolve results | |
sb.AppendLine($@"_callbacks: new Map(),"); | |
// And a shared counter to index into that map | |
sb.Append($@"_callbackIndex: 0,"); | |
sb.AppendLine("}"); | |
Logger.Debug("Creating web object {WebObjectName}: {WebObjectScript}", name, sb.ToString()); | |
Logger.Debug("Methods GUID: {MethodsGuid}", methodsGuid); | |
Logger.Debug("Properties GUID: {PropertiesGuid}", propertiesGuid); | |
try | |
{ | |
//await webview.ExecuteScriptAsync($"try {{ {sb} }} catch (ex) {{ console.error(ex); }}").AsTask(); | |
await webview.ExecuteScriptAsync($"{sb}").AsTask(); | |
} | |
catch (Exception ex) | |
{ | |
// So we can see it in the JS debugger | |
Logger.Error(ex, "Error installing web allowed object"); | |
} | |
var handler = (TypedEventHandler<WebView2, WebView2WebMessageReceivedEventArgs>)(async (_, e) => | |
{ | |
Logger.Information("Received web message {WebMessage}", e.WebMessageAsString); | |
var message = JsonConvert.DeserializeObject<WebMessage>(e.WebMessageAsString); | |
if (message.Guid == methodsGuid) | |
{ | |
var methodMessage = JsonConvert.DeserializeObject<MethodWebMessage>(e.WebMessageAsString); | |
var method = methods[methodMessage.Method]; | |
try | |
{ | |
var result = method.Invoke(@object, JsonConvert.DeserializeObject<object[]>(methodMessage.Args)); | |
if (result is object) | |
{ | |
var resultType = result.GetType(); | |
dynamic task = null; | |
if (resultType.Name.StartsWith("TaskToAsyncOperationAdapter") | |
|| resultType.IsInstanceOfType(typeof(IAsyncInfo))) | |
{ | |
// IAsyncOperation that needs to be converted to a task first | |
if (resultType.GenericTypeArguments.Length > 0) | |
{ | |
var asTask = typeof(WindowsRuntimeSystemExtensions) | |
.GetMethods(BindingFlags.Public | BindingFlags.Static) | |
.Where(method => method.GetParameters().Length == 1 | |
&& method.Name == "AsTask" | |
&& method.ToString().Contains("Windows.Foundation.IAsyncOperation`1[TResult]")) | |
.FirstOrDefault(); | |
//var asTask = typeof(WindowsRuntimeSystemExtensions) | |
// .GetMethod(nameof(WindowsRuntimeSystemExtensions.AsTask), | |
// new[] { typeof(IAsyncOperation<>).MakeGenericType(resultType.GenericTypeArguments[0]) } | |
// ); | |
asTask = asTask.MakeGenericMethod(resultType.GenericTypeArguments[0]); | |
task = (Task)asTask.Invoke(null, new[] { result }); | |
} | |
else | |
{ | |
task = WindowsRuntimeSystemExtensions.AsTask((dynamic)result); | |
} | |
} | |
else | |
{ | |
var awaiter = resultType.GetMethod(nameof(Task.GetAwaiter)); | |
if (awaiter is object) | |
{ | |
task = (dynamic)result; | |
} | |
} | |
if (task is object) | |
{ | |
result = await task; | |
} | |
} | |
var json = JsonConvert.SerializeObject(result); | |
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});"); | |
} | |
catch (Exception ex) | |
{ | |
Logger.Error(ex, "Exception in {WebObjectName}.{WebObjectFunction}", name, method.Name); | |
var json = JsonConvert.SerializeObject(ex, new JsonSerializerSettings() { Error = (_, e) => e.ErrorContext.Handled = true }); | |
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({methodMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({methodMessage.Id});"); | |
//throw; | |
} | |
} | |
else if (message.Guid == propertiesGuid) | |
{ | |
var propertyMessage = JsonConvert.DeserializeObject<PropertyWebMessage>(e.WebMessageAsString); | |
var property = properties[propertyMessage.Property]; | |
try | |
{ | |
object result; | |
if (propertyMessage.Action == PropertyAction.Read) | |
{ | |
result = property.GetValue(@object); | |
} | |
else | |
{ | |
var value = JsonConvert.DeserializeObject(propertyMessage.Value, property.PropertyType); | |
property.SetValue(@object, value); | |
result = new object(); | |
} | |
var json = JsonConvert.SerializeObject(result); | |
await webview.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).accept(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});"); | |
} | |
catch (Exception ex) | |
{ | |
Logger.Error(ex, "Exception in {WebObjectName}.{WebObjectProperty}", name, property.Name); | |
var json = JsonConvert.SerializeObject(ex, new JsonSerializerSettings() { Error = (_, e) => e.ErrorContext.Handled = true }); | |
//await webview.ExecuteScriptAsync($@"{name}._callbacks.get({propertyMessage.Id}).reject(JSON.parse({json})); {name}._callbacks.delete({propertyMessage.Id});"); | |
//throw; | |
} | |
} | |
}); | |
_handlers.Add(handler); | |
webview.WebMessageReceived += handler; | |
} | |
public static async Task<string> InvokeScriptAsync(this WebView2 webview, string function, params object[] args) | |
{ | |
var array = JsonConvert.SerializeObject(args); | |
string result = null; | |
// Tested and checked: this dispatch is required, even though the web view is in a different process | |
await webview.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => | |
{ | |
var script = $"{function}(...{array});"; | |
try | |
{ | |
result = await webview.ExecuteScriptAsync(script).AsTask(); | |
} | |
catch (Exception ex) | |
{ | |
Logger.Error(ex, "Error executing JavaScript function {JSFunction}", function); | |
} | |
}); | |
return result; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment