Skip to content

Commit

Permalink
.NET MAUI BlazorWebView control for Android, iOS/MacCatalyst, and Win…
Browse files Browse the repository at this point in the history
…UI (#654)

* BlazorWebView control for .NET MAUI:
- Supports Android, iOS, and WinUI
- Samples updates to show simple Blazor demo

* Revert more sample changes for cleanup

* Update samples

* Shorten folder names

* Code cleanup

* Sample cleanup

* Update non-net6 project/SLN

* Revert "Update non-net6 project/SLN"

This reverts commit cc6be87.

* Skip Blazor for non-.NET6 samples

* Update BlazorPage.cs

* Rebase on 'main' and update sample to match

* Added BlazorWebView project to the winui sln

* Remove scrollview for now

Co-authored-by: Jonathan Dick <[email protected]>
  • Loading branch information
Eilon and Redth authored Apr 15, 2021
1 parent 45dd0ed commit 9c1bdcb
Show file tree
Hide file tree
Showing 36 changed files with 1,677 additions and 12 deletions.
9 changes: 9 additions & 0 deletions Microsoft.Maui-net6.sln
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{50C758FE-4E1
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Templates", "Templates", "{72397ADB-40A8-4B8E-8E08-2DBE2803C845}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BlazorWebView", "BlazorWebView", "{1614D1A4-5C3D-4D5B-8C89-426E37A564EF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core", "src\BlazorWebView\src\core\Microsoft.AspNetCore.Components.WebView.Maui.csproj", "{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -141,6 +145,10 @@ Global
{92644F6F-5946-48FC-A21A-A3D6EE24E8B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{92644F6F-5946-48FC-A21A-A3D6EE24E8B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92644F6F-5946-48FC-A21A-A3D6EE24E8B3}.Release|Any CPU.Build.0 = Release|Any CPU
{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -166,6 +174,7 @@ Global
{E8AD265B-3C67-4640-AC58-A522F9FB3361} = {09C264E9-E3F3-4586-9151-DCBB1F6DA7AB}
{C564DDD6-DE79-45CD-88EA-3F690481572A} = {09C264E9-E3F3-4586-9151-DCBB1F6DA7AB}
{50C758FE-4E10-409A-94F5-A75480960864} = {459BF674-83CB-46F6-881F-A2D2117DBF4D}
{F7F2B379-52CE-4802-9EC9-0D7967B6BFB7} = {1614D1A4-5C3D-4D5B-8C89-426E37A564EF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50}
Expand Down
21 changes: 21 additions & 0 deletions Microsoft.Maui.WinUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtils", "src\TestUtils\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.UnitTests", "src\Core\tests\UnitTests\Core.UnitTests.csproj", "{CB3EEB1E-311B-4BF6-9E1A-9CEF74200448}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BlazorWebView", "BlazorWebView", "{17CD1D1D-B4EA-4E7E-ABEF-945AC320936B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.WebView.Maui", "src\BlazorWebView\src\core\Microsoft.AspNetCore.Components.WebView.Maui.csproj", "{BEC57018-9B33-417B-A3A8-F8F154218C61}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -242,6 +246,22 @@ Global
{CB3EEB1E-311B-4BF6-9E1A-9CEF74200448}.Release|x64.Build.0 = Release|Any CPU
{CB3EEB1E-311B-4BF6-9E1A-9CEF74200448}.Release|x86.ActiveCfg = Release|Any CPU
{CB3EEB1E-311B-4BF6-9E1A-9CEF74200448}.Release|x86.Build.0 = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|arm64.ActiveCfg = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|arm64.Build.0 = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|x64.ActiveCfg = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|x64.Build.0 = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|x86.ActiveCfg = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Debug|x86.Build.0 = Debug|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|Any CPU.Build.0 = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|arm64.ActiveCfg = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|arm64.Build.0 = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|x64.ActiveCfg = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|x64.Build.0 = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|x86.ActiveCfg = Release|Any CPU
{BEC57018-9B33-417B-A3A8-F8F154218C61}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -261,6 +281,7 @@ Global
{5774C01E-9D4D-47C0-BECD-1564EDE70926} = {09C264E9-E3F3-4586-9151-DCBB1F6DA7AB}
{CAD6061F-A1B1-41B3-B906-6C6C0BD850EE} = {44510C11-7CBF-4FE4-9F23-AE1FEE743522}
{CB3EEB1E-311B-4BF6-9E1A-9CEF74200448} = {5774C01E-9D4D-47C0-BECD-1564EDE70926}
{BEC57018-9B33-417B-A3A8-F8F154218C61} = {17CD1D1D-B4EA-4E7E-ABEF-945AC320936B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50}
Expand Down
2 changes: 1 addition & 1 deletion eng/Microsoft.Extensions.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup Condition="'$(TargetFramework.Contains(net6.0))' == 'true'">
<_MicrosoftHostingVersion>6.0.0-preview.2.21110.7</_MicrosoftHostingVersion>
<_MicrosoftHostingVersion>6.0.0-preview.3.21174.7</_MicrosoftHostingVersion>
<_MicrosoftDependencyInjectionVersion>$(_MicrosoftHostingVersion)</_MicrosoftDependencyInjectionVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework.Contains(net6.0))' != 'true'">
Expand Down
91 changes: 91 additions & 0 deletions src/BlazorWebView/src/core/Android/AndroidWebKitWebViewManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Android.Webkit;
using AWebView = Android.Webkit.WebView;
using Microsoft.Extensions.FileProviders;
using System;
using System.IO;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
/// <summary>
/// An implementation of <see cref="WebViewManager"/> that uses the Android WebKit WebView browser control
/// to render web content.
/// </summary>
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 readonly AWebView _webview;

/// <summary>
/// Constructs an instance of <see cref="AndroidWebKitWebViewManager"/>.
/// </summary>
/// <param name="webview">A wrapper to access platform-specific WebView APIs.</param>
/// <param name="services">A service provider containing services to be used by this class and also by application code.</param>
/// <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, string hostPageRelativePath)
: base(services, dispatcher, new Uri(AppOrigin), fileProvider, hostPageRelativePath)
{
_blazorWebViewHandler = blazorMauiWebViewHandler ?? throw new ArgumentNullException(nameof(blazorMauiWebViewHandler));
_webview = webview ?? throw new ArgumentNullException(nameof(webview));
}

/// <inheritdoc />
protected override void NavigateCore(Uri absoluteUri)
{
_webview.LoadUrl(absoluteUri.AbsoluteUri);
}

/// <inheritdoc />
protected override void SendMessage(string message)
{
_webview.PostWebMessage(new WebMessage(message), AndroidAppOriginUri);
}

internal bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out string headers) =>
TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers);
//internal bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers) =>
// TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers);

internal void SetUpMessageChannel()
{
var nativeToJsPorts = _webview.CreateWebMessageChannel();

var nativeToJs = new BlazorWebMessageCallback(message =>
{
MessageReceived(new Uri(AppOrigin), message!);
});

var destPort = new[] { nativeToJsPorts[1] };

nativeToJsPorts[0].SetWebMessageCallback(nativeToJs);

_webview.PostWebMessage(new WebMessage("capturePort", destPort), AndroidAppOriginUri);
}

private class BlazorWebMessageCallback : WebMessagePort.WebMessageCallback
{
private readonly Action<string?> _onMessageReceived;

public BlazorWebMessageCallback(Action<string?> onMessageReceived)
{
_onMessageReceived = onMessageReceived ?? throw new ArgumentNullException(nameof(onMessageReceived));
}

public override void OnMessage(WebMessagePort? port, WebMessage? message)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}

_onMessageReceived(message.Data);
}
}
}
}
121 changes: 121 additions & 0 deletions src/BlazorWebView/src/core/Android/BlazorWebViewHandler.Android.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Android.Webkit;
using static Android.Views.ViewGroup;
using AWebView = Android.Webkit.WebView;
using Microsoft.Extensions.FileProviders;
using Microsoft.Maui.Handlers;
using System;
using System.Collections.ObjectModel;
using Path = System.IO.Path;

