1

The WebBrowser.Print method has the limitation of not allowing the caller to specify a printer other than the system's default one. As a workaround, it has been suggested[1], [2] to alter the system's default printer prior to calling Print(), however it's also reported[3] (and I experienced firsthand) that the WebBrowser instance will continue to print to the previously defined printer even after the system default is altered.

To work around that, registering a handler to the PrintTemplateTeardown event by accessing the underlying ActiveX object of the managed WebBrowser instance and waiting for the event to fire before printing further documents has been proposed[4], [5], and that is what I am trying to implement. I simplified what is a much more complex program to the MVCE presented below.

(The program is a .NET Core 3.1 Windows Forms application, with one form containing nothing more than a BackgroundWorker object named bw.)

Form1.cs

using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.Drawing.Printing;
using System.Management;

namespace Demo_1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            bw.RunWorkerAsync();
        }

        private void bw_DoWork(object sender, DoWorkEventArgs e)
        {
            while (true)
            {
                BwPolling();

                Thread.Sleep(20000);
            }
        }

        private void BwPolling()
        {
            string[] htmlStrings = { "test1", "test2" };

            foreach (string html in htmlStrings)
            {
                Invoke((MethodInvoker)delegate
                {
                    PrinterSettings.StringCollection installedPrinters = PrinterSettings.InstalledPrinters;
                    foreach (string printer in installedPrinters)
                    {
                        string[] validPrinterNames =
                        {
                            "Microsoft Print to PDF",
                            "Microsoft XPS Document Writer"
                        };

                        if ( validPrinterNames.Contains(printer) )
                        {
                            SetDefaultPrinter(printer);
                            
                            var wb = new WebBrowser();
                            wb.DocumentText = html;

                            // With inspiration from code by Andrew Nosenko <https://stackoverflow.com/users/1768303/noseratio>
                            // From: https://stackoverflow.com/a/19737374/3258851
                            // CC BY-SA 3.0

                            var wbax = (SHDocVw.WebBrowser)wb.ActiveXInstance;

                            TaskCompletionSource<bool> printedTcs = null;
                            SHDocVw.DWebBrowserEvents2_PrintTemplateTeardownEventHandler printTemplateTeardownHandler =
                            (p)
                                => printedTcs.TrySetResult(true); // turn event into awaitable task

                            printedTcs = new TaskCompletionSource<bool>();
                            wbax.PrintTemplateTeardown += printTemplateTeardownHandler;

                            try
                            {
                                MessageBox.Show("Printing to " + printer);

                                wb.Print();

                                printedTcs.Task.Wait();
                            }

                            finally
                            {
                                wbax.PrintTemplateTeardown -= printTemplateTeardownHandler;
                            }

                            wb.Dispose();
                        }
                    }
                });
            }
        }

        private static bool SetDefaultPrinter(string name)
        {
            // With credits to Austin Salonen <https://stackoverflow.com/users/4068/austin-salonen>
            // From: https://stackoverflow.com/a/714543/3258851
            // CC BY-SA 3.0
            using ( ManagementObjectSearcher objectSearcher = new ManagementObjectSearcher("SELECT * FROM Win32_Printer") )
            {
                using ( ManagementObjectCollection objectCollection = objectSearcher.Get() )
                {
                    foreach (ManagementObject mo in objectCollection)
                    {
                        if ( string.Compare(mo["Name"].ToString(), name, true) == 0 )
                        {
                            mo.InvokeMethod("SetDefaultPrinter", null);
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
}

The problem being faced is when I remove the message box from the BwPolling() method, right before calling Print(), i.e. when this line is removed:

MessageBox.Show("Printing to " + printer);

then the program freezes, nothing is printed, and the process must eventually be terminated.

I believe I can sort of understand the issue on its surface: WebBrowser requires an STA thread with an active message loop[6], [7]; by calling printedTcs.Task.Wait(); within a Invoke((MethodInvoker)delegate block (called on the Form1 instance; this. is ommited), I am blocking the STA thread and the application hangs waiting for an event that is never fired. This is in fact mentioned in a comment under the answer I credited in my code.

Just can't figure out what a proper solution would be. Got lost in attempts to run the printing routine in a secondary thread. Maybe something wrong in my execution, guess I require assistance in this. Any help?

Thanks.

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
  • Try a small delay, experiment with the optimal duration instead of the message box – Simon Sultana Dec 05 '20 at 00:18
  • I see your still working on this problem. I would seriously consider using an alternative to `WebBrowser` – TheGeneral Dec 05 '20 at 00:22
  • @Simon, I tried, to no avail; delaying execution by `Thread.Sleep()` has the same hanging effect. – Marc.2377 Dec 05 '20 at 00:24
  • @TheGeneral I did! Very seriously - `WebBrowser` has been nothing short of a pain in the arse. But all things considered, found no suitable solution. Even came so far as to do what's suggested at https://github.com/cefsharp/CefSharp/wiki/General-Usage#printing. Created more problems than it solved in my scenario. – Marc.2377 Dec 05 '20 at 00:28
  • What if you work around it by a message box close command after displaying it? – Simon Sultana Dec 05 '20 at 00:47
  • Install-Package AutoClosingMessageBox – Simon Sultana Dec 05 '20 at 00:58
  • 2
    Have you tried `Application.DoEvents()` and `Thread.Yield()`? – Theodor Zoulias Dec 05 '20 at 01:37
  • @Simon that would be an acceptable hack for my immediate problem... Something to try tomorrow, will report back. – Marc.2377 Dec 05 '20 at 03:02
  • @TheodorZoulias, I have not. How do you suggest I do so? – Marc.2377 Dec 05 '20 at 03:06
  • As a replacement of the `MessageBox.Show(...` line. I don't expect it to solve the hanging problem, but you could give it a try. – Theodor Zoulias Dec 05 '20 at 03:26
  • What you are probably searching for is a way to get the effect of `MessageBox.Show`, without actually showing anything to the user. A kind of invisible transient message box. – Theodor Zoulias Dec 05 '20 at 03:30
  • 1
    @TheodorZoulias, `Application.DoEvents()` seems to have worked in my local tests with fake printers. I'll have to wait until tomorrow to test on my client's machine; if it proves to be robust enough I guess I can settle with that solution. Thanks – Marc.2377 Dec 05 '20 at 04:06
  • @TheodorZoulias As an update, the proposed workaround has been working well for the past couple of days in the live application. – Marc.2377 Dec 08 '20 at 02:17
  • 1
    Happy to hear that! But I guess it's too trivial a solution (and probably too accidental) to post it as an answer. – Theodor Zoulias Dec 08 '20 at 03:28

0 Answers0