-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
HybridWebView control for integrating JS/HTML/CSS easily into a .NET MAUI app #22880
Changes from all commits
c4d65c5
d21a331
df6a7bf
e14563b
1ebbd71
87d679c
9ab3ea6
5f04859
14104af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<views:BasePage | ||
xmlns="https://schemas.microsoft.com/dotnet/2021/maui" | ||
xmlns:x="https://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> | ||
|
||
<Grid ColumnDefinitions="2*,1*" RowDefinitions="Auto,1*"> | ||
|
||
<Label | ||
Grid.Row="0" | ||
Grid.Column="0" | ||
Text="HybridWebView here" | ||
x:Name="statusLabel" /> | ||
|
||
<Button | ||
Grid.Row="0" | ||
Grid.Column="1" | ||
Text="Send message to JS" | ||
Clicked="SendMessageButton_Clicked" /> | ||
|
||
<HybridWebView | ||
x:Name="hwv" | ||
Grid.Row="1" | ||
Grid.ColumnSpan="2" | ||
HybridRoot="HybridSamplePage" | ||
RawMessageReceived="hwv_RawMessageReceived"/> | ||
|
||
</Grid> | ||
</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_Clicked(object sender, EventArgs e) | ||
{ | ||
hwv.SendRawMessage("Hello from C#!"); | ||
} | ||
|
||
private void hwv_RawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e) | ||
{ | ||
Dispatcher.Dispatch(() => statusLabel.Text += e.Message); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<!DOCTYPE html> | ||
|
||
<html lang="en" xmlns="https://www.w3.org/1999/xhtml"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<title></title> | ||
<link rel="stylesheet" href="styles/app.css"> | ||
<script src="scripts/HybridWebView.js"></script> | ||
<script> | ||
window.addEventListener( | ||
"HybridWebViewMessageReceived", | ||
function (e) { | ||
var messageFromCSharp = document.getElementById("messageFromCSharp"); | ||
messageFromCSharp.value += '\r\n' + e.detail.message; | ||
}); | ||
</script> | ||
</head> | ||
<body> | ||
<div> | ||
Hybrid sample! | ||
</div> | ||
<div> | ||
<button onclick="window.HybridWebView.SendRawMessage('Message from JS!')">Send message to C#</button> | ||
</div> | ||
<div> | ||
Message from C#: <textarea readonly id="messageFromCSharp" style="width: 80%; height: 10em;"></textarea> | ||
</div> | ||
<div> | ||
Consider checking out this PDF: <a href="docs/sample.pdf">sample.pdf</a> | ||
</div> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
function HybridWebViewInit() { | ||
|
||
function DispatchHybridWebViewMessage(message) { | ||
const event = new CustomEvent("HybridWebViewMessageReceived", { detail: { message: message } }); | ||
window.dispatchEvent(event); | ||
} | ||
|
||
if (window.chrome && window.chrome.webview) { | ||
// Windows WebView2 | ||
window.chrome.webview.addEventListener('message', arg => { | ||
DispatchHybridWebViewMessage(arg.data); | ||
}); | ||
} | ||
else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { | ||
// iOS and MacCatalyst WKWebView | ||
window.external = { | ||
"receiveMessage": message => { | ||
DispatchHybridWebViewMessage(message); | ||
} | ||
}; | ||
} | ||
else { | ||
// Android WebView | ||
window.addEventListener('message', arg => { | ||
DispatchHybridWebViewMessage(arg.data); | ||
}); | ||
} | ||
} | ||
|
||
window.HybridWebView = { | ||
"SendRawMessage": function (message) { | ||
|
||
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.sendRawMessage(message); | ||
} | ||
} | ||
} | ||
|
||
HybridWebViewInit(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
button | ||
{ | ||
background-color: dodgerblue; | ||
border-radius: 5px; | ||
border-width: 1px; | ||
color: white; | ||
cursor: pointer; | ||
margin: 6px; | ||
text-align: center; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
using System; | ||
|
||
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"); | ||
mattleibow marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// <summary>Bindable property for <see cref="HybridRoot"/>.</summary> | ||
public static readonly BindableProperty HybridRootProperty = | ||
BindableProperty.Create(nameof(HybridRoot), typeof(string), typeof(HybridWebView), defaultValue: "wwwroot"); | ||
|
||
|
||
/// <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> | ||
/// 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>wwwroot</c>, which maps to <c>[ProjectFolder]/Resources/Raw/wwwroot</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 void SendRawMessage(string rawMessage) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any way that platforms support a "blocking" request where we could have a My main thought is that if you want to invoke multiple messages, but they need to run in series and not just span and maybe a small delay somewhere will mix the order. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are several variations of these methods that I'll be adding in subsequent PRs, including async versions, plus method invocation. Some of this is tracked in #22303 and #22304. You can technically implement this right now manually by sending messages back and forth like a state machine, but it's of course not ideal. I described this PR to another person as M-MVP (Minimum Minimal Viable Product 😁 ). |
||
{ | ||
Handler?.Invoke(nameof(IHybridWebView.SendRawMessage), rawMessage); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For future proofing, this should not pass the string directly but instead pass a new record type: // in src/Core/src/Primitives/SendRawMessageRequest.cs
namespace Microsoft.Maui
{
public record SendRawMessageRequest(string rawMessage);
} If there is any need to return a value - such as a result of the request or some response, then inherit from // in src/Core/src/Primitives/SendRawMessageRequest.cs
namespace Microsoft.Maui
{
public class SendRawMessageRequest(string rawMessage) : RetrievePlatformValueRequest<string>
{
// base members:
// public string Result { get; }
// public void SetResult(string result);
// public bool TrySetResult(string result);
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the original prototype the pattern was that the messages were encoded as JSON and there was a message type ID that said whether it was raw, method invoke, etc. Not sure what the new design will be but it will be capable of supporting multiple message types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean that for the maui codebase, we want to be consistent and use a wrapper for commands so people can cast instead of each command being its parameter data type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed in the mtg; something like this will definitely happen but I'd rather do it once I add another message type (such as to invoke methods). That way I'll have a clearer sense of the design. |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using System; | ||
|
||
namespace Microsoft.Maui.Controls | ||
{ | ||
public class HybridWebViewRawMessageReceivedEventArgs : EventArgs | ||
{ | ||
public HybridWebViewRawMessageReceivedEventArgs(string? message) | ||
{ | ||
Message = message; | ||
} | ||
|
||
public string? Message { get; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On CI the tests were failing due to timeout, so I doubled it.