Skip to content

Commit e4ce427

Browse files
committed
WIP: Initial HybridWebView control
Very basic functionality working! (Windows and Android)
1 parent 00f6fcd commit e4ce427

File tree

20 files changed

+1607
-0
lines changed

20 files changed

+1607
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<views:BasePage
2+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Maui.Controls.Sample.Pages.HybridWebViewPage"
5+
xmlns:views="clr-namespace:Maui.Controls.Sample.Pages.Base"
6+
Title="HybridWebView">
7+
<views:BasePage.Content>
8+
<ScrollView>
9+
<VerticalStackLayout Padding="12">
10+
<Label Text="HybridWebView here" x:Name="statusLabel" />
11+
<Button Text="Send message to JS" Pressed="SendMessageButton_Pressed" />
12+
<HybridWebView
13+
x:Name="hwv"
14+
HeightRequest="150"
15+
HorizontalOptions="FillAndExpand"
16+
HybridRoot="HybridSamplePage"
17+
RawMessageReceived="hwv_RawMessageReceived"/>
18+
</VerticalStackLayout>
19+
</ScrollView>
20+
</views:BasePage.Content>
21+
</views:BasePage>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using Microsoft.Maui.Controls;
3+
4+
namespace Maui.Controls.Sample.Pages
5+
{
6+
public partial class HybridWebViewPage
7+
{
8+
public HybridWebViewPage()
9+
{
10+
InitializeComponent();
11+
}
12+
13+
private void SendMessageButton_Pressed(object sender, EventArgs e)
14+
{
15+
hwv.SendRawMessage("Hello from C#!");
16+
}
17+
18+
private void hwv_RawMessageReceived(object sender, HybridWebView.HybridWebViewRawMessageReceivedEventArgs e)
19+
{
20+
Dispatcher.Dispatch(() => statusLabel.Text += e.Message);
21+
}
22+
}
23+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<!DOCTYPE html>
2+
3+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
4+
<head>
5+
<meta charset="utf-8" />
6+
<title></title>
7+
<script>
8+
function SendRawMessageToCSharp() {
9+
10+
var message = "Message from JS!";
11+
12+
if (window.chrome && window.chrome.webview) {
13+
// Windows WebView2
14+
window.chrome.webview.postMessage(message);
15+
}
16+
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) {
17+
// iOS and MacCatalyst WKWebView
18+
window.webkit.messageHandlers.webwindowinterop.postMessage(message);
19+
}
20+
else {
21+
// Android WebView
22+
hybridWebViewHost.sendMessage(message);
23+
}
24+
25+
}
26+
27+
function ReceiveRawMessageFromCSharp(message) {
28+
var messageFromCSharp = document.getElementById("messageFromCSharp");
29+
messageFromCSharp.value += '\r\n' + message;
30+
}
31+
32+
if (window.chrome && window.chrome.webview) {
33+
// Windows WebView2
34+
window.chrome.webview.addEventListener('message', arg => {
35+
ReceiveRawMessageFromCSharp(arg.data);
36+
});
37+
}
38+
else {
39+
// Android WebView
40+
window.addEventListener('message', arg => {
41+
ReceiveRawMessageFromCSharp(arg.data);
42+
});
43+
}
44+
45+
</script>
46+
</head>
47+
<body>
48+
<div>
49+
Hybrid sample!
50+
</div>
51+
<div>
52+
<button onclick="SendRawMessageToCSharp()">Send message to C#</button>
53+
</div>
54+
<div>
55+
Message from C#: <textarea readonly id="messageFromCSharp" style="width: 80%; height: 10em;"></textarea>
56+
</div>
57+
</body>
58+
</html>

src/Controls/samples/Controls.Sample/ViewModels/ControlsViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ protected override IEnumerable<SectionModel> CreateItems() => new[]
3939
new SectionModel(typeof(EntryPage), "Entry",
4040
"The Entry control is used for single-line text input."),
4141

