Skip to content

Commit

Permalink
[Windows] Remove 2nd WebView used to add base tag when using HtmlWe…
Browse files Browse the repository at this point in the history
…bViewSource (#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
  • Loading branch information
PureWeen authored Apr 23, 2024
2 parents 17b05bc + 143550b commit 440fa7f
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue21631">
<WebView
AutomationId="WaitForWebView"
x:Name="WaitForWebView">
<WebView.Source>
<HtmlWebViewSource
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;"/>
</WebView.Source>
</WebView>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Maui.Controls.Sample.Issues
{
[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 21631, "Injecting base tag in Webview2 works", PlatformAffected.UWP)]
public partial class Issue21631 : ContentPage
{
public Issue21631()
{
InitializeComponent();
}
}
}
24 changes: 24 additions & 0 deletions src/Controls/tests/UITests/Tests/Issues/Issue21631.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.AppiumTests.Issues
{
public class Issue21631 : _IssuesUITest
{
public Issue21631(TestDevice device) : base(device) { }

public override string Issue =>
"Injecting base tag in Webview2 works";

[Test]
public async Task NavigateToStringWithWebviewWorks()
{
this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Android, TestDevice.Mac, TestDevice.iOS });

App.WaitForElement("WaitForWebView");
await Task.Delay(500);
VerifyScreenshot();
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 21 additions & 54 deletions src/Core/src/Platform/Windows/MauiWebView.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Maui.ApplicationModel;
using Microsoft.UI.Xaml.Controls;
using Windows.ApplicationModel;
Expand All @@ -28,8 +27,6 @@ public MauiWebView(WebViewHandler handler)
SetupPlatformEvents();
}

WebView2? _internalWebView;

// Arbitrary local host name for virtual folder mapping
const string LocalHostName = "appdir";
const string LocalScheme = $"https://{LocalHostName}/";
Expand All @@ -48,66 +45,29 @@ public MauiWebView(WebViewHandler handler)
: AppContext.BaseDirectory;

public async void LoadHtml(string? html, string? baseUrl)
{
{
var mapBaseDirectory = false;

if (string.IsNullOrEmpty(baseUrl))
{
baseUrl = LocalScheme;
mapBaseDirectory = true;
}

// Generate a base tag for the document
var baseTag = $"<base href=\"{baseUrl}\"></base>";

string htmlWithBaseTag;

// Set up an internal WebView we can use to load and parse the original HTML string
// Make _internalWebView a field instead of local variable to avoid garbage collection
_internalWebView = new WebView2();

// TODO: For now, the CoreWebView2 won't be created without either setting Source or
// calling EnsureCoreWebView2Async().
await _internalWebView.EnsureCoreWebView2Async();
await EnsureCoreWebView2Async();

// When the 'navigation' to the original HTML string is done, we can modify it to include our <base> tag
_internalWebView.NavigationCompleted += async (sender, args) =>
{
// Generate a version of the <base> script with the correct <base> tag
var script = BaseInsertionScript.Replace("baseTag", baseTag, StringComparison.Ordinal);

// Run it and retrieve the updated HTML from our WebView
await sender.ExecuteScriptAsync(script);
htmlWithBaseTag = await sender.ExecuteScriptAsync("document.documentElement.outerHTML;");
if (mapBaseDirectory)
{
CoreWebView2.SetVirtualHostNameToFolderMapping(
LocalHostName,
ApplicationPath,
Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
}

htmlWithBaseTag = Regex.Unescape(htmlWithBaseTag);
htmlWithBaseTag = htmlWithBaseTag.Remove(0, 1);
htmlWithBaseTag = htmlWithBaseTag.Remove(htmlWithBaseTag.Length - 1, 1);

await EnsureCoreWebView2Async();
// Insert script to set the base tag
var script = GetBaseTagInsertionScript(baseUrl);
var htmlWithScript = $"{script}\n{html}";

if (mapBaseDirectory)
{
CoreWebView2.SetVirtualHostNameToFolderMapping(
LocalHostName,
ApplicationPath,
Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
}

// Set the HTML for the 'real' WebView to the updated HTML
NavigateToString(!string.IsNullOrEmpty(htmlWithBaseTag) ? htmlWithBaseTag : html);

// Free up memory after we're done with _internalWebView
if (_internalWebView.IsValid())
{
_internalWebView.Close();
_internalWebView = null;
}
};

// Kick off the initial navigation
if (_internalWebView.IsValid())
_internalWebView.NavigateToString(html);
NavigateToString(htmlWithScript);
}

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

var localSchemeScript = GetBaseTagInsertionScript(LocalScheme);
return decodedHtml.Contains(
$"<base href=\"{LocalScheme}",
localSchemeScript,
StringComparison.OrdinalIgnoreCase);
}

static string GetBaseTagInsertionScript(string baseUrl)
{
var baseTag = $"<base href=\"{baseUrl}\"></base>";
return $"<script>{BaseInsertionScript.Replace("baseTag", baseTag, StringComparison.Ordinal)}</script>";
}
}
}

0 comments on commit 440fa7f

Please sign in to comment.