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

Proxy requests and round trip JavaScript invocations #43

Merged
merged 10 commits into from
Mar 11, 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
83 changes: 80 additions & 3 deletions HybridWebView/HybridWebView.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System.Text.Json;
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>.
Expand Down Expand Up @@ -34,6 +37,12 @@ public partial class HybridWebView : WebView

public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;

/// <summary>
/// Async event handler that is called when a proxy request is received from the webview.
/// </summary>

public event Func<HybridWebViewProxyEventArgs, Task>? ProxyRequestReceived;

public void Navigate(string url)
{
NavigateCore(url);
Expand Down Expand Up @@ -117,7 +126,66 @@ public virtual void OnMessageReceived(string message)

}

private void InvokeDotNetMethod(JSInvokeMethodData invokeData)
/// <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)
{
Expand All @@ -140,7 +208,7 @@ private void InvokeDotNetMethod(JSInvokeMethodData invokeData)
.Zip(invokeMethod.GetParameters(), (s, p) => JsonSerializer.Deserialize(s, p.ParameterType))
.ToArray();

var returnValue = invokeMethod.Invoke(JSInvokeTarget, paramObjectValues);
return invokeMethod.Invoke(JSInvokeTarget, paramObjectValues);
}

private sealed class JSInvokeMethodData
Expand All @@ -155,6 +223,15 @@ private sealed class WebMessageData
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);
Expand Down
39 changes: 39 additions & 0 deletions HybridWebView/HybridWebViewProxyEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace HybridWebView
{
/// <summary>
/// Event arg object for a proxy request from the <see cref="HybridWebView"/>.
/// </summary>
public class HybridWebViewProxyEventArgs
{
/// <summary>
/// Creates a new instance of <see cref="HybridWebViewProxyEventArgs"/>.
/// </summary>
/// <param name="fullUrl">The full request URL.</param>
public HybridWebViewProxyEventArgs(string fullUrl)
{
Url = fullUrl;
QueryParams = QueryStringHelper.GetKeyValuePairs(fullUrl);
}

/// <summary>
/// The full request URL.
/// </summary>
public string Url { get; }

/// <summary>
/// Query string values extracted from the request URL.
/// </summary>
public IDictionary<string, string> QueryParams { get; }

/// <summary>
/// The response content type.
/// </summary>

public string? ResponseContentType { get; set; } = "text/plain";

/// <summary>
/// The response stream to be used to respond to the request.
/// </summary>
public Stream? ResponseStream { get; set; } = null;
}
}
67 changes: 67 additions & 0 deletions HybridWebView/KnownStaticFiles/HybridWebView.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
// Standard methods for HybridWebView

window.HybridWebView = {
/**
* Sends a message to .NET using the built in
* @param {string} message Message to send.
*/
"SendRawMessageToDotNet": function SendRawMessageToDotNet(message) {
window.HybridWebView.SendMessageToDotNet(0, message);
},

/**
* Invoke a .NET method. No result is expected.
* @param {string} methodName Name of .NET method to invoke.
* @param {any[]} paramValues Parameters to pass to the method.
*/
"SendInvokeMessageToDotNet": function SendInvokeMessageToDotNet(methodName, paramValues) {
if (typeof paramValues !== 'undefined') {
if (!Array.isArray(paramValues)) {
Expand All @@ -18,6 +27,64 @@ window.HybridWebView = {
window.HybridWebView.SendMessageToDotNet(1, JSON.stringify({ "MethodName": methodName, "ParamValues": paramValues }));
},

/**
* Asynchronously invoke .NET method and get a result.
* Leverages the proxy to send the message to .NET.
* @param {string} methodName Name of .NET method to invoke.
* @param {any[]} paramValues Parameters to pass to the method.
* @returns {Promise<any>} Result of the .NET method.
*/
"SendInvokeMessageToDotNetAsync": async function SendInvokeMessageToDotNetAsync(methodName, paramValues) {
const body = {
MethodName: methodName
};

if (typeof paramValues !== 'undefined') {
if (!Array.isArray(paramValues)) {
paramValues = [paramValues];
}

for (var i = 0; i < paramValues.length; i++) {
paramValues[i] = JSON.stringify(paramValues[i]);
}

if (paramValues.length > 0) {
body.ParamValues = paramValues;
}
}

const message = JSON.stringify(body);

try {
// Android web view doesn't support getting the body of a POST request, so we use a GET request instead and pass the body as a query string parameter.
var requestUrl = `${window.location.origin}/proxy?__ajax=${encodeURIComponent(message)}`;

const rawResponse = await fetch(requestUrl, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
const response = await rawResponse.json();

if (response) {
if (response.IsJson) {
return JSON.parse(response.Result);
}

return response.Result;
}
} catch (e) { }

return null;
},

/**
* Sends a message to .NET using the built in
* @private
* @param {number} messageType The type of message to send.
* @param {string} messageContent The message content.
*/
"SendMessageToDotNet": function SendMessageToDotNet(messageType, messageContent) {
var message = JSON.stringify({ "MessageType": messageType, "MessageContent": messageContent });

Expand Down
31 changes: 27 additions & 4 deletions HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Android.Webkit;
using Java.Time;
using Microsoft.Maui.Platform;
using System.Text;
using AWebView = Android.Webkit.WebView;
Expand All @@ -15,8 +16,10 @@
}
public override WebResourceResponse? ShouldInterceptRequest(AWebView? view, IWebResourceRequest? request)
{
var requestUri = request?.Url?.ToString();
requestUri = QueryStringHelper.RemovePossibleQueryString(requestUri);
var fullUrl = request?.Url?.ToString();
var requestUri = QueryStringHelper.RemovePossibleQueryString(fullUrl);

var webView = (HybridWebView)_handler.VirtualView;

if (new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
{
Expand All @@ -25,7 +28,7 @@
string contentType;
if (string.IsNullOrEmpty(relativePath))
{
relativePath = ((HybridWebView)_handler.VirtualView).MainFile;
relativePath = webView.MainFile;
contentType = "text/html";
}
else
Expand All @@ -40,7 +43,27 @@
};
}

var contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!);
Stream? contentStream = null;

// Check to see if the request is a proxy request.
if (relativePath == HybridWebView.ProxyRequestPath)
{
var args = new HybridWebViewProxyEventArgs(fullUrl);

Check warning on line 51 in HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs

View workflow job for this annotation

GitHub Actions / call-build-workflow / Build (windows-latest)

Possible null reference argument for parameter 'fullUrl' in 'HybridWebViewProxyEventArgs.HybridWebViewProxyEventArgs(string fullUrl)'.

Check warning on line 51 in HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs

View workflow job for this annotation

GitHub Actions / call-build-workflow / Build (macos-14)

Possible null reference argument for parameter 'fullUrl' in 'HybridWebViewProxyEventArgs.HybridWebViewProxyEventArgs(string fullUrl)'.

// TODO: Don't block async. Consider making this an async call, and then calling DidFinish when done
webView.OnProxyRequestMessage(args).Wait();

if (args.ResponseStream != null)
{
contentType = args.ResponseContentType ?? "text/plain";
contentStream = args.ResponseStream;
}
}

if (contentStream == null)
{
contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!);
}

if (contentStream is null)
{
Expand Down
Loading
Loading