42+
new SectionModel(typeof(HybridWebViewPage), "HybridWebView",
43+
"The HybridWebView control embeds web content locally and natively in an app."),
44+
4245
new SectionModel(typeof(ImagePage), "Image",
4346
"Displays an image."),
4447

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.Maui.Controls
10+
{
11+
/// <summary>
12+
/// A <see cref="View"/> that presents local HTML content in a web view and allows JavaScript and C# code to interop using messages.
13+
/// </summary>
14+
public class HybridWebView : View, IHybridWebView
15+
{
16+
/// <summary>Bindable property for <see cref="DefaultFile"/>.</summary>
17+
public static readonly BindableProperty DefaultFileProperty =
18+
BindableProperty.Create(nameof(DefaultFile), typeof(string), typeof(HybridWebView), defaultValue: "index.html");
19+
/// <summary>Bindable property for <see cref="HybridRoot"/>.</summary>
20+
public static readonly BindableProperty HybridRootProperty =
21+
BindableProperty.Create(nameof(HybridRoot), typeof(string), typeof(HybridWebView), defaultValue: "HybridRoot");
22+
23+
24+
/// <summary>
25+
/// Specifies the file within the <see cref="HybridRoot"/> that should be served as the default file. The
26+
/// default value is <c>index.html</c>.
27+
/// </summary>
28+
public string? DefaultFile
29+
{
30+
get { return (string)GetValue(DefaultFileProperty); }
31+
set { SetValue(DefaultFileProperty, value); }
32+
}
33+
34+
///// <summary>
35+
///// Gets or sets the path for initial navigation after the content is finished loading. The default value is <c>/</c>.
36+
///// </summary>
37+
//public string StartPath { get; set; } = "/";
38+
39+
/// <summary>
40+
/// The path within the app's "Raw" asset resources that contain the web app's contents. For example, if the
41+
/// files are located in <c>[ProjectFolder]/Resources/Raw/hybrid_root</c>, then set this property to "hybrid_root".
42+
/// The default value is <c>HybridRoot</c>, which maps to <c>[ProjectFolder]/Resources/Raw/HybridRoot</c>.
43+
/// </summary>
44+
public string? HybridRoot
45+
{
46+
get { return (string)GetValue(HybridRootProperty); }
47+
set { SetValue(HybridRootProperty, value); }
48+
}
49+
50+
void IHybridWebView.RawMessageReceived(string rawMessage)
51+
{
52+
RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(rawMessage));
53+
}
54+
55+
/// <summary>
56+
/// Raised when a raw message is received from the web view. Raw messages are strings that have no additional processing.
57+
/// </summary>
58+
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;
59+
60+
public class HybridWebViewRawMessageReceivedEventArgs : EventArgs
61+
{
62+
public HybridWebViewRawMessageReceivedEventArgs(string? message)
63+
{
64+
Message = message;
65+
}
66+
67+
public string? Message { get; }
68+
}
69+
//public void Navigate(string url)
70+
//{
71+
// NavigateCore(url);
72+
//}
73+
74+
// protected override async void OnHandlerChanged()
75+
// {
76+
// base.OnHandlerChanged();
77+
78+
// await InitializeHybridWebView();
79+
80+
// HybridWebViewInitialized?.Invoke(this, new HybridWebViewInitializedEventArgs()
81+
// {
82+
//#if ANDROID || IOS || MACCATALYST || WINDOWS
83+
// WebView = PlatformWebView,
84+
//#endif
85+
// });
86+
87+
// Navigate(StartPath);
88+
// }
89+
90+
public void SendRawMessage(string rawMessage)
91+
{
92+
Handler?.Invoke(nameof(IHybridWebView.SendRawMessage), rawMessage);
93+
94+
//EvaluateJavaScriptAsync($"window.mauiBlazorWebView.receiveMessage({JsonSerializer.Serialize(message)})");
95+
}
96+
97+
// private partial Task InitializeHybridWebView();
98+
99+
// private partial void NavigateCore(string url);
100+
101+
102+
//#if !ANDROID && !IOS && !MACCATALYST && !WINDOWS
103+
// private partial Task InitializeHybridWebView() => throw null!;
104+
105+
// private partial void NavigateCore(string url) => throw null!;
106+
//#endif
107+
108+
// public virtual void OnMessageReceived(string message)
109+
// {
110+
// var messageData = JsonSerializer.Deserialize<WebMessageData>(message);
111+
// switch (messageData?.MessageType)
112+
// {
113+
// case 0: // "raw" message (just a string)
114+
// RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(messageData.MessageContent));
115+
// break;
116+
// default:
117+
// throw new InvalidOperationException($"Unknown message type: {messageData?.MessageType}. Message contents: {messageData?.MessageContent}");
118+
// }
119+
120+
// }
121+
122+
// private sealed class WebMessageData
123+
// {
124+
// public int MessageType { get; set; }
125+
// public string? MessageContent { get; set; }
126+
// }
127+
128+
// internal static async Task<string?> GetAssetContentAsync(string assetPath)
129+
// {
130+
// using var stream = await GetAssetStreamAsync(assetPath);
131+
// if (stream == null)
132+
// {
133+
// return null;
134+
// }
135+
// using var reader = new StreamReader(stream);
136+
137+
// var contents = reader.ReadToEnd();
138+
139+
// return contents;
140+
// }
141+
142+
// internal static async Task<Stream?> GetAssetStreamAsync(string assetPath)
143+
// {
144+
// if (!await FileSystem.AppPackageFileExistsAsync(assetPath))
145+
// {
146+
// return null;
147+
// }
148+
// return await FileSystem.OpenAppPackageFileAsync(assetPath);
149+
// }
150+
}
151+
}

