Skip to content

Commit

Permalink
WIP: Initial HybridWebView control
Browse files Browse the repository at this point in the history
Very basic functionality working! (Windows and Android)
  • Loading branch information
Eilon committed Jun 19, 2024
1 parent 00f6fcd commit e4ce427
Show file tree
Hide file tree
Showing 20 changed files with 1,607 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<views:BasePage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Pages.HybridWebViewPage"
xmlns:views="clr-namespace:Maui.Controls.Sample.Pages.Base"
Title="HybridWebView">
<views:BasePage.Content>
<ScrollView>
<VerticalStackLayout Padding="12">
<Label Text="HybridWebView here" x:Name="statusLabel" />
<Button Text="Send message to JS" Pressed="SendMessageButton_Pressed" />
<HybridWebView
x:Name="hwv"
HeightRequest="150"
HorizontalOptions="FillAndExpand"
HybridRoot="HybridSamplePage"
RawMessageReceived="hwv_RawMessageReceived"/>
</VerticalStackLayout>
</ScrollView>
</views:BasePage.Content>
</views:BasePage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Microsoft.Maui.Controls;

namespace Maui.Controls.Sample.Pages
{
public partial class HybridWebViewPage
{
public HybridWebViewPage()
{
InitializeComponent();
}

private void SendMessageButton_Pressed(object sender, EventArgs e)
{
hwv.SendRawMessage("Hello from C#!");
}

private void hwv_RawMessageReceived(object sender, HybridWebView.HybridWebViewRawMessageReceivedEventArgs e)
{
Dispatcher.Dispatch(() => statusLabel.Text += e.Message);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
<script>
function SendRawMessageToCSharp() {

var message = "Message from JS!";

if (window.chrome && window.chrome.webview) {
// Windows WebView2
window.chrome.webview.postMessage(message);
}
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
// iOS and MacCatalyst WKWebView
window.webkit.messageHandlers.webwindowinterop.postMessage(message);
}
else {
// Android WebView
hybridWebViewHost.sendMessage(message);
}

}

function ReceiveRawMessageFromCSharp(message) {
var messageFromCSharp = document.getElementById("messageFromCSharp");
messageFromCSharp.value += '\r\n' + message;
}

if (window.chrome && window.chrome.webview) {
// Windows WebView2
window.chrome.webview.addEventListener('message', arg => {
ReceiveRawMessageFromCSharp(arg.data);
});
}
else {
// Android WebView
window.addEventListener('message', arg => {
ReceiveRawMessageFromCSharp(arg.data);
});
}

</script>
</head>
<body>
<div>
Hybrid sample!
</div>
<div>
<button onclick="SendRawMessageToCSharp()">Send message to C#</button>
</div>
<div>
Message from C#: <textarea readonly id="messageFromCSharp" style="width: 80%; height: 10em;"></textarea>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
new SectionModel(typeof(EntryPage), "Entry",
"The Entry control is used for single-line text input."),

new SectionModel(typeof(HybridWebViewPage), "HybridWebView",
"The HybridWebView control embeds web content locally and natively in an app."),

new SectionModel(typeof(ImagePage), "Image",
"Displays an image."),

Expand Down
151 changes: 151 additions & 0 deletions src/Controls/src/Core/HybridWebView/HybridWebView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Maui.Controls
{
/// <summary>
/// A <see cref="View"/> that presents local HTML content in a web view and allows JavaScript and C# code to interop using messages.
/// </summary>
public class HybridWebView : View, IHybridWebView
{
/// <summary>Bindable property for <see cref="DefaultFile"/>.</summary>
public static readonly BindableProperty DefaultFileProperty =
BindableProperty.Create(nameof(DefaultFile), typeof(string), typeof(HybridWebView), defaultValue: "index.html");
/// <summary>Bindable property for <see cref="HybridRoot"/>.</summary>
public static readonly BindableProperty HybridRootProperty =
BindableProperty.Create(nameof(HybridRoot), typeof(string), typeof(HybridWebView), defaultValue: "HybridRoot");


/// <summary>
/// Specifies the file within the <see cref="HybridRoot"/> that should be served as the default file. The
/// default value is <c>index.html</c>.
/// </summary>
public string? DefaultFile
{
get { return (string)GetValue(DefaultFileProperty); }
set { SetValue(DefaultFileProperty, value); }
}

///// <summary>
///// Gets or sets the path for initial navigation after the content is finished loading. The default value is <c>/</c>.
///// </summary>
//public string StartPath { get; set; } = "/";

/// <summary>
/// The path within the app's "Raw" asset resources that contain the web app's contents. For example, if the
/// files are located in <c>[ProjectFolder]/Resources/Raw/hybrid_root</c>, then set this property to "hybrid_root".
/// The default value is <c>HybridRoot</c>, which maps to <c>[ProjectFolder]/Resources/Raw/HybridRoot</c>.
/// </summary>
public string? HybridRoot
{
get { return (string)GetValue(HybridRootProperty); }
set { SetValue(HybridRootProperty, value); }
}

void IHybridWebView.RawMessageReceived(string rawMessage)
{
RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(rawMessage));
}

/// <summary>
/// Raised when a raw message is received from the web view. Raw messages are strings that have no additional processing.
/// </summary>
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;

public class HybridWebViewRawMessageReceivedEventArgs : EventArgs
{
public HybridWebViewRawMessageReceivedEventArgs(string? message)
{
Message = message;
}

public string? Message { get; }
}
//public void Navigate(string url)
//{
// NavigateCore(url);
//}

// protected override async void OnHandlerChanged()
// {
// base.OnHandlerChanged();

// await InitializeHybridWebView();

// HybridWebViewInitialized?.Invoke(this, new HybridWebViewInitializedEventArgs()
// {
//#if ANDROID || IOS || MACCATALYST || WINDOWS
// WebView = PlatformWebView,
//#endif
// });

// Navigate(StartPath);
// }

public void SendRawMessage(string rawMessage)
{
Handler?.Invoke(nameof(IHybridWebView.SendRawMessage), rawMessage);

//EvaluateJavaScriptAsync($"window.mauiBlazorWebView.receiveMessage({JsonSerializer.Serialize(message)})");
}

// private partial Task InitializeHybridWebView();

// private partial void NavigateCore(string url);


//#if !ANDROID && !IOS && !MACCATALYST && !WINDOWS
// private partial Task InitializeHybridWebView() => throw null!;

// private partial void NavigateCore(string url) => throw null!;
//#endif

// public virtual void OnMessageReceived(string message)
// {
// var messageData = JsonSerializer.Deserialize<WebMessageData>(message);
// switch (messageData?.MessageType)
// {
// case 0: // "raw" message (just a string)
// RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(messageData.MessageContent));
// break;
// default:
// throw new InvalidOperationException($"Unknown message type: {messageData?.MessageType}. Message contents: {messageData?.MessageContent}");
// }

// }

// private sealed class WebMessageData
// {
// public int MessageType { get; set; }
// public string? MessageContent { get; set; }
// }

// internal static async Task<string?> GetAssetContentAsync(string assetPath)
// {
// using var stream = await GetAssetStreamAsync(assetPath);
// if (stream == null)
// {
// return null;
// }
// using var reader = new StreamReader(stream);

// var contents = reader.ReadToEnd();

// return contents;
// }

// internal static async Task<Stream?> GetAssetStreamAsync(string assetPath)
// {
// if (!await FileSystem.AppPackageFileExistsAsync(assetPath))
// {
// return null;
// }
// return await FileSystem.OpenAppPackageFileAsync(assetPath);
// }
}
}
1 change: 1 addition & 0 deletions src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public static IMauiHandlersCollection AddMauiControlsHandlers(this IMauiHandlers
handlersCollection.AddHandler<TimePicker, TimePickerHandler>();
handlersCollection.AddHandler<Page, PageHandler>();
handlersCollection.AddHandler<WebView, WebViewHandler>();
handlersCollection.AddHandler<HybridWebView, HybridWebViewHandler>();
handlersCollection.AddHandler<Border, BorderHandler>();
handlersCollection.AddHandler<IContentView, ContentViewHandler>();
handlersCollection.AddHandler<Shapes.Ellipse, ShapeViewHandler>();
Expand Down
4 changes: 4 additions & 0 deletions src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<EmbeddedResource Include="Resources\Images\red-embedded.png" LogicalName="red-embedded.png" />
<MauiIcon Include="Resources\appicon.svg" ForegroundFile="Resources\appiconfg.svg" Color="#512BD4" />
<MauiSplashScreen Include="Resources\appiconfg.svg" Color="#512BD4" BaseSize="128,128" />

<!-- Raw Assets for HybridWebView tests (removes the "Resources\Raw" prefix, to mimic what project templates do) -->
<None Remove="Resources\Raw\**" />
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Hosting;
using Xunit;

namespace Microsoft.Maui.DeviceTests
{
[Category(TestCategory.HybridWebView)]
public class HybridWebViewTests : ControlsHandlerTestBase
{
void SetupBuilder()
{
EnsureHandlerCreated(builder =>
{
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<HybridWebView, HybridWebViewHandler>();
});
});
}

[Fact]
public async Task LoadsHtmlAndSendReceiveRawMessage()
{
SetupBuilder();

var actual = string.Empty;

var pageLoadTimeout = TimeSpan.FromSeconds(2);

//string html =
// @"
// <!DOCTYPE html>
// <html>
// <head>
// </head>
// <body>
// <script>
// function test() {
// return 'Test';
// }
// </script>
// <p>
// WebView Unit Test
// </p>
// </body>
// </html>
// ";
await InvokeOnMainThreadAsync(async () =>
{
var hybridWebView = new HybridWebView()
{
WidthRequest = 100,
HeightRequest = 100,
//Source = new HtmlWebViewSource { Html = html }
};
var handler = CreateHandler(hybridWebView);
var platformView = handler.PlatformView;
// Setup the view to be displayed/parented and run our tests on it
await AttachAndRun(hybridWebView, async (handler) =>
{
await Task.Delay(5000);
//// Wait for the page to load
//var tcsLoaded = new TaskCompletionSource<bool>();
//var ctsTimeout = new CancellationTokenSource(pageLoadTimeout);
//ctsTimeout.Token.Register(() => tcsLoaded.TrySetException(new TimeoutException($"Failed to load HTML")));
//webView.Navigated += async (source, args) =>
//{
// // Set success when we have a successful nav result
// if (args.Result == WebNavigationResult.Success)
// {
// tcsLoaded.TrySetResult(args.Result == WebNavigationResult.Success);
// // Evaluate JavaScript
// var script = "test();";
// actual = await webView.EvaluateJavaScriptAsync(script);
// // If the result is equal to the script string result, the test has passed
// Assert.Equal("Test", actual);
// }
//};
//Assert.True(await tcsLoaded.Task);
});
});
}
}
}
11 changes: 11 additions & 0 deletions src/Controls/tests/DeviceTests/Resources/Raw/HybridRoot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
I'm a hybrid!
</body>
</html>
Loading

0 comments on commit e4ce427

Please sign in to comment.