diff --git a/ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png b/ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png
new file mode 100644
index 0000000..aecf8d1
Binary files /dev/null and b/ExampleWrappedHybridWebViewLibrary/EmbeddedImage.png differ
diff --git a/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html b/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html
new file mode 100644
index 0000000..7adafcb
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/EmbeddedWebPageResource.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
HybridWebView demo: Embedded Resource page
+
+ This web page is an embedded resource. It can access resources from in the
+ same way as normal via the HybridAssetRoot (not an embedded resrouce) and other embedded resources.
+ Native/JS communication works as expected.
+ This is useful when using the HybridWebView in a .NET Maui library when wrapping a web app that is
+ to be conusmed by other .NET Maui app.
+
+
+
+
+
+
+
+
+ The following image is loaded from an embedded resource via the proxy:
+
+
+
+
+
+
+
+
+
diff --git a/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj b/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj
new file mode 100644
index 0000000..c2fdf7c
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/ExampleWrappedHybridWebViewLibrary.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+ true
+ true
+ enable
+ enable
+
+ 11.0
+ 13.1
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs
new file mode 100644
index 0000000..b6788f6
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/MyCustomControl.cs
@@ -0,0 +1,122 @@
+using HybridWebView;
+
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ ///
+ /// A custom control that wraps a hybrid web view and loads embedded resources in addition to supporting the
+ /// Hybrid asset root and all other capabilites of the HybridWebView.
+ ///
+ public class MyCustomControl: Grid
+ {
+ private readonly HybridWebView.HybridWebView _webView;
+
+ #region Constructor
+
+ public MyCustomControl() : base()
+ {
+ bool enableWebDevTools = false;
+
+#if DEBUG
+ //Enable web dev tools when in debug mode.
+ enableWebDevTools = true;
+#endif
+
+ //Create a web view control.
+ _webView = new HybridWebView.HybridWebView
+ {
+ HybridAssetRoot = HybridAssetRoot ?? "hybrid_root",
+ MainFile = "proxy?operation=embeddedResource&resourceName=EmbeddedWebPageResource.html",
+ EnableWebDevTools = enableWebDevTools
+ };
+
+ //Set the target for JavaScript interop.
+ _webView.JSInvokeTarget = new MyJSInvokeTarget();
+
+ //Monitor proxy requests.
+ _webView.ProxyRequestReceived += WebView_ProxyRequestReceived;
+
+#if WINDOWS
+ //In Windows, disable manual user zooming of web pages.
+ _webView.HybridWebViewInitialized += (s, e) =>
+ {
+ //Disable the user manually zooming. Don't want the user accidentally zooming the HTML page.
+ e.WebView.CoreWebView2.Settings.IsZoomControlEnabled = false;
+ };
+#endif
+
+ //Add the web view to the control.
+ this.Children.Insert(0, _webView);
+ }
+
+ #endregion
+
+ #region Public Properties
+
+ public string? HybridAssetRoot { get; set; } = null;
+
+ #endregion
+
+ #region Private Methods
+
+ private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxyEventArgs args)
+ {
+ // Check to see if our custom parameter is present.
+ if (args.QueryParams.ContainsKey("operation"))
+ {
+ switch (args.QueryParams["operation"])
+ {
+ case "embeddedResource":
+ if (args.QueryParams.TryGetValue("resourceName", out string? resourceName) && !string.IsNullOrWhiteSpace(resourceName))
+ {
+ var thisAssembly = typeof(MyCustomControl).Assembly;
+ var assemblyName = thisAssembly.GetName().Name;
+ using (var fs = thisAssembly.GetManifestResourceStream($"{assemblyName}.{resourceName.Replace("/", ".")}"))
+ {
+ if (fs != null)
+ {
+ var ms = new MemoryStream();
+ await fs.CopyToAsync(ms);
+ ms.Position = 0;
+
+ args.ResponseStream = ms;
+ args.ResponseContentType = PathUtils.GetMimeType(resourceName);
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private sealed class MyJSInvokeTarget
+ {
+ public MyJSInvokeTarget()
+ {
+ }
+
+ ///
+ /// An example of a round trip method that takes an input parameter and returns a simple value type (number).
+ ///
+ ///
+ ///
+ public double Fibonacci(int n)
+ {
+ if (n == 0) return 0;
+
+ int prev = 0;
+ int next = 1;
+ for (int i = 1; i < n; i++)
+ {
+ int sum = prev + next;
+ prev = next;
+ next = sum;
+ }
+ return next;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs
new file mode 100644
index 0000000..f822afc
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Android/PlatformClass1.cs
@@ -0,0 +1,7 @@
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ // All the code in this file is only included on Android.
+ public class PlatformClass1
+ {
+ }
+}
diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs
new file mode 100644
index 0000000..98e6f32
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/Platforms/MacCatalyst/PlatformClass1.cs
@@ -0,0 +1,7 @@
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ // All the code in this file is only included on Mac Catalyst.
+ public class PlatformClass1
+ {
+ }
+}
diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs
new file mode 100644
index 0000000..5bc2f1e
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Tizen/PlatformClass1.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ // All the code in this file is only included on Tizen.
+ public class PlatformClass1
+ {
+ }
+}
\ No newline at end of file
diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs
new file mode 100644
index 0000000..f21c4ea
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/Platforms/Windows/PlatformClass1.cs
@@ -0,0 +1,7 @@
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ // All the code in this file is only included on Windows.
+ public class PlatformClass1
+ {
+ }
+}
diff --git a/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs b/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs
new file mode 100644
index 0000000..68b9c82
--- /dev/null
+++ b/ExampleWrappedHybridWebViewLibrary/Platforms/iOS/PlatformClass1.cs
@@ -0,0 +1,7 @@
+namespace ExampleWrappedHybridWebViewLibrary
+{
+ // All the code in this file is only included on iOS.
+ public class PlatformClass1
+ {
+ }
+}
diff --git a/HybridWebView/HybridWebViewProxyEventArgs.cs b/HybridWebView/HybridWebViewProxyEventArgs.cs
index 0f6299d..77385f1 100644
--- a/HybridWebView/HybridWebViewProxyEventArgs.cs
+++ b/HybridWebView/HybridWebViewProxyEventArgs.cs
@@ -15,6 +15,18 @@ public HybridWebViewProxyEventArgs(string fullUrl)
QueryParams = QueryStringHelper.GetKeyValuePairs(fullUrl);
}
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The full request URL.
+ /// The estimated response content type based on uri parsing.
+ public HybridWebViewProxyEventArgs(string fullUrl, string contentType)
+ {
+ Url = fullUrl;
+ ResponseContentType = contentType;
+ QueryParams = QueryStringHelper.GetKeyValuePairs(fullUrl);
+ }
+
///
/// The full request URL.
///
@@ -35,5 +47,11 @@ public HybridWebViewProxyEventArgs(string fullUrl)
/// The response stream to be used to respond to the request.
///
public Stream? ResponseStream { get; set; } = null;
+
+ ///
+ /// Additional headers to be added to the response.
+ /// Useful for things like adding a cache control header.
+ ///
+ public IDictionary? CustomResponseHeaders { get; set; }
}
}
diff --git a/HybridWebView/PathUtils.cs b/HybridWebView/PathUtils.cs
index 2a92cee..d137e13 100644
--- a/HybridWebView/PathUtils.cs
+++ b/HybridWebView/PathUtils.cs
@@ -1,10 +1,224 @@
-namespace HybridWebView
+using System;
+
+namespace HybridWebView
{
- internal static class PathUtils
+ public static class PathUtils
{
+ public const string PlanTextMimeType = "text/plain";
+ public const string HtmlMimeType = "text/html";
+
public static string NormalizePath(string filename) =>
filename
.Replace('\\', Path.DirectorySeparatorChar)
.Replace('/', Path.DirectorySeparatorChar);
+
+ public static void GetRelativePathAndContentType(Uri appOriginUri, Uri requestUri, string originalUrl, string? mainFileName, out string relativePath, out string contentType, out string fullUrl)
+ {
+ relativePath = appOriginUri.MakeRelativeUri(requestUri).ToString().Replace('/', '\\');
+ fullUrl = originalUrl;
+
+ if (string.IsNullOrEmpty(relativePath))
+ {
+ //The main file may be a URL that has a query string. For example if we want the main page to go through the proxy.
+ if (!string.IsNullOrEmpty(mainFileName))
+ {
+ relativePath = QueryStringHelper.RemovePossibleQueryString(mainFileName);
+ fullUrl = mainFileName;
+ }
+
+ //Try and get the mime type from the full URL (main file could be a URL that has a query string pointing to a file such as a PDF).
+ string? minType;
+ if (PathUtils.TryGetMimeType(fullUrl, out minType))
+ {
+ if (!string.IsNullOrEmpty(minType))
+ {
+ contentType = minType;
+ }
+ else
+ {
+ contentType = PathUtils.HtmlMimeType;
+ }
+ }
+ else
+ {
+ contentType = PathUtils.HtmlMimeType;
+ }
+ }
+ else
+ {
+ contentType = PathUtils.GetMimeType(fullUrl);
+ }
+ }
+
+ ///
+ /// Tries to get the mime type from a file name or URL by looking for valid file extensions or mime types in a data URI.
+ /// Input can be a file name, url (with query string), a data uri, mime type, or file extension.
+ ///
+ /// A file name, url (with query string), a data uri, mime type, or file extension.
+ /// The determined mime type. Fallback to "text/plain"
+ public static string GetMimeType(string fileNameOrUrl)
+ {
+ string? ext;
+ string? mimeType;
+
+ //Check for a mime type in a data uri.
+ if (fileNameOrUrl.Contains("data:") && fileNameOrUrl.Contains(";base64,"))
+ {
+ ext = fileNameOrUrl.Substring(5, fileNameOrUrl.IndexOf(";base64,") - 5);
+
+ if (TryGetMimeType(ext, out mimeType))
+ {
+ return mimeType ?? PlanTextMimeType;
+ }
+ }
+
+ //Seperate out query string if it exists.
+ string queryString = string.Empty;
+
+ if (fileNameOrUrl.Contains("?"))
+ {
+ queryString = fileNameOrUrl.Substring(fileNameOrUrl.IndexOf("?"));
+ fileNameOrUrl = fileNameOrUrl.Substring(0, fileNameOrUrl.IndexOf("?"));
+ }
+
+ //If there is still a url or file name, check it for a valid file extension.
+ if (!string.IsNullOrWhiteSpace(fileNameOrUrl))
+ {
+ ext = Path.GetExtension(fileNameOrUrl);
+
+ if (TryGetMimeType(ext, out mimeType))
+ {
+ return mimeType ?? PlanTextMimeType;
+ }
+
+ //Try passing the whole file name to see if it is itself a valid mime type. This would work if only a file extension or mimetype was passed in.
+ if (TryGetMimeType(fileNameOrUrl, out mimeType))
+ {
+ return mimeType ?? PlanTextMimeType;
+ }
+ }
+
+ //If there is a query string, check it's parameter values to see if it contains something with a valid file extension.
+ if (!string.IsNullOrWhiteSpace(queryString))
+ {
+ var queryParameters = queryString.Split('&');
+
+ foreach (var param in queryParameters)
+ {
+ if (param.Contains("="))
+ {
+ ext = param.Substring(param.IndexOf("=") + 1);
+
+ if (TryGetMimeType(ext, out mimeType))
+ {
+ return mimeType ?? PlanTextMimeType;
+ }
+ }
+ }
+ }
+
+ //If get here, return plain text mime type.
+ return PlanTextMimeType;
+ }
+
+ ///
+ /// Looks up a mime type based on a file extension or mime type.
+ ///
+ /// A mimeType or file extension to validate and get the mime type for.
+ /// A boolean indicating if it found a supported mime type.
+ public static bool TryGetMimeType(string? mimeTypeOrFileExtension, out string? mimeType)
+ {
+ mimeType = null;
+
+ if (string.IsNullOrWhiteSpace(mimeTypeOrFileExtension))
+ {
+ return false;
+ }
+
+ //If content type starts with a period, remove it. File extension may have been passed in.
+ if (mimeTypeOrFileExtension.StartsWith("."))
+ {
+ mimeTypeOrFileExtension = mimeTypeOrFileExtension.Substring(1);
+ }
+
+ //For simplirity, if the content type contains a slash, assume it is a file path extension.
+ if (mimeTypeOrFileExtension.Contains("/"))
+ {
+ //Return the string after the last index of the slash.
+ mimeTypeOrFileExtension = mimeTypeOrFileExtension.Substring(mimeTypeOrFileExtension.LastIndexOf("/") + 1);
+ }
+
+ //Sanitize the content type.
+ mimeType = mimeTypeOrFileExtension.ToLowerInvariant() switch
+ {
+ //WebAssembly file types
+ "wasm" => "application/wasm",
+
+ //Image file types
+ "png" => "image/png",
+ "jpg" or "jpeg" or "jfif" or "pjpeg" or "pjp" => "image/jpeg",
+ "gif" => "image/gif",
+ "webp" => "image/webp",
+ "svg" or "svg+xml" => "image/svg+xml",
+ "ico" or "x-icon" => "image/x-icon",
+ "bmp" => "image/bmp",
+ "tif" or "tiff" => "image/tiff",
+ "avif" => "image/avif",
+ "apng" => "image/apng",
+
+ //Video file types
+ "mp4" => "video/mp4",
+ "webm" => "video/webm",
+ "mpeg" => "video/mpeg",
+
+ //Audio file types
+ "mp3" => "audio/mpeg",
+ "wav" => "audio/wav",
+
+ //Font file types
+ "woff" => "font/woff",
+ "woff2" => "font/woff2",
+ "otf" => "font/otf",
+
+ //JSON and XML based file types
+ "json" or "geojson" or "geojsonseq" or "topojson" => "application/json",
+ "gpx" or "georss" or "gml" or "citygml" or "czml" or "xml" => "application/xml",
+ "kml" or "kml+xml" or "vnd.google-earth.kml+xml" => "application/vnd.google-earth.kml+xml",
+
+ //Office file types
+ "doc" or "docx" or "msword" => "application/msword",
+ "xls" or "xlsx" or "vnd.ms-excel" => "application/vnd.ms-excel",
+ "ppt" or "pptx" or "vnd.ms-powerpoint" => "application/vnd.ms-powerpoint",
+
+ //3D model file types commonly used in web.
+ "gltf" or "gltf+json" => "model/gltf+json",
+ "glb" or "gltf-binary" => "model/gltf-binary",
+ "dae" => "model/vnd.collada+xml",
+
+ //Other binary file types
+ "zip" => "application/zip",
+ "pbf" or "x-protobuf" => "application/x-protobuf",
+ "mvt" or "vnd.mapbox-vector-tile" => "application/vnd.mapbox-vector-tile",
+ "kmz" or "vnd.google-earth.kmz" or "shp" or "dbf" or "bin" or "b3dm" or "i3dm" or "pnts" or "subtree" or "octet-stream" => "application/octet-stream",
+ "pdf" => "application/pdf",
+
+ //Other map tile file types
+ "terrian" => "application/vnd.quantized-mesh",
+ "pmtiles" => "application/vnd.pmtiles",
+
+ //Text based file types
+ "htm" or "html" => "text/html",
+ "xhtml" or "xhtml+xml" => "application/xhtml+xml",
+ "js" or "javascript" => "text/javascript",
+ "css" => "text/css",
+ "csv" => "text/csv",
+ "md" => "text/markdown",
+ "plain" or "txt" or "wat" => "text/plain",
+
+ _ => null,
+ };
+
+ return mimeType != null;
+ }
}
}
diff --git a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs
index f0f6619..35592af 100644
--- a/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs
+++ b/HybridWebView/Platforms/Android/AndroidHybridWebViewClient.cs
@@ -14,53 +14,38 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler)
{
_handler = handler;
}
+
public override WebResourceResponse? ShouldInterceptRequest(AWebView? view, IWebResourceRequest? request)
{
- var fullUrl = request?.Url?.ToString();
- var requestUri = QueryStringHelper.RemovePossibleQueryString(fullUrl);
+ var originalUrl = request?.Url?.ToString();
+ var requestUri = QueryStringHelper.RemovePossibleQueryString(originalUrl);
var webView = (HybridWebView)_handler.VirtualView;
- if (new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
+ if (!string.IsNullOrEmpty(originalUrl) && new Uri(requestUri) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
{
- var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\');
-
- string contentType;
- if (string.IsNullOrEmpty(relativePath))
- {
- relativePath = webView.MainFile;
- contentType = "text/html";
- }
- else
- {
- var requestExtension = Path.GetExtension(relativePath);
- contentType = requestExtension switch
- {
- ".htm" or ".html" => "text/html",
- ".js" => "application/javascript",
- ".css" => "text/css",
- _ => "text/plain",
- };
- }
-
+ PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, webView.MainFile, out string relativePath, out string contentType, out string fullUrl);
+
Stream? contentStream = null;
+ IDictionary? customHeaders = null;
// Check to see if the request is a proxy request.
- if (relativePath == HybridWebView.ProxyRequestPath)
+ if (!string.IsNullOrEmpty(relativePath) && relativePath.Equals(HybridWebView.ProxyRequestPath))
{
- var args = new HybridWebViewProxyEventArgs(fullUrl);
+ var args = new HybridWebViewProxyEventArgs(fullUrl, contentType);
// 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";
+ contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType;
contentStream = args.ResponseStream;
+ customHeaders = args.CustomResponseHeaders;
}
}
- if (contentStream == null)
+ if (contentStream is null)
{
contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!);
}
@@ -78,12 +63,12 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler)
var notFoundByteArray = Encoding.UTF8.GetBytes(notFoundContent);
var notFoundContentStream = new MemoryStream(notFoundByteArray);
- return new WebResourceResponse("text/plain", "UTF-8", 404, "Not Found", GetHeaders("text/plain"), notFoundContentStream);
+ return new WebResourceResponse("text/plain", "UTF-8", 404, "Not Found", GetHeaders("text/plain", null), notFoundContentStream);
}
else
{
// TODO: We don't know the content length because Android doesn't tell us. Seems to work without it!
- return new WebResourceResponse(contentType, "UTF-8", 200, "OK", GetHeaders(contentType), contentStream);
+ return new WebResourceResponse(contentType, "UTF-8", 200, "OK", GetHeaders(contentType, customHeaders), contentStream);
}
}
else
@@ -106,9 +91,29 @@ public AndroidHybridWebViewClient(HybridWebViewHandler handler) : base(handler)
}
}
- private protected static IDictionary GetHeaders(string contentType) =>
- new Dictionary {
- { "Content-Type", contentType },
- };
+ private protected static IDictionary GetHeaders(string contentType, IDictionary? customHeaders)
+ {
+ var headers = new Dictionary();
+
+ if (customHeaders != null)
+ {
+ foreach (var header in customHeaders)
+ {
+ // Add custom headers to the response. Skip the Content-Length and Content-Type headers.
+ if (header.Key != "Content-Length" && header.Key != "Content-Type")
+ {
+ headers[header.Key] = header.Value;
+ }
+ }
+ }
+
+ //If a custom header hasn't specified a content type, use the one we've determined.
+ if (!headers.ContainsKey("Content-Type"))
+ {
+ headers.Add("Content-Type", contentType);
+ }
+
+ return headers;
+ }
}
}
diff --git a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs
index d31f86f..b7a38a6 100644
--- a/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs
+++ b/HybridWebView/Platforms/MacCatalyst/HybridWebViewHandler.MacCatalyst.cs
@@ -1,5 +1,7 @@
using Foundation;
+using Intents;
using Microsoft.Maui.Platform;
+using System;
using System.Drawing;
using System.Globalization;
using System.Runtime.Versioning;
@@ -78,8 +80,26 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
{
dic.Add((NSString)"Content-Length", (NSString)(responseData.ResponseBytes.Length.ToString(CultureInfo.InvariantCulture)));
dic.Add((NSString)"Content-Type", (NSString)responseData.ContentType);
- // Disable local caching. This will prevent user scripts from executing correctly.
- dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
+
+ if (responseData.CustomHeaders != null)
+ {
+ foreach (var header in responseData.CustomHeaders)
+ {
+ // Add custom headers to the response. Skip the Content-Length and Content-Type headers.
+ if (header.Key != "Content-Length" && header.Key != "Content-Type")
+ {
+ dic.Add((NSString)header.Key, (NSString)header.Value);
+ }
+ }
+ }
+
+ //Ensure that the Cache-Control header is not set in the custom headers.
+ if(responseData.CustomHeaders == null || responseData.CustomHeaders.ContainsKey("Cache-Control"))
+ {
+ // Disable local caching. This will prevent user scripts from executing correctly.
+ dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
+ }
+
if (urlSchemeTask.Request.Url != null)
{
using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, responseData.StatusCode, "HTTP/1.1", dic);
@@ -92,55 +112,36 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
}
}
- private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytes(string? url)
+ private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode, IDictionary? CustomHeaders)> GetResponseBytes(string? url)
{
- string contentType;
+ //string contentType;
- string fullUrl = url;
+ string? originalUrl = url;
url = QueryStringHelper.RemovePossibleQueryString(url);
- if (new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
+ if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
{
- var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/');
-
var hwv = (HybridWebView)_webViewHandler.VirtualView;
-
- var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!);
-
- if (string.IsNullOrEmpty(relativePath))
- {
- relativePath = hwv.MainFile!.Replace('\\', '/');
- contentType = "text/html";
- }
- else
- {
- var requestExtension = Path.GetExtension(relativePath);
- contentType = requestExtension switch
- {
- ".htm" or ".html" => "text/html",
- ".js" => "application/javascript",
- ".css" => "text/css",
- _ => "text/plain",
- };
- }
-
+ PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string relativePath, out string contentType, out string fullUrl);
+
Stream? contentStream = null;
+ IDictionary? customHeaders = null;
// Check to see if the request is a proxy request.
if (relativePath == HybridWebView.ProxyRequestPath)
{
- var args = new HybridWebViewProxyEventArgs(fullUrl);
-
+ var args = new HybridWebViewProxyEventArgs(fullUrl, contentType);
await hwv.OnProxyRequestMessage(args);
if (args.ResponseStream != null)
{
- contentType = args.ResponseContentType ?? "text/plain";
+ contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType;
contentStream = args.ResponseStream;
+ customHeaders = args.CustomResponseHeaders;
}
}
- if (contentStream == null)
+ if (contentStream is null)
{
contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!);
}
@@ -149,18 +150,19 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
{
using var ms = new MemoryStream();
contentStream.CopyTo(ms);
- return (ms.ToArray(), contentType, StatusCode: 200);
+ return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders);
}
- var assetPath = Path.Combine(bundleRootDir, relativePath);
+ string bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot ?? "");
+ string assetPath = Path.Combine(bundleRootDir, relativePath);
if (File.Exists(assetPath))
{
- return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200);
+ return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200, CustomHeaders: null);
}
}
- return (Array.Empty(), ContentType: string.Empty, StatusCode: 404);
+ return (Array.Empty(), ContentType: string.Empty, StatusCode: 404, CustomHeaders: null);
}
[Export("webView:stopURLSchemeTask:")]
diff --git a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs
index dfa037e..0b2c70b 100644
--- a/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs
+++ b/HybridWebView/Platforms/Windows/HybridWebView.Windows.cs
@@ -46,45 +46,27 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe
{
// Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method.
using var deferral = eventArgs.GetDeferral();
-
+
var requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri);
-
+
if (new Uri(requestUri) is Uri uri && AppOriginUri.IsBaseOf(uri))
{
- var relativePath = AppOriginUri.MakeRelativeUri(uri).ToString().Replace('/', '\\');
-
- string contentType;
- if (string.IsNullOrEmpty(relativePath))
- {
- relativePath = MainFile;
- contentType = "text/html";
- }
- else
- {
- var requestExtension = Path.GetExtension(relativePath);
- contentType = requestExtension switch
- {
- ".htm" or ".html" => "text/html",
- ".js" => "application/javascript",
- ".css" => "text/css",
- _ => "text/plain",
- };
- }
-
+ PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, eventArgs.Request.Uri, MainFile, out string relativePath, out string contentType, out string fullUrl);
+
Stream? contentStream = null;
+ IDictionary? customHeaders = null;
// Check to see if the request is a proxy request
if (relativePath == ProxyRequestPath)
{
- var fullUrl = eventArgs.Request.Uri;
-
- var args = new HybridWebViewProxyEventArgs(fullUrl);
+ var args = new HybridWebViewProxyEventArgs(fullUrl, contentType);
await OnProxyRequestMessage(args);
if (args.ResponseStream != null)
{
- contentType = args.ResponseContentType ?? "text/plain";
+ contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType;
contentStream = args.ResponseStream;
+ customHeaders = args.CustomResponseHeaders;
}
}
@@ -106,7 +88,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe
Content: null,
StatusCode: 404,
ReasonPhrase: "Not Found",
- Headers: GetHeaderString("text/plain", notFoundContent.Length)
+ Headers: GetHeaderString("text/plain", notFoundContent.Length, null)
);
}
else
@@ -115,7 +97,7 @@ private async void CoreWebView2_WebResourceRequested(CoreWebView2 sender, CoreWe
Content: await CopyContentToRandomAccessStreamAsync(contentStream),
StatusCode: 200,
ReasonPhrase: "OK",
- Headers: GetHeaderString(contentType, (int)contentStream.Length)
+ Headers: GetHeaderString(contentType, (int)contentStream.Length, customHeaders)
);
}
@@ -135,9 +117,34 @@ async Task CopyContentToRandomAccessStreamAsync(Stream cont
}
}
- private protected static string GetHeaderString(string contentType, int contentLength) =>
-$@"Content-Type: {contentType}
-Content-Length: {contentLength}";
+ private protected static string GetHeaderString(string contentType, int contentLength, IDictionary? customHeaders)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.AppendLine($"Content-Type: {contentType}");
+ sb.AppendLine($"Content-Length: {contentLength}");
+
+ if (customHeaders != null)
+ {
+ foreach (var header in customHeaders)
+ {
+ // Add custom headers to the response. Skip the Content-Length and Content-Type headers.
+ if (header.Key != "Content-Length" && header.Key != "Content-Type")
+ {
+ sb.AppendLine($"{header.Key}: {header.Value}");
+ }
+ }
+ }
+
+ // Ensure that the Cache-Control header is not set in the custom headers.
+ if (customHeaders == null || !customHeaders.ContainsKey("Cache-Control"))
+ {
+ // Disable local caching. This will prevent user scripts from executing correctly.
+ sb.AppendLine("Cache-Control: no-cache, max-age=0, must-revalidate, no-store");
+ }
+
+ return sb.ToString();
+ }
private void Wv2_WebMessageReceived(Microsoft.UI.Xaml.Controls.WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
diff --git a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs
index a5fcf9f..5dd342a 100644
--- a/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs
+++ b/HybridWebView/Platforms/iOS/HybridWebViewHandler.iOS.cs
@@ -1,5 +1,7 @@
using Foundation;
+using Intents;
using Microsoft.Maui.Platform;
+using System;
using System.Drawing;
using System.Globalization;
using System.Reflection.Metadata;
@@ -79,8 +81,26 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
{
dic.Add((NSString)"Content-Length", (NSString)(responseData.ResponseBytes.Length.ToString(CultureInfo.InvariantCulture)));
dic.Add((NSString)"Content-Type", (NSString)responseData.ContentType);
- // Disable local caching. This will prevent user scripts from executing correctly.
- dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
+
+ if (responseData.CustomHeaders != null)
+ {
+ foreach (var header in responseData.CustomHeaders)
+ {
+ // Add custom headers to the response. Skip the Content-Length and Content-Type headers.
+ if (header.Key != "Content-Length" && header.Key != "Content-Type")
+ {
+ dic.Add((NSString)header.Key, (NSString)header.Value);
+ }
+ }
+ }
+
+ //Ensure that the Cache-Control header is not set in the custom headers.
+ if (responseData.CustomHeaders == null || responseData.CustomHeaders.ContainsKey("Cache-Control"))
+ {
+ // Disable local caching. This will prevent user scripts from executing correctly.
+ dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
+ }
+
if (urlSchemeTask.Request.Url != null)
{
using var response = new NSHttpUrlResponse(urlSchemeTask.Request.Url, responseData.StatusCode, "HTTP/1.1", dic);
@@ -93,55 +113,37 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
}
}
- private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode)> GetResponseBytes(string? url)
+ private async Task<(byte[] ResponseBytes, string ContentType, int StatusCode, IDictionary? CustomHeaders)> GetResponseBytes(string? url)
{
- string contentType;
+ //string contentType;
- string fullUrl = url;
+ string? originalUrl = url;
url = QueryStringHelper.RemovePossibleQueryString(url);
- if (new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
+ if (!string.IsNullOrEmpty(originalUrl) && new Uri(url) is Uri uri && HybridWebView.AppOriginUri.IsBaseOf(uri))
{
- var relativePath = HybridWebView.AppOriginUri.MakeRelativeUri(uri).ToString().Replace('\\', '/');
-
var hwv = (HybridWebView)_webViewHandler.VirtualView;
-
- var bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot!);
-
- if (string.IsNullOrEmpty(relativePath))
- {
- relativePath = hwv.MainFile!.Replace('\\', '/');
- contentType = "text/html";
- }
- else
- {
- var requestExtension = Path.GetExtension(relativePath);
- contentType = requestExtension switch
- {
- ".htm" or ".html" => "text/html",
- ".js" => "application/javascript",
- ".css" => "text/css",
- _ => "text/plain",
- };
- }
+ PathUtils.GetRelativePathAndContentType(HybridWebView.AppOriginUri, uri, originalUrl, hwv.MainFile, out string relativePath, out string contentType, out string fullUrl);
Stream? contentStream = null;
+ IDictionary? customHeaders = null;
// Check to see if the request is a proxy request.
if (relativePath == HybridWebView.ProxyRequestPath)
{
- var args = new HybridWebViewProxyEventArgs(fullUrl);
+ var args = new HybridWebViewProxyEventArgs(fullUrl, contentType);
await hwv.OnProxyRequestMessage(args);
if (args.ResponseStream != null)
{
- contentType = args.ResponseContentType ?? "text/plain";
+ contentType = args.ResponseContentType ?? PathUtils.PlanTextMimeType;
contentStream = args.ResponseStream;
+ customHeaders = args.CustomResponseHeaders;
}
}
- if (contentStream == null)
+ if (contentStream is null)
{
contentStream = KnownStaticFileProvider.GetKnownResourceStream(relativePath!);
}
@@ -150,18 +152,19 @@ public async void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSche
{
using var ms = new MemoryStream();
contentStream.CopyTo(ms);
- return (ms.ToArray(), contentType, StatusCode: 200);
+ return (ms.ToArray(), contentType, StatusCode: 200, CustomHeaders: customHeaders);
}
- var assetPath = Path.Combine(bundleRootDir, relativePath);
+ string bundleRootDir = Path.Combine(NSBundle.MainBundle.ResourcePath, hwv.HybridAssetRoot ?? "");
+ string assetPath = Path.Combine(bundleRootDir, relativePath);
if (File.Exists(assetPath))
{
- return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200);
+ return (File.ReadAllBytes(assetPath), contentType, StatusCode: 200, CustomHeaders: null);
}
}
- return (Array.Empty(), ContentType: string.Empty, StatusCode: 404);
+ return (Array.Empty(), ContentType: string.Empty, StatusCode: 404, CustomHeaders: null);
}
[Export("webView:stopURLSchemeTask:")]
diff --git a/HybridWebView/QueryStringHelper.cs b/HybridWebView/QueryStringHelper.cs
index 007cc12..3780c53 100644
--- a/HybridWebView/QueryStringHelper.cs
+++ b/HybridWebView/QueryStringHelper.cs
@@ -27,11 +27,10 @@ public static Dictionary GetKeyValuePairs(string? url)
var result = new Dictionary();
if (!string.IsNullOrEmpty(url))
{
- var query = new Uri(url).Query;
- if (query != null && query.Length > 1)
+ string query = url.Substring(url.IndexOf("?") + 1);
+ if (!string.IsNullOrWhiteSpace(query))
{
result = query
- .Substring(1)
.Split('&')
.Select(p => p.Split('='))
.ToDictionary(p => p[0], p => Uri.UnescapeDataString(p[1]));
diff --git a/MauiCSharpInteropWebView.sln b/MauiCSharpInteropWebView.sln
index ce38ce3..48e39a6 100644
--- a/MauiCSharpInteropWebView.sln
+++ b/MauiCSharpInteropWebView.sln
@@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleWrappedHybridWebViewLibrary", "ExampleWrappedHybridWebViewLibrary\ExampleWrappedHybridWebViewLibrary.csproj", "{7C75B779-B7C2-4321-AE13-24470017AFC2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -38,6 +40,10 @@ Global
{9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.Build.0 = Release|Any CPU
{9B164FED-BBE0-4E85-957D-7DEC74C6C792}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {7C75B779-B7C2-4321-AE13-24470017AFC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C75B779-B7C2-4321-AE13-24470017AFC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C75B779-B7C2-4321-AE13-24470017AFC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C75B779-B7C2-4321-AE13-24470017AFC2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MauiCSharpInteropWebView/AppShell.xaml b/MauiCSharpInteropWebView/AppShell.xaml
index 212a40d..1a25a9a 100644
--- a/MauiCSharpInteropWebView/AppShell.xaml
+++ b/MauiCSharpInteropWebView/AppShell.xaml
@@ -4,11 +4,11 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiCSharpInteropWebView"
- Shell.FlyoutBehavior="Disabled">
-
-
+ Shell.FlyoutBehavior="Flyout">
+
+
+
+
+
diff --git a/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml
new file mode 100644
index 0000000..6d70ff5
--- /dev/null
+++ b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs
new file mode 100644
index 0000000..507f7a0
--- /dev/null
+++ b/MauiCSharpInteropWebView/EmbeddedResourceSample.xaml.cs
@@ -0,0 +1,13 @@
+using HybridWebView;
+using System.Globalization;
+using System.IO.Compression;
+
+namespace MauiCSharpInteropWebView;
+
+public partial class EmbeddedResourceSample : ContentPage
+{
+ public EmbeddedResourceSample()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml
new file mode 100644
index 0000000..80e1d45
--- /dev/null
+++ b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs
new file mode 100644
index 0000000..36bd411
--- /dev/null
+++ b/MauiCSharpInteropWebView/LoadFromFileSystemSample.xaml.cs
@@ -0,0 +1,118 @@
+using Microsoft.Maui.Storage;
+using SQLite;
+
+namespace MauiCSharpInteropWebView;
+
+public partial class LoadFromFileSystemSample : ContentPage
+{
+ public LoadFromFileSystemSample()
+ {
+ //For this sample we need a HTML file in the file system.
+ //We will copy the file from the app package Raw folder to the app data storage. If it isn't already there.
+
+ LoadFileIntoFileSystem("SampleFileSystemPage.html");
+ LoadFileIntoFileSystem("SampleFileSystemImage.png");
+
+ InitializeComponent();
+
+ //Set the target for JavaScript interop.
+ myHybridWebView.JSInvokeTarget = new MyJSInvokeTarget();
+
+ //Monitor proxy requests.
+ myHybridWebView.ProxyRequestReceived += WebView_ProxyRequestReceived;
+ }
+
+ #region Private Methods
+
+ private async void LoadFileIntoFileSystem(string assetPath)
+ {
+ //Get local file path to app data storage.
+ var localPath = Path.Combine(FileSystem.AppDataDirectory, Path.GetFileName(assetPath));
+
+ //Check to see if the file exists in the app data storage already.
+ if (!File.Exists(localPath))
+ {
+ //If it doesn't, assume it is an asset and copy the file to local app data storage access Raw folder.
+ using (var asset = await FileSystem.OpenAppPackageFileAsync(assetPath))
+ {
+ using (var file = File.Create(localPath))
+ {
+ asset.CopyTo(file);
+ }
+ }
+ }
+ }
+
+ private async Task WebView_ProxyRequestReceived(HybridWebView.HybridWebViewProxyEventArgs args)
+ {
+ // Check to see if our custom parameter is present.
+ if (args.QueryParams.ContainsKey("operation"))
+ {
+ switch (args.QueryParams["operation"])
+ {
+ case "loadFromFileSystem":
+ if (args.QueryParams.TryGetValue("fileName", out string fileName) && !string.IsNullOrWhiteSpace(fileName))
+ {
+ var filePath = System.IO.Path.Combine(FileSystem.Current.AppDataDirectory, Path.GetFileName(fileName));
+
+ //Check to see if the file exists.
+ if (File.Exists(filePath))
+ {
+ try
+ {
+ using (var fs = System.IO.File.OpenRead(filePath))
+ {
+ var ms = new MemoryStream();
+ await fs.CopyToAsync(ms);
+ ms.Position = 0;
+
+ args.ResponseStream = ms;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.Write(ex.Message);
+ }
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ private sealed class MyJSInvokeTarget
+ {
+ public MyJSInvokeTarget()
+ {
+ }
+
+ ///
+ /// An example of a round trip method that takes an input parameter and returns a simple value type (number).
+ ///
+ ///
+ ///
+ public double Fibonacci(int n)
+ {
+ if (n == 0) return 0;
+
+ int prev = 0;
+ int next = 1;
+ for (int i = 1; i < n; i++)
+ {
+ int sum = prev + next;
+ prev = next;
+ next = sum;
+ }
+ return next;
+ }
+ }
+
+ #endregion
+
+ private void OnHybridWebViewRawMessageReceived(object sender, HybridWebView.HybridWebViewRawMessageReceivedEventArgs e)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/MauiCSharpInteropWebView/MainPage.xaml.cs b/MauiCSharpInteropWebView/MainPage.xaml.cs
index 9a528dd..be65e34 100644
--- a/MauiCSharpInteropWebView/MainPage.xaml.cs
+++ b/MauiCSharpInteropWebView/MainPage.xaml.cs
@@ -1,4 +1,5 @@
using HybridWebView;
+using System.Diagnostics;
using System.Globalization;
using System.IO.Compression;
using System.Text;
@@ -83,6 +84,36 @@ private async Task MyHybridWebView_OnProxyRequestReceived(HybridWebView.HybridWe
{
switch (args.QueryParams["operation"])
{
+ case "fetchWithCors":
+ if(args.QueryParams.TryGetValue("url", out string urlParam) && !string.IsNullOrWhiteSpace(urlParam))
+ {
+ // Fetch a URL with CORS enabled.
+#if ANDROID
+ var client = new HttpClient(new Xamarin.Android.Net.AndroidMessageHandler());
+#else
+ var client = new HttpClient();
+#endif
+ //Enable Cors
+ client.DefaultRequestHeaders.Add("Access-Control-Allow-Origin", "*");
+
+ try
+ {
+ var response = await client.GetAsync(urlParam);
+ if (response.IsSuccessStatusCode)
+ {
+ args.ResponseStream = await response.Content.ReadAsStreamAsync();
+ if (response.Content?.Headers?.ContentType != null)
+ {
+ args.ResponseContentType = response.Content.Headers.ContentType.MediaType;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine("Error proxying request: " + ex.Message);
+ }
+ }
+ break;
case "loadImageFromZip":
// Ensure the file name parameter is present.
if (args.QueryParams.TryGetValue("fileName", out string fileName) && fileName != null)
diff --git a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj
index 36cb1ec..6ed7b3d 100644
--- a/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj
+++ b/MauiCSharpInteropWebView/MauiCSharpInteropWebView.csproj
@@ -48,6 +48,11 @@
+
+
+
+
+
@@ -57,7 +62,23 @@
+
+
+
+ LoadFromFileSystemSample.xaml
+
+
+
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
diff --git a/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png b/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png
new file mode 100644
index 0000000..dcfdf0b
Binary files /dev/null and b/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemImage.png differ
diff --git a/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemPage.html b/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemPage.html
new file mode 100644
index 0000000..6203f9b
--- /dev/null
+++ b/MauiCSharpInteropWebView/Resources/Raw/SampleFileSystemPage.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
HybridWebView demo: Local File system page
+
+ This web page is loaded from the local file system (FileSystem.Current.AppDataDirectory).
+ It can access resources from in the same way as normal via the HybridAssetRoot and other embedded resources.
+ Native/JS communication works as expected. The proxy can be further leveraged to reference
+ local file system files within the web pages (e.g. images). This is useful when you have resources that have
+ been downloaded and saved to the file system (app data directory).
+
+
+
+
+
+
+
+
+ The following image is via the local file system use the proxy:
+
+
+
+
+
+
+
+
+
diff --git a/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html b/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html
index d663807..076da0b 100644
--- a/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html
+++ b/MauiCSharpInteropWebView/Resources/Raw/hybrid_root/proxy.html
@@ -25,6 +25,34 @@
document.getElementById('proxyImage').src = `${window.location.origin}/proxy?operation=loadImageFromWeb&tileId=${randomInt}`;
}
+ //GitHub does not enable CORS on raw files. This will fail normally in a browser.
+ var noCorsEnabledFile = 'https://github.com/Eilon/MauiHybridWebView/blob/main/HybridWebView/HybridWebView.cs';
+
+ function fetchNoCors() {
+ fetchUrl(noCorsEnabledFile);
+ }
+
+ function fetchWithCorsEnabledProxy() {
+ fetchUrl(`${window.location.origin}/proxy?operation=fetchWithCors&url=` + encodeURIComponent(noCorsEnabledFile));
+ }
+
+ function fetchUrl(url) {
+ var outputElm = document.getElementById('fetchResponseTextArea');
+ outputElm.value = 'Fetching...';
+
+ fetch(url).then(response => {
+ if (!response.ok) {
+ outputElm.value = 'Error: Network response was not ok';
+ }
+
+ return response.text();
+ }).then(data => {
+ outputElm.value = data;
+ }).catch(error => {
+ outputElm.value = 'There was a problem with the fetch operation: ' + error;
+ });
+ }
+
//Load the map.
window.onload = function () {
var map = new maplibregl.Map({
@@ -76,12 +104,19 @@
HybridWebView demo: Proxy
-
The image below uses an img tag with a proxy URL in it's HTML like <img src="/proxy?operation=loadImageFromZip&fileName=happy.jpeg" />
-
+
+
+ Leverage the proxy to access resources that are on non-CORS enabled domains.
+