namespace Microsoft.AspNetCore.Components.WebView.Maui
{
public partial class BlazorWebViewHandler : ViewHandler<IBlazorWebView, AWebView>
{
private WebViewClient? _webViewClient;
private WebChromeClient? _webChromeClient;
private AndroidWebKitWebViewManager? _webviewManager;
internal AndroidWebKitWebViewManager? WebviewManager => _webviewManager;

protected override AWebView CreateNativeView()
{
var aWebView = new AWebView(Context!)
{
#pragma warning disable 618 // This can probably be replaced with LinearLayout(LayoutParams.MatchParent, LayoutParams.MatchParent); just need to test that theory
LayoutParameters = new Android.Widget.AbsoluteLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent, 0, 0)
#pragma warning restore 618
};

if (aWebView.Settings != null)
{
aWebView.Settings.JavaScriptEnabled = true;
aWebView.Settings.DomStorageEnabled = true;
}

_webViewClient = GetWebViewClient();
aWebView.SetWebViewClient(_webViewClient);

_webChromeClient = GetWebChromeClient();
aWebView.SetWebChromeClient(_webChromeClient);

return aWebView;
}

protected override void DisconnectHandler(AWebView nativeView)
{
nativeView.StopLoading();

_webViewClient?.Dispose();
_webChromeClient?.Dispose();
}

private bool RequiredStartupPropertiesSet =>
//_webview != null &&
HostPage != null &&
Services != null;

private string? HostPage { get; set; }
private ObservableCollection<RootComponent>? RootComponents { get; set; }
private new IServiceProvider? Services { get; set; }

private void StartWebViewCoreIfPossible()
{
if (!RequiredStartupPropertiesSet ||
false)//_webviewManager != null)
{
return;
}
if (NativeView == null)
{
throw new InvalidOperationException($"Can't start {nameof(BlazorWebView)} without native web view instance.");
}

var resourceAssembly = RootComponents?[0]?.ComponentType?.Assembly;
if (resourceAssembly == null)
{
throw new InvalidOperationException($"Can't start {nameof(BlazorWebView)} without a component type assembly.");
}

// We assume the host page is always in the root of the content directory, because it's
// unclear there's any other use case. We can add more options later if so.
var contentRootDir = Path.GetDirectoryName(HostPage) ?? string.Empty;
var hostPageRelativePath = Path.GetRelativePath(contentRootDir, HostPage!);
var fileProvider = new ManifestEmbeddedFileProvider(resourceAssembly, root: contentRootDir);

_webviewManager = new AndroidWebKitWebViewManager(this, NativeView, Services!, MauiDispatcher.Instance, fileProvider, hostPageRelativePath);
if (RootComponents != null)
{
foreach (var rootComponent in RootComponents)
{
// Since the page isn't loaded yet, this will always complete synchronously
_ = rootComponent.AddToWebViewManagerAsync(_webviewManager);
}
}

_webviewManager.Navigate("/");
}

protected virtual WebViewClient GetWebViewClient() =>
new WebKitWebViewClient(this);

protected virtual WebChromeClient GetWebChromeClient() =>
new WebChromeClient();

public static void MapHostPage(BlazorWebViewHandler handler, IBlazorWebView webView)
{
handler.HostPage = webView.HostPage;
handler.StartWebViewCoreIfPossible();
}

public static void MapRootComponents(BlazorWebViewHandler handler, IBlazorWebView webView)
{
handler.RootComponents = webView.RootComponents;
handler.StartWebViewCoreIfPossible();
}

public static void MapServices(BlazorWebViewHandler handler, IBlazorWebView webView)
{
handler.Services = webView.Services;
handler.StartWebViewCoreIfPossible();
}
}
}
Loading

0 comments on commit 9c1bdcb

Please sign in to comment.