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

Blazor Open Links in Browser with Configurability #4645

Merged
merged 16 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
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\ExternalLinkNavigationInfo.cs = src\BlazorWebView\src\SharedSource\ExternalLinkNavigationInfo.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;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
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.Handlers;
using static Android.Views.ViewGroup;
using Path = System.IO.Path;
Expand All @@ -31,6 +23,9 @@ protected override BlazorAndroidWebView CreatePlatformView()
#pragma warning restore 618
};

// To allow overriding ExternalLinkMode.OpenInWebView 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 +88,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 +111,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 ExternalLinkNavigationInfo(uri);
var externalLinkMode = _webViewHandler.OnExternalNavigationStarting?.Invoke(callbackArgs) ?? ExternalLinkNavigationPolicy.OpenInExternalBrowser;

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

if (externalLinkMode != ExternalLinkNavigationPolicy.OpenInWebView)
{
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
6 changes: 6 additions & 0 deletions src/BlazorWebView/src/Maui/BlazorWebView.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebView;
using Microsoft.Extensions.FileProviders;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
public class BlazorWebView : Microsoft.Maui.Controls.View, IBlazorWebView
{
internal static readonly string AppHostAddress = "0.0.0.0";

private readonly JSComponentConfigurationStore _jSComponents = new();

public BlazorWebView()
Expand All @@ -19,6 +22,9 @@ public BlazorWebView()

public RootComponentsCollection RootComponents { get; }

/// <inheritdoc/>
public Func<ExternalLinkNavigationInfo, ExternalLinkNavigationPolicy>? OnExternalNavigationStarting { get; set; }
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved

/// <inheritdoc/>
public virtual IFileProvider CreateFileProvider(string contentRootDir)
{
Expand Down
16 changes: 14 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)
private static readonly PropertyMapper<IBlazorWebView, BlazorWebViewHandler> BlazorWebViewMapper = new(ViewMapper)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change to private deliberate? The mappers are supposed to be public so that derived types can re-use them. Examples: https://github.com/dotnet/maui/blob/main/src/Core/src/Handlers/Button/ButtonHandler.cs#L19-L41

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think making it readonly is good, though interestingly in the one I linked they are not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I logged #4870 to see if they should be readonly across all of MAUI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to public static readonly.

{
[nameof(IBlazorWebView.HostPage)] = MapHostPage,
[nameof(IBlazorWebView.RootComponents)] = MapRootComponents,
[nameof(IBlazorWebView.OnExternalNavigationStarting)] = MapOnExternalNavigationStarting,
};

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

public static void MapOnExternalNavigationStarting(BlazorWebViewHandler handler, IBlazorWebView webView)
{
#if !NETSTANDARD
handler.OnExternalNavigationStarting = webView.OnExternalNavigationStarting;
handler.StartWebViewCoreIfPossible();
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
#endif
}

#if !NETSTANDARD
private string? HostPage { get; set; }
internal Func<ExternalLinkNavigationInfo, ExternalLinkNavigationPolicy>? OnExternalNavigationStarting { get; private set; }

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

Expand All @@ -10,6 +12,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>
Func<ExternalLinkNavigationInfo, ExternalLinkNavigationPolicy>? OnExternalNavigationStarting { get; set; }

/// <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
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
40 changes: 39 additions & 1 deletion src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Web.WebView2.Core;
using Windows.ApplicationModel;
using Windows.Storage.Streams;
using Launcher = Windows.System.Launcher;
using WebView2Control = Microsoft.UI.Xaml.Controls.WebView2;

namespace Microsoft.AspNetCore.Components.WebView.Maui
Expand All @@ -21,13 +22,23 @@ public class WinUIWebViewManager : WebView2WebViewManager
private readonly WebView2Control _webview;
private readonly string _hostPageRelativePath;
private readonly string _contentRootDir;
private readonly BlazorWebViewHandler _blazorWebViewHandler;

public WinUIWebViewManager(WebView2Control webview, IServiceProvider services, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath, string contentRootDir)
public WinUIWebViewManager(
WebView2Control webview,
IServiceProvider services,
Dispatcher dispatcher,
IFileProvider fileProvider,
JSComponentConfigurationStore jsComponents,
string hostPageRelativePath,
string contentRootDir,
BlazorWebViewHandler blazorWebViewHandler)
: base(webview, services, dispatcher, fileProvider, jsComponents, hostPageRelativePath)
{
_webview = webview;
_hostPageRelativePath = hostPageRelativePath;
_contentRootDir = contentRootDir;
_blazorWebViewHandler = blazorWebViewHandler;
}

protected override async Task HandleWebResourceRequest(CoreWebView2WebResourceRequestedEventArgs eventArgs)
Expand Down Expand Up @@ -92,6 +103,33 @@ protected override async Task HandleWebResourceRequest(CoreWebView2WebResourceRe
deferral.Complete();
}

protected override void CoreWebView2_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
{
if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri) && uri.Host != AppHostAddress)
{
var callbackArgs = new ExternalLinkNavigationInfo(uri);
var externalLinkMode = _blazorWebViewHandler.OnExternalNavigationStarting?.Invoke(callbackArgs) ?? ExternalLinkNavigationPolicy.OpenInExternalBrowser;

if (externalLinkMode == ExternalLinkNavigationPolicy.OpenInExternalBrowser)
{
_ = Launcher.LaunchUriAsync(uri);
}

args.Cancel = externalLinkMode != ExternalLinkNavigationPolicy.OpenInWebView;
}
}

protected override void CoreWebView2_NewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs args)
{
// Intercept _blank target <a> tags to always open in device browser.
// The ExternalLinkCallback is not invoked.
if (Uri.TryCreate(args.Uri, UriKind.RelativeOrAbsolute, out var uri))
{
_ = Launcher.LaunchUriAsync(uri);
args.Handled = true;
}
}

protected override void QueueBlazorStart()
{
// In .NET MAUI we use autostart='false' for the Blazor script reference, so we start it up manually in this event
Expand Down
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 static readonly string AppOrigin = $"app://{BlazorWebView.AppHostAddress}/";
TanayParikh marked this conversation as resolved.
Show resolved Hide resolved
private const string BlazorInitScript = @"
window.__receiveMessageCallbacks = [];
window.__dispatchMessageCallback = function(message) {
Expand Down
Loading