Skip to content

Commit

Permalink
Blazor Open Links in Browser with Configurability (#4645)
Browse files Browse the repository at this point in the history
* Blazor Android Open Links in Browser with Configurability

* Blazor Windows Open Links in Browser with Configurability (#4680)

* Blazor Windows Open Links in Browser with Configurability

Windows portion of #4338

* TryCreate URI

* PR Feedback

(cherry picked from commit 35f637e)

* OnExternalNavigationStarting

* Pranav Points

* Event based approach

* Info -> EventArgs

* iOS & Mac Catalyst

* Fix WPF/Winforms Browser Start

* Winforms ExternalNavigationStarting

* PR Feedback

* Remove ordering dependency during property mapping

* @blowdart feedback

* @Eilon feedback
  • Loading branch information
TanayParikh authored Feb 24, 2022
1 parent 451b484 commit 78c1ffe
Show file tree
Hide file tree
Showing 17 changed files with 341 additions and 56 deletions.
2 changes: 2 additions & 0 deletions Microsoft.Maui.sln
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MauiBlazorWebView.DeviceTes
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SharedSource", "SharedSource", "{4F2926C8-43AB-4328-A735-D9EAD699F81D}"
ProjectSection(SolutionItems) = preProject
src\BlazorWebView\src\SharedSource\ExternalLinkNavigationEventArgs.cs = src\BlazorWebView\src\SharedSource\ExternalLinkNavigationEventArgs.cs
src\BlazorWebView\src\SharedSource\ExternalLinkNavigationPolicy.cs = src\BlazorWebView\src\SharedSource\ExternalLinkNavigationPolicy.cs
src\BlazorWebView\src\SharedSource\QueryStringHelper.cs = src\BlazorWebView\src\SharedSource\QueryStringHelper.cs
src\BlazorWebView\src\SharedSource\WebView2WebViewManager.cs = src\BlazorWebView\src\SharedSource\WebView2WebViewManager.cs
EndProjectSection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.FileProviders;
using AWebView = Android.Webkit.WebView;
using AUri = Android.Net.Uri;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
Expand All @@ -17,9 +18,8 @@ public class AndroidWebKitWebViewManager : WebViewManager
// Using an IP address means that WebView doesn't wait for any DNS resolution,
// making it substantially faster. Note that this isn't real HTTP traffic, since
// we intercept all the requests within this origin.
private const string AppOrigin = "https://0.0.0.0/";
private static readonly Android.Net.Uri AndroidAppOriginUri = Android.Net.Uri.Parse(AppOrigin)!;
private readonly BlazorWebViewHandler _blazorWebViewHandler;
private static readonly string AppOrigin = $"https://{BlazorWebView.AppHostAddress}/";
private static readonly AUri AndroidAppOriginUri = AUri.Parse(AppOrigin)!;
private readonly AWebView _webview;

/// <summary>
Expand All @@ -30,11 +30,10 @@ public class AndroidWebKitWebViewManager : WebViewManager
/// <param name="dispatcher">A <see cref="Dispatcher"/> instance that can marshal calls to the required thread or sync context.</param>
/// <param name="fileProvider">Provides static content to the webview.</param>
/// <param name="hostPageRelativePath">Path to the host page within the <paramref name="fileProvider"/>.</param>
public AndroidWebKitWebViewManager(BlazorWebViewHandler blazorMauiWebViewHandler, AWebView webview, IServiceProvider services, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath)
public AndroidWebKitWebViewManager(AWebView webview!!, IServiceProvider services, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath)
: base(services, dispatcher, new Uri(AppOrigin), fileProvider, jsComponents, hostPageRelativePath)
{
_blazorWebViewHandler = blazorMauiWebViewHandler ?? throw new ArgumentNullException(nameof(blazorMauiWebViewHandler));
_webview = webview ?? throw new ArgumentNullException(nameof(webview));
_webview = webview;
}

/// <inheritdoc />
Expand Down
26 changes: 26 additions & 0 deletions src/BlazorWebView/src/Maui/Android/BlazorWebChromeClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

using Android.Content;
using Android.Net;
using Android.OS;
using Android.Webkit;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
class BlazorWebChromeClient : WebChromeClient
{
public override bool OnCreateWindow(Android.Webkit.WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg)
{
if (view?.Context is not null)
{
// Intercept _blank target <a> tags to always open in device browser
// regardless of ExternalLinkMode.OpenInWebview
var requestUrl = view.GetHitTestResult().Extra;
var intent = new Intent(Intent.ActionView, Uri.Parse(requestUrl));
view.Context.StartActivity(intent);
}

// We don't actually want to create a new WebView window so we just return false
return false;
}
}
}
16 changes: 6 additions & 10 deletions src/BlazorWebView/src/Maui/Android/BlazorWebViewHandler.Android.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Android.Webkit;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Microsoft.Maui;
using Microsoft.Maui.Handlers;
using static Android.Views.ViewGroup;
using Path = System.IO.Path;
Expand All @@ -31,6 +24,9 @@ protected override BlazorAndroidWebView CreatePlatformView()
#pragma warning restore 618
};

