Skip to content

Commit 440fa7f

Browse files
authored
[Windows] Remove 2nd WebView used to add base tag when using HtmlWebViewSource (#21892)
### Description of Change This PR removes the use of a 2nd "hidden" WebView2 that was used to parse and add a HTML `base` tag to the `head` tag when setting the HTML source of a WebView to a string. This was done by appending the `base` tag script to the start of the user's HTML string, which the WebView then adds into the `head` element. While this is technically not valid HTML, all current browsers correct this behavior. This is a work-around for the lack of being able to set the base URL when navigating to a string using WebView2 (MicrosoftEdge/WebView2Feedback#530). As a bonus, using `HtmlWebViewSource` should now be 2x faster 😅 ### Issues Fixed Fixes #21631
2 parents 17b05bc + 143550b commit 440fa7f

File tree

5 files changed

+73
-54
lines changed

5 files changed

+73
-54
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Maui.Controls.Sample.Issues.Issue21631">
5+
<WebView
6+
AutomationId="WaitForWebView"
7+
x:Name="WaitForWebView">
8+
<WebView.Source>
9+
<HtmlWebViewSource
10+
Html="&lt;html&gt;&lt;body&gt;&lt;h1&gt;hello world&lt;/h1&gt;&lt;img src='appiconLargeTile.scale-100.png'/&gt;&lt;/html&gt;"/>
11+
</WebView.Source>
12+
</WebView>
13+
</ContentPage>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.Maui.Controls;
2+
using Microsoft.Maui.Controls.Xaml;
3+
4+
namespace Maui.Controls.Sample.Issues
5+
{
6+
[XamlCompilation(XamlCompilationOptions.Compile)]
7+
[Issue(IssueTracker.Github, 21631, "Injecting base tag in Webview2 works", PlatformAffected.UWP)]
8+
public partial class Issue21631 : ContentPage
9+
{
10+
public Issue21631()
11+
{
12+
InitializeComponent();
13+
}
14+
}
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.AppiumTests.Issues
6+
{
7+
public class Issue21631 : _IssuesUITest
8+
{
9+
public Issue21631(TestDevice device) : base(device) { }
10+
11+
public override string Issue =>
12+
"Injecting base tag in Webview2 works";
13+
14+
[Test]
15+
public async Task NavigateToStringWithWebviewWorks()
16+
{
17+
this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.iOS });
18+
19+
App.WaitForElement("WaitForWebView");
20+
await Task.Delay(500);
21+
VerifyScreenshot();
22+
}
23+
}
24+
}
9.86 KB
Loading

src/Core/src/Platform/Windows/MauiWebView.cs

Lines changed: 21 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Diagnostics;
33
using System.Text;
4-
using System.Text.RegularExpressions;
54
using Microsoft.Maui.ApplicationModel;
65
using Microsoft.UI.Xaml.Controls;
76
using Windows.ApplicationModel;
@@ -28,8 +27,6 @@ public MauiWebView(WebViewHandler handler)
2827
SetupPlatformEvents();
2928
}
3029

31-
WebView2? _internalWebView;
32-
3330
// Arbitrary local host name for virtual folder mapping
3431
const string LocalHostName = "appdir";
3532
const string LocalScheme = $"https://{LocalHostName}/";
@@ -48,66 +45,29 @@ public MauiWebView(WebViewHandler handler)
4845
: AppContext.BaseDirectory;
4946

5047
public async void LoadHtml(string? html, string? baseUrl)
51-
{
48+
{
5249
var mapBaseDirectory = false;
53-
5450
if (string.IsNullOrEmpty(baseUrl))
5551
{
5652
baseUrl = LocalScheme;
5753
mapBaseDirectory = true;
5854
}
5955

60-
// Generate a base tag for the document
61-
var baseTag = $"<base href=\"{baseUrl}\"></base>";
62-
63-
string htmlWithBaseTag;
64-
65-
// Set up an internal WebView we can use to load and parse the original HTML string
66-
// Make _internalWebView a field instead of local variable to avoid garbage collection
67-
_internalWebView = new WebView2();
68-
69-
// TODO: For now, the CoreWebView2 won't be created without either setting Source or
70-
// calling EnsureCoreWebView2Async().
71-
await _internalWebView.EnsureCoreWebView2Async();
56+
await EnsureCoreWebView2Async();
7257

73-
// When the 'navigation' to the original HTML string is done, we can modify it to include our <base> tag
74-
_internalWebView.NavigationCompleted += async (sender, args) =>
75-
{
76-
// Generate a version of the <base> script with the correct <base> tag
77-
var script = BaseInsertionScript.Replace("baseTag", baseTag, StringComparison.Ordinal);
78-
79-
// Run it and retrieve the updated HTML from our WebView
80-
await sender.ExecuteScriptAsync(script);
81-
htmlWithBaseTag = await sender.ExecuteScriptAsync("document.documentElement.outerHTML;");
58+
if (mapBaseDirectory)
59+
{
60+
CoreWebView2.SetVirtualHostNameToFolderMapping(
61+
LocalHostName,
62+
ApplicationPath,
63+
Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
64+
}
8265

83-
htmlWithBaseTag = Regex.Unescape(htmlWithBaseTag);
84-
htmlWithBaseTag = htmlWithBaseTag.Remove(0, 1);
85-
htmlWithBaseTag = htmlWithBaseTag.Remove(htmlWithBaseTag.Length - 1, 1);
86-
87-
await EnsureCoreWebView2Async();
66+
// Insert script to set the base tag
67+
var script = GetBaseTagInsertionScript(baseUrl);
68+
var htmlWithScript = $"{script}\n{html}";
8869

89-
if (mapBaseDirectory)
90-
{
91-
CoreWebView2.SetVirtualHostNameToFolderMapping(
92-
LocalHostName,
93-
ApplicationPath,
94-
Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
95-
}
96-
97-
// Set the HTML for the 'real' WebView to the updated HTML
98-
NavigateToString(!string.IsNullOrEmpty(htmlWithBaseTag) ? htmlWithBaseTag : html);
99-
100-
// Free up memory after we're done with _internalWebView
101-
if (_internalWebView.IsValid())
102-
{
103-
_internalWebView.Close();
104-
_internalWebView = null;
105-
}
106-
};
107-
108-
// Kick off the initial navigation
109-
if (_internalWebView.IsValid())
110-
_internalWebView.NavigateToString(html);
70+
NavigateToString(htmlWithScript);
11171
}
11272

11373
public async void LoadUrl(string? url)
@@ -184,9 +144,16 @@ static bool IsWebView2DataUriWithBaseUrl(string? uri)
184144
Convert.FromBase64String(
185145
uri.Substring(dataUriBase64.Length)));
186146

147+
var localSchemeScript = GetBaseTagInsertionScript(LocalScheme);
187148
return decodedHtml.Contains(
188-
$"<base href=\"{LocalScheme}",
149+
localSchemeScript,
189150
StringComparison.OrdinalIgnoreCase);
190151
}
152+
153+
static string GetBaseTagInsertionScript(string baseUrl)
154+
{
155+
var baseTag = $"<base href=\"{baseUrl}\"></base>";
156+
return $"<script>{BaseInsertionScript.Replace("baseTag", baseTag, StringComparison.Ordinal)}</script>";
157+
}
191158
}
192159
}

0 commit comments

Comments
 (0)