WebView2Feedback icon indicating copy to clipboard operation
WebView2Feedback copied to clipboard

[Problem]: WebView2 won't work outside of a UI thread

Open CocoDico78 opened this issue 2 years ago • 16 comments

What happened?

Several issues come to the same conclusions:

The WV2's API need to run in a UI thread [from #3617]

there was no UI thread in my application, causing the await to hang [from #202 ]

This causes a hard dependency on heavy Windows DLLs to use a Dispatcher, and forces the host app to run as STA (Single-Threaded Apartment), de facto excluding async main for console apps (see this closed issue).

It would be beneficial to make this dependency more explicit and less cumbersome, eg:

  • expose a way to create a CoreWebView2 without any reference to a UI framework and without creating an unused CompositionController
  • remove dependency to a IntPtr hwnd when creating the webview
  • expose a way to initialize a CoreWebView2 by passing in a Dispatcher, to make the dependency more explicit
  • remove WPF/Winforms from the NuGet package for purely headless uses
  • document the issue more clearly and document the headless mode more clearly

Importance

Moderate. My app's user experience is affected, but still usable.

Runtime Channel

Stable release (WebView2 Runtime)

Runtime Version

No response

SDK Version

No response

Framework

Other

Operating System

Windows 11

OS Version

No response

Repro steps

in a blank console app Program.cs:

var env = await CoreWebView2Environment.CreateAsync();
var controller = await env.CreateCoreWebView2ControllerAsync(new IntPtr(-3)); // doesn't complete
var coreWebView = controller.CoreWebView2; // never reached

Repros in Edge Browser

Not Applicable

Regression

No, this never worked

Last working version (if regression)

No response

AB#48454374

CocoDico78 avatar Nov 28 '23 08:11 CocoDico78

One can as you note, create a separate STA thread and use that to run the WebView2 but its not easy from the API to create a headless WebView2. I've created an internal item to track this. Thanks.

david-risney avatar Jan 10 '24 22:01 david-risney

@CocoDico78 can you please details on the scenarios you would use this for?

Thanks!

nishitha-burman avatar Feb 23 '24 18:02 nishitha-burman

I am wrapping a JS API inside a .NET class library. Calls to that JS API require a Chrome extension to be running in the web browser (because it internally uses firebreath to communicate with a native app). The API doesn't have any UI so I don't need any UI either, and it can be used in console apps.

CocoDico78 avatar Feb 26 '24 08:02 CocoDico78

I am replacing an existing C# implementation using the InternetExplorer object which navigates to a local HTML file; then uses the ExecWB method to pass in a COM OLECMDID.OLECMDID_PRINT command. With IE, this window was not visible to the end user and worked seamlessly. So, I'm now looking for an alternative not using Internet Explorer, since it's deprecated/disabled through a recent Windows update.

rh185056 avatar Feb 27 '24 16:02 rh185056

Trying to use this for headless HTML to PDF conversion. I can get this to work with a desktop context active and using an offscreen Windows form. But it doesn't work if there's no desktop context (ie. from within IIS or a service for example).

HWND_MESSAGE (IntPtr(-3)) just hangs even if running off an STA thread. IntPtr.Zero gives invalid value.

RickStrahl avatar Mar 27 '24 01:03 RickStrahl

Trying to use this for headless HTML to PDF conversion. I can get this to work with a desktop context active and using an offscreen Windows form. But it doesn't work if there's no desktop context (ie. from within IIS or a service for example).

HWND_MESSAGE (IntPtr(-3)) just hangs even if running off an STA thread. IntPtr.Zero gives invalid value.

I Have Same Exactly Problem

lucaswhob avatar Apr 16 '24 12:04 lucaswhob

I ran across the blog post by @RickStrahl on this, where he does a great job as always explaining the issue. I'd also like to use WebView2 in non-interactive mode for PDF generation and other features. I'd use it in .NET hosted Windows Services. Microsoft did a great job with making .NET cross-platform by segmenting-off things that need Windows features. It would be awesome to see that same mindset here, not by making it cross-platform, but a way to use this without needing STA, Windows Forms, event loop, etc. A reliable worry-free browser engine that could be used by .NET server apps. That would give me an even stronger desire to host my web apps on Windows than I already have.

Dean-NC avatar Jun 08 '24 14:06 Dean-NC

Hello, I have encountered a few opened issues regarding this topic, so I was interested if there are any news about this. My use case is to silently print a pdf file from ASP.NET web api and I do not really like the idea of starting a Adobe Reader process or something similar, would be nice if I was able to use the WebView2 for that.

DavidM29 avatar Dec 02 '24 15:12 DavidM29

It is possible but it requires a bit of work to make it work as it requires setting up a headless environment for the WebVIew to run in.

You can read more about it in this blog post:

Programmatic Html to PDF Generation using the WebView2 Control with .NET

or you can take a look and use the Westwind.WebView NuGet package that includes the HtmlToPdf functionality described in the post using a simple class.

https://github.com/RickStrahl/Westwind.WebView/blob/master/HtmlToPdf.md

RickStrahl avatar Dec 03 '24 07:12 RickStrahl

Thanks a lot @RickStrahl, I will look through your article. However, I think it would be extremely nice to have this kind of functionality out of the box.

DavidM29 avatar Dec 03 '24 08:12 DavidM29

I am also interested in a solution - in my case, I am creating a NativeAoT Library which get's consumed by a Clarion app - in this library i need to do a oAUTH2.0 login, currently I am just using the browser for this, but this also forces me to have an external service running to catch the oauth response correctly - would love to use a Webview for this so I can catch it in my libraries flow, bit like how MSAL does it with WAM (Web Account Manager) - Also checked how they do it, but it seems to be something natively integrated in Windows and non-extendable

ThaDaVos avatar Mar 14 '25 10:03 ThaDaVos

@david-risney @nishitha-burman what is the status on the internal item? Is there a roadmap for allowing headless, non-interactive use-cases such as from Windows service?

CocoDico78 avatar Apr 09 '25 15:04 CocoDico78

The feature which is part of backlog has not been prioritized for now. Will provide an update when the status changes.

Lakshmisha-KS avatar Apr 10 '25 09:04 Lakshmisha-KS

If anyone needs an immediate workaround, @RickStrahl's solution has worked pretty well for my case, I have created a small ASP.NET Web API that prints a pdf file using headless WebView2 and it's working in production.

DavidM29 avatar Apr 10 '25 09:04 DavidM29

I'll try to apply @RickStrahl workaround from a Windows Service, hopefully it works.

CocoDico78 avatar Apr 10 '25 13:04 CocoDico78

Hey @CocoDico78,

If it helps this is the code that works for me, do not forget to enable Enable WPF for this project in the project properties.

[HttpPost("PrintPdf")]
public async Task<IActionResult> PrintPdf([FromBody]string pdfBase64String, string printerName)
{
    string filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.pdf");

    await using (FileStream fileStream = new(filePath, FileMode.Create))
    {
        await fileStream.WriteAsync(Convert.FromBase64String(pdfBase64String));
    }

    TaskCompletionSource<PrintResult> tcs = new();

    Thread thread = new (() =>
    {
        Dispatcher.CurrentDispatcher.Invoke(async () =>
        {
            try
            {
                var webView2Environment = await CoreWebView2Environment.CreateAsync(
                    userDataFolder: Path.Combine(Path.GetTempPath(), "WebView2_Environment"));
                var coreWebView2Controller = await webView2Environment.CreateCoreWebView2ControllerAsync(new IntPtr(-3));

                coreWebView2Controller.CoreWebView2.NavigationCompleted += async (_, _) =>
                {
                    try
                    {
                        var printSettings = webView2Environment.CreatePrintSettings();
                        printSettings.PrinterName = printerName;

                        var printStatus = await coreWebView2Controller.CoreWebView2.PrintAsync(printSettings);
                        coreWebView2Controller.Close();
                        tcs.SetResult(new PrintResult(printStatus == CoreWebView2PrintStatus.Succeeded, printStatus.ToString()));
                    }
                    catch (Exception e)
                    {
                        logger.LogError(e, "Error occured during the printing process.");
                        tcs.SetResult(new PrintResult(false, "Error occured during the printing process."));
                    }
                };

                coreWebView2Controller.CoreWebView2.Navigate(new Uri(filePath, UriKind.Absolute).ToString());
            }
            catch (Exception e)
            {
                logger.LogError(e, "Error occured during the WebView initialization.");
                tcs.SetResult(new PrintResult(false, "Error occured during the WebView initialization."));
            }
        });

        Dispatcher.Run();
    });

    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();

    var result = await tcs.Task;

    try
    {
        System.IO.File.Delete(filePath);
    }
    catch (Exception e)
    {
        logger.LogError(e, "Error occured during the file cleanup process.");
    }

    return result.IsSuccess ? Ok(result) : StatusCode(StatusCodes.Status500InternalServerError, result);
}

DavidM29 avatar Apr 10 '25 14:04 DavidM29