// To allow overriding ExternalLinkMode.InsecureOpenInWebView and open links in browser with a _blank target
blazorAndroidWebView.Settings.SetSupportMultipleWindows(true);

BlazorAndroidWebView.SetWebContentsDebuggingEnabled(enabled: true);

if (blazorAndroidWebView.Settings != null)
Expand Down Expand Up @@ -93,7 +89,7 @@ private void StartWebViewCoreIfPossible()

var fileProvider = VirtualView.CreateFileProvider(contentRootDir);

_webviewManager = new AndroidWebKitWebViewManager(this, PlatformView, Services!, ComponentsDispatcher, fileProvider, VirtualView.JSComponents, hostPageRelativePath);
_webviewManager = new AndroidWebKitWebViewManager(PlatformView, Services!, ComponentsDispatcher, fileProvider, VirtualView.JSComponents, hostPageRelativePath);

if (RootComponents != null)
{
Expand All @@ -116,6 +112,6 @@ protected virtual WebViewClient GetWebViewClient() =>
new WebKitWebViewClient(this);

protected virtual WebChromeClient GetWebChromeClient() =>
new WebChromeClient();
new BlazorWebChromeClient();
}
}
49 changes: 36 additions & 13 deletions src/BlazorWebView/src/Maui/Android/WebKitWebViewClient.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,67 @@
using System;
using Android.Content;
using Android.Runtime;
using Android.Webkit;
using AWebView = Android.Webkit.WebView;
using AUri = Android.Net.Uri;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
internal class WebKitWebViewClient : WebViewClient
{
private const string AppOrigin = "https://0.0.0.0/";
// Using an IP address means that WebView doesn't wait for any DNS resolution,
// making it substantially faster. Note that this isn't real HTTP traffic, since
// we intercept all the requests within this origin.
private static readonly string AppOrigin = $"https://{BlazorWebView.AppHostAddress}/";

private readonly BlazorWebViewHandler? _webViewHandler;

public WebKitWebViewClient(BlazorWebViewHandler webViewHandler)
public WebKitWebViewClient(BlazorWebViewHandler webViewHandler!!)
{
_webViewHandler = webViewHandler ?? throw new ArgumentNullException(nameof(webViewHandler));
_webViewHandler = webViewHandler;
}

protected WebKitWebViewClient(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
// This constructor is called whenever the .NET proxy was disposed, and it was recreated by Java. It also
// happens when overridden methods are called between execution of this constructor and the one above.
// because of these facts, we have to check
// all methods below for null field references and properties.
// because of these facts, we have to check all methods below for null field references and properties.
}

public override bool ShouldOverrideUrlLoading(AWebView? view, IWebResourceRequest? request)
{
// handle redirects to the app custom scheme by reloading the url in the view.
// otherwise they will be blocked by Android.
// Handle redirects to the app custom scheme by reloading the URL in the view.
// Handle navigation to external URLs using the system browser, unless overriden.
var requestUri = request?.Url?.ToString();
if (requestUri != null && view != null &&
request != null && request.IsRedirect && request.IsForMainFrame)
if (Uri.TryCreate(requestUri, UriKind.RelativeOrAbsolute, out var uri))
{
var uri = new Uri(requestUri);
if (uri.Host == "0.0.0.0")
if (uri.Host == BlazorWebView.AppHostAddress &&
view is not null &&
request is not null &&
request.IsRedirect &&
request.IsForMainFrame)
{
view.LoadUrl(uri.ToString());
return true;
}
else if (uri.Host != BlazorWebView.AppHostAddress && _webViewHandler != null)
{
var callbackArgs = new ExternalLinkNavigationEventArgs(uri);
_webViewHandler.ExternalNavigationStarting?.Invoke(callbackArgs);

if (callbackArgs.ExternalLinkNavigationPolicy == ExternalLinkNavigationPolicy.OpenInExternalBrowser)
{
var intent = new Intent(Intent.ActionView, AUri.Parse(requestUri));
_webViewHandler.Context.StartActivity(intent);
}

if (callbackArgs.ExternalLinkNavigationPolicy != ExternalLinkNavigationPolicy.InsecureOpenInWebView)
{
return true;
}
}
}

return base.ShouldOverrideUrlLoading(view, request);
}

Expand Down Expand Up @@ -167,9 +190,9 @@ private class JavaScriptValueCallback : Java.Lang.Object, IValueCallback
{
private readonly Action _callback;

public JavaScriptValueCallback(Action callback)
public JavaScriptValueCallback(Action callback!!)
{
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
_callback = callback;
}

public void OnReceiveValue(Java.Lang.Object? value)
Expand Down
10 changes: 10 additions & 0 deletions src/BlazorWebView/src/Maui/BlazorWebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
{
public class BlazorWebView : Microsoft.Maui.Controls.View, IBlazorWebView
{
internal const string AppHostAddress = "0.0.0.0";

private readonly JSComponentConfigurationStore _jSComponents = new();

public BlazorWebView()
Expand All @@ -19,11 +21,19 @@ public BlazorWebView()

public RootComponentsCollection RootComponents { get; }

/// <inheritdoc/>
public event EventHandler<ExternalLinkNavigationEventArgs>? ExternalNavigationStarting;

/// <inheritdoc/>
public virtual IFileProvider CreateFileProvider(string contentRootDir)
{
// Call into the platform-specific code to get that platform's asset file provider
return ((BlazorWebViewHandler)(Handler!)).CreateFileProvider(contentRootDir);
}

internal void NotifyExternalNavigationStarting(ExternalLinkNavigationEventArgs args)
{
ExternalNavigationStarting?.Invoke(this, args);
}
}
}
18 changes: 16 additions & 2 deletions src/BlazorWebView/src/Maui/BlazorWebViewHandler.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using System.Linq;
using System;
using System.Linq;
using Microsoft.AspNetCore.Components.WebView;
using Microsoft.Maui;
using Microsoft.Maui.Handlers;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
public partial class BlazorWebViewHandler
{
public static PropertyMapper<IBlazorWebView, BlazorWebViewHandler> BlazorWebViewMapper = new(ViewHandler.ViewMapper)
public static readonly PropertyMapper<IBlazorWebView, BlazorWebViewHandler> BlazorWebViewMapper = new(ViewMapper)
{
[nameof(IBlazorWebView.HostPage)] = MapHostPage,
[nameof(IBlazorWebView.RootComponents)] = MapRootComponents,
[nameof(IBlazorWebView.ExternalNavigationStarting)] = MapNotifyExternalNavigationStarting,
};

public BlazorWebViewHandler() : base(BlazorWebViewMapper)
Expand Down Expand Up @@ -40,8 +43,19 @@ public static void MapRootComponents(BlazorWebViewHandler handler, IBlazorWebVie
#endif
}

public static void MapNotifyExternalNavigationStarting(BlazorWebViewHandler handler, IBlazorWebView webView)
{
#if !NETSTANDARD
if (webView is BlazorWebView bwv)
{
handler.ExternalNavigationStarting = bwv.NotifyExternalNavigationStarting;
}
#endif
}

#if !NETSTANDARD
private string? HostPage { get; set; }
internal Action<ExternalLinkNavigationEventArgs>? ExternalNavigationStarting;

private RootComponentsCollection? _rootComponents;
private RootComponentsCollection? RootComponents
Expand Down
9 changes: 8 additions & 1 deletion src/BlazorWebView/src/Maui/IBlazorWebView.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components.Web;
using System;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.FileProviders;
using Microsoft.Maui;

Expand All @@ -10,6 +11,12 @@ public interface IBlazorWebView : IView
RootComponentsCollection RootComponents { get; }
JSComponentConfigurationStore JSComponents { get; }

/// <summary>
/// Allows customizing how external links are opened.
/// Opens external links in the system browser by default.
/// </summary>
event EventHandler<ExternalLinkNavigationEventArgs>? ExternalNavigationStarting;

/// <summary>
/// Creates a file provider for static assets used in the <see cref="BlazorWebView"/>. The default implementation
/// serves files from a platform-specific location. Override this method to return a custom <see cref="IFileProvider"/> to serve assets such
Expand Down
12 changes: 10 additions & 2 deletions src/BlazorWebView/src/Maui/Windows/BlazorWebViewHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,16 @@ private void StartWebViewCoreIfPossible()
var hostPageRelativePath = Path.GetRelativePath(contentRootDir, HostPage!);

var fileProvider = VirtualView.CreateFileProvider(contentRootDir);

_webviewManager = new WinUIWebViewManager(PlatformView, Services!, ComponentsDispatcher, fileProvider, VirtualView.JSComponents, hostPageRelativePath, contentRootDir);

_webviewManager = new WinUIWebViewManager(
PlatformView,
Services!,
ComponentsDispatcher,
fileProvider,
VirtualView.JSComponents,
hostPageRelativePath,
contentRootDir,
this);

if (RootComponents != null)
{
Expand Down
12 changes: 10 additions & 2 deletions src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ public class WinUIWebViewManager : WebView2WebViewManager
private readonly string _hostPageRelativePath;
private readonly string _contentRootDir;

public WinUIWebViewManager(WebView2Control webview, IServiceProvider services, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath, string contentRootDir)
: base(webview, services, dispatcher, fileProvider, jsComponents, hostPageRelativePath)
public WinUIWebViewManager(
WebView2Control webview,
IServiceProvider services,
Dispatcher dispatcher,
IFileProvider fileProvider,
JSComponentConfigurationStore jsComponents,
string hostPageRelativePath,
string contentRootDir,
BlazorWebViewHandler webViewHandler)
: base(webview, services, dispatcher, fileProvider, jsComponents, hostPageRelativePath, webViewHandler)
{
_webview = webview;
_hostPageRelativePath = hostPageRelativePath;
Expand Down
2 changes: 1 addition & 1 deletion src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public partial class BlazorWebViewHandler : ViewHandler<IBlazorWebView, WKWebVie
{
private IOSWebViewManager? _webviewManager;

private const string AppOrigin = "app://0.0.0.0/";
internal const string AppOrigin = "app://" + BlazorWebView.AppHostAddress + "/";
private const string BlazorInitScript = @"
window.__receiveMessageCallbacks = [];
window.__dispatchMessageCallback = function(message) {
Expand Down
Loading

0 comments on commit 78c1ffe

Please sign in to comment.