Background
I want to render some HTML inside a headless instance of the MS CoreWebView2
to a PNG file.
The approach has to work inside a “pure” .Net8.0 console application on windows
(e.g. <TargetFramework>net8.0-windows</TargetFramework>
).
Here is a condensed code sample showing my current approach:
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
namespace UiThread
{
internal class Program
{
static async Task Main(string[] args)
{
var filePath = "C:\Temp\webViewImage.png";
var html = @"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
";
var imageStream = await CaptureImage(html, new Size(512, 512));
using (var fileStream = File.Create(filePath))
{
imageStream.CopyTo(fileStream);
}
// Open file with default program.
Process.Start("explorer", """ + filePath + """);
}
static Task<Stream> CaptureImage(string html, Size size)
{
var tcs = new TaskCompletionSource<Stream>();
var thread = new Thread(() =>
{
// A SynchronizationContext like WindowsFormsSynchronizationContext is required.
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
if (SynchronizationContext.Current == null)
throw new InvalidOperationException("Failed to create STA synchronization context.");
SynchronizationContext.Current.Post(async (state) =>
{
try
{
var isComplete = new TaskCompletionSource<bool>();
var host = new HeadlessCoreWebView2(isComplete, size);
await host.CaptureImage(html);
await isComplete.Task;
tcs.SetResult(host.Result);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
finally
{
Application.ExitThread(); // Exit the windows message loop.
}
}, null);
// A windows event loop with a message pump is needed in order to run the WebView2.
Application.Run();
});
// The thread apartment state must be STA (Single-threaded apartment).
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return tcs.Task;
}
}
internal class HeadlessCoreWebView2
{
private const IntPtr HWND_MESSAGE = -3;
private Size size;
private TaskCompletionSource<bool> tcs;
private CoreWebView2Controller? controller;
private TaskCompletionSource<bool> isInitialized = new TaskCompletionSource<bool>();
private Stream? result;
public HeadlessCoreWebView2(TaskCompletionSource<bool> tcs, Size size)
{
this.size = size;
this.tcs = tcs;
InitializeAsync();
}
public Stream Result
{
get
{
return result ?? new MemoryStream();
}
}
public async Task CaptureImage(string html)
{
await isInitialized.Task;
if (controller == null)
throw new ArgumentNullException();
controller.CoreWebView2.NavigateToString(html);
}
protected async void InitializeAsync()
{
var environment = await CoreWebView2Environment.CreateAsync(userDataFolder: null);
controller = await environment.CreateCoreWebView2ControllerAsync(HWND_MESSAGE);
controller.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; ;
// Can be used to ensure, that the webview is initialized.
isInitialized.SetResult(true);
}
private async void CoreWebView2_DOMContentLoaded(object? sender, CoreWebView2DOMContentLoadedEventArgs e)
{
await RenderImage();
tcs?.SetResult(true);
}
private async Task<Stream> RenderImage()
{
if (controller == null)
throw new ArgumentNullException();
// Resize if needed.
if (controller.Bounds.Width != size.Width || controller.Bounds.Height != size.Height)
controller.Bounds = new Rectangle(Point.Empty, size);
var ms = new MemoryStream();
await controller.CoreWebView2.CapturePreviewAsync(CoreWebView2CapturePreviewImageFormat.Png, ms);
ms.Seek(0, SeekOrigin.Begin);
result = ms;
return ms;
}
}
}
Problem
The problem with the code above is that it requires me to enable <UseWindowsForms>true</UseWindowsForms>
inside the .csproj
.
However I have the requirement to neither use UseWindowsForms
or UseWPF
.
The root problem is that the CoreWebview2
requires a UI Thread / message loop in order to properly function.
Question
How can the behaviour of Application.Run()
and WindowsFormsSynchronizationContext
from WinForms be replicated, using only P/Invoke?