src/Controls/src/Xaml/Hosting/AppHostBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static IMauiHandlersCollection AddMauiControlsHandlers(this IMauiHandlers
8686
handlersCollection.AddHandler<TimePicker, TimePickerHandler>();
8787
handlersCollection.AddHandler<Page, PageHandler>();
8888
handlersCollection.AddHandler<WebView, WebViewHandler>();
89+
handlersCollection.AddHandler<HybridWebView, HybridWebViewHandler>();
8990
handlersCollection.AddHandler<Border, BorderHandler>();
9091
handlersCollection.AddHandler<IContentView, ContentViewHandler>();
9192
handlersCollection.AddHandler<Shapes.Ellipse, ShapeViewHandler>();

src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<EmbeddedResource Include="Resources\Images\red-embedded.png" LogicalName="red-embedded.png" />
2727
<MauiIcon Include="Resources\appicon.svg" ForegroundFile="Resources\appiconfg.svg" Color="#512BD4" />
2828
<MauiSplashScreen Include="Resources\appiconfg.svg" Color="#512BD4" BaseSize="128,128" />
29+
30+
<!-- Raw Assets for HybridWebView tests (removes the "Resources\Raw" prefix, to mimic what project templates do) -->
31+
<None Remove="Resources\Raw\**" />
32+
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
2933
</ItemGroup>
3034

3135
<PropertyGroup>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Maui.Controls;
8+
using Microsoft.Maui.Handlers;
9+
using Microsoft.Maui.Hosting;
10+
using Xunit;
11+
12+
namespace Microsoft.Maui.DeviceTests
13+
{
14+
[Category(TestCategory.HybridWebView)]
15+
public class HybridWebViewTests : ControlsHandlerTestBase
16+
{
17+
void SetupBuilder()
18+
{
19+
EnsureHandlerCreated(builder =>
20+
{
21+
builder.ConfigureMauiHandlers(handlers =>
22+
{
23+
handlers.AddHandler<HybridWebView, HybridWebViewHandler>();
24+
});
25+
});
26+
}
27+
28+
[Fact]
29+
public async Task LoadsHtmlAndSendReceiveRawMessage()
30+
{
31+
SetupBuilder();
32+
33+
var actual = string.Empty;
34+
35+
var pageLoadTimeout = TimeSpan.FromSeconds(2);
36+
37+
//string html =
38+
// @"
39+
// <!DOCTYPE html>
40+
// <html>
41+
// <head>
42+
// </head>
43+
// <body>
44+
// <script>
45+
// function test() {
46+
// return 'Test';
47+
// }
48+
// </script>
49+
// <p>
50+
// WebView Unit Test
51+
// </p>
52+
// </body>
53+
// </html>
54+
// ";
55+
await InvokeOnMainThreadAsync(async () =>
56+
{
57+
var hybridWebView = new HybridWebView()
58+
{
59+
WidthRequest = 100,
60+
HeightRequest = 100,
61+
//Source = new HtmlWebViewSource { Html = html }
62+
};
63+
64+
var handler = CreateHandler(hybridWebView);
65+
66+
var platformView = handler.PlatformView;
67+
68+
// Setup the view to be displayed/parented and run our tests on it
69+
await AttachAndRun(hybridWebView, async (handler) =>
70+
{
71+
await Task.Delay(5000);
72+
//// Wait for the page to load
73+
//var tcsLoaded = new TaskCompletionSource<bool>();
74+
//var ctsTimeout = new CancellationTokenSource(pageLoadTimeout);
75+
//ctsTimeout.Token.Register(() => tcsLoaded.TrySetException(new TimeoutException($"Failed to load HTML")));
76+
77+
//webView.Navigated += async (source, args) =>
78+
//{
79+
// // Set success when we have a successful nav result
80+
// if (args.Result == WebNavigationResult.Success)
81+
// {
82+
// tcsLoaded.TrySetResult(args.Result == WebNavigationResult.Success);
83+
84+
// // Evaluate JavaScript
85+
// var script = "test();";
86+
// actual = await webView.EvaluateJavaScriptAsync(script);
87+
88+
// // If the result is equal to the script string result, the test has passed
89+
// Assert.Equal("Test", actual);
90+
// }
91+
//};
92+
93+
//Assert.True(await tcsLoaded.Task);
94+
});
95+
});
96+
}
97+
}
98+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
3+
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
4+
<head>
5+
<meta charset="utf-8" />
6+
<title></title>
7+
</head>
8+
<body>
9+
I'm a hybrid!
10+
</body>
11+
</html>

0 commit comments

Comments
 (0)