-
Notifications
You must be signed in to change notification settings - Fork 50
/
Copy pathHybridWebView.cs
279 lines (235 loc) · 12 KB
/
HybridWebView.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
using System.Diagnostics;
using System.Text.Json;
namespace HybridWebView
{
public partial class HybridWebView : WebView
{
internal const string ProxyRequestPath = "proxy";
/// <summary>
/// Specifies the file within the <see cref="HybridAssetRoot"/> that should be served as the main file. The
/// default value is <c>index.html</c>.
/// </summary>
public string? MainFile { get; set; } = "index.html";
/// <summary>
/// Gets or sets the path for initial navigation after the content is finished loading. The default value is <c>/</c>.
/// </summary>
public string StartPath { get; set; } = "/";
/// <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 "ProjectFolder/Resources/Raw/hybrid_root", then set this property to "hybrid_root".
/// </summary>
public string? HybridAssetRoot { get; set; }
/// <summary>
/// The target object for JavaScript method invocations. When an "invoke" message is sent from JavaScript,
/// the invoked method will be located on this object, and any specified parameters will be passed in.
/// </summary>
public object? JSInvokeTarget { get; set; }
/// <summary>
/// Enables web developers tools (such as "F12 web dev tools inspectors")
/// </summary>
public bool EnableWebDevTools { get; set; }
/// <summary>
/// Raised when a raw message is received from the web view. Raw messages are strings that have no additional processing.
/// </summary>
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;
/// <summary>
/// Async event handler that is called when a proxy request is received from the web view.
/// </summary>
public event Func<HybridWebViewProxyEventArgs, Task>? ProxyRequestReceived;
/// <summary>
/// Raised after the web view is initialized but before any content has been loaded into the web view. The event arguments provide the instance of the platform-specific web view control.
/// </summary>
public event EventHandler<HybridWebViewInitializedEventArgs>? HybridWebViewInitialized;
public void Navigate(string url)
{
NavigateCore(url);
}
protected override async void OnHandlerChanged()
{
base.OnHandlerChanged();
await InitializeHybridWebView();
// HybridWebViewInitialized assumes Handler != null
if (Handler == null)
{
return;
}
HybridWebViewInitialized?.Invoke(this, new HybridWebViewInitializedEventArgs()
{
#if ANDROID || IOS || MACCATALYST || WINDOWS
WebView = PlatformWebView,
#endif
});
Navigate(StartPath);
}
private partial Task InitializeHybridWebView();
private partial void NavigateCore(string url);
#if !ANDROID && !IOS && !MACCATALYST && !WINDOWS
private partial Task InitializeHybridWebView() => throw null!;
private partial void NavigateCore(string url) => throw null!;
#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>
/// <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>A string containing the return value of the called method.</returns>
public async Task<string> InvokeJsMethodAsync(string methodName, params object[] paramValues)
{
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentException($"The method name cannot be null or empty.", nameof(methodName));
}
return await EvaluateJavaScriptAsync($"{methodName}({(paramValues == null ? string.Empty : string.Join(", ", paramValues.Select(v => JsonSerializer.Serialize(v))))})");
}
/// <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>
public async Task<TReturnType?> InvokeJsMethodAsync<TReturnType>(string methodName, params object[] paramValues)
{
var stringResult = await InvokeJsMethodAsync(methodName, paramValues);
if (stringResult is null)
{
return default;
}
return JsonSerializer.Deserialize<TReturnType>(stringResult);
}
public virtual void OnMessageReceived(string message)
{
var messageData = JsonSerializer.Deserialize<WebMessageData>(message);
switch (messageData?.MessageType)
{
case 0: // "raw" message (just a string)
RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(messageData.MessageContent));
break;
case 1: // "invoke" message
if (messageData.MessageContent == null)
{
throw new InvalidOperationException($"Expected invoke message to contain MessageContent, but it was null.");
}
var invokeData = JsonSerializer.Deserialize<JSInvokeMethodData>(messageData.MessageContent)!;
InvokeDotNetMethod(invokeData);
break;
default:
throw new InvalidOperationException($"Unknown message type: {messageData?.MessageType}. Message contents: {messageData?.MessageContent}");
}
}
/// <summary>
/// Handle the proxy request message.
/// </summary>
/// <param name="args"></param>
/// <returns>A Task</returns>
public virtual async Task OnProxyRequestMessage(HybridWebViewProxyEventArgs args)
{
// Don't let failed proxy requests crash the app.
try
{
// When no query parameters are passed, the SendRoundTripMessageToDotNet JavaScript method is expected to have been called.
if (args.QueryParams != null && args.QueryParams.TryGetValue("__ajax", out string? jsonQueryString))
{
if (jsonQueryString != null)
{
var invokeData = JsonSerializer.Deserialize<JSInvokeMethodData>(jsonQueryString);
if (invokeData != null && invokeData.MethodName != null)
{
object? result = InvokeDotNetMethod(invokeData);
if (result != null)
{
args.ResponseContentType = "application/json";
DotNetInvokeResult dotNetInvokeResult;
var resultType = result.GetType();
if (resultType.IsArray || resultType.IsClass)
{
dotNetInvokeResult = new DotNetInvokeResult()
{
Result = JsonSerializer.Serialize(result),
IsJson = true,
};
}
else
{
dotNetInvokeResult = new DotNetInvokeResult()
{
Result = result,
};
}
args.ResponseStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(dotNetInvokeResult)));
}
}
}
}
else if (ProxyRequestReceived != null) //Check to see if user has subscribed to the event.
{
await ProxyRequestReceived(args);
}
}
catch (Exception ex)
{
Debug.WriteLine($"An exception occurred while handling the proxy request: {ex.Message}");
}
}
private object? InvokeDotNetMethod(JSInvokeMethodData invokeData)
{
if (JSInvokeTarget is null)
{
throw new NotImplementedException($"The {nameof(JSInvokeTarget)} property must have a value in order to invoke a .NET method from JavaScript.");
}
var invokeMethod = JSInvokeTarget.GetType().GetMethod(invokeData.MethodName!, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.InvokeMethod);
if (invokeMethod == null)
{
throw new InvalidOperationException($"The method {invokeData.MethodName} couldn't be found on the {nameof(JSInvokeTarget)} of type {JSInvokeTarget.GetType().FullName}.");
}
if (invokeData.ParamValues != null && invokeMethod.GetParameters().Length != invokeData.ParamValues.Length)
{
throw new InvalidOperationException($"The number of parameters on {nameof(JSInvokeTarget)}'s method {invokeData.MethodName} ({invokeMethod.GetParameters().Length}) doesn't match the number of values passed from JavaScript code ({invokeData.ParamValues.Length}).");
}
var paramObjectValues =
invokeData.ParamValues?
.Zip(invokeMethod.GetParameters(), (s, p) => s == null ? null : JsonSerializer.Deserialize(s, p.ParameterType))
.ToArray();
return invokeMethod.Invoke(JSInvokeTarget, paramObjectValues);
}
private sealed class JSInvokeMethodData
{
public string? MethodName { get; set; }
public string[]? ParamValues { get; set; }
}
private sealed class WebMessageData
{
public int MessageType { get; set; }
public string? MessageContent { get; set; }
}
/// <summary>
/// A simple internal class to hold the result of a .NET method invocation, and whether it should be treated as JSON.
/// </summary>
private sealed class DotNetInvokeResult
{
public object? Result { get; set; }
public bool IsJson { get; set; }
}
internal static async Task<string?> GetAssetContentAsync(string assetPath)
{
using var stream = await GetAssetStreamAsync(assetPath);
if (stream == null)
{
return null;
}
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
return contents;
}
internal static async Task<Stream?> GetAssetStreamAsync(string assetPath)
{
if (!await FileSystem.AppPackageFileExistsAsync(assetPath))
{
return null;
}
return await FileSystem.OpenAppPackageFileAsync(assetPath);
}
}
}