System.Windows.Controls.WebBrowser、System.Windows.Threading.Dispatcher 和一个 Windows 服务

3

我正在尝试在Windows服务中将一些HTML内容渲染为位图。

我使用System.Windows.Controls.WebBrowser执行渲染。基本的渲染设置作为独立进程使用WPF窗口托管控件可以工作,但作为服务,至少我没有得到LoadCompleted事件。

我知道我至少需要一个Dispatcher或其他消息泵循环来处理这个WPF控件。也许我做得对,只是WebBrowser控件需要额外的技巧/不兼容性。这就是我的代码:

我相信只需要运行一个Dispatcher,并且它可以在服务的生命周期内运行。我认为Dispatcher.Run()是实际的循环本身,因此需要自己的线程,否则可能会阻塞。而且,在这种情况下,该线程需要[STAThread]。因此,在相关的静态构造函数中,我有以下内容:

var thread = new Thread(() =>
{
    dispatcher = Dispatcher.CurrentDispatcher;

    Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

其中dispatcher是一个静态字段。我认为只能有一个,但我不确定是否可以从任何地方使用Dispatcher.CurrentDispatcher()并获取正确的引用。

渲染操作如下。我在dispatcher的线程上创建、导航和处理WebBrowser,但事件处理程序分配和mres.Wait可能都发生在渲染请求处理操作中。我曾经遇到过“调用线程无法访问此对象,因为不同的线程拥有它”的问题,但现在通过这种设置,我没有遇到这个问题了。

WebBrowser wb = null;
var mres = new ManualResetEventSlim();

try
{
    dispatcher.Invoke(() => { wb = new WebBrowser(); });

    wb.LoadCompleted += (s, e) =>
    {
        // Not firing
    };

    try
    {
        using (var ms = new MemoryStream())
        using (var sw = new StreamWriter(ms, Encoding.Unicode))
        {
            sw.Write(html);
            sw.Flush();
            ms.Seek(0, SeekOrigin.Begin);

            // GO!
            dispatcher.Invoke(() =>
            {
                try
                {
                    wb.NavigateToStream(ms);
                    Debug.Assert(Dispatcher.FromThread(Thread.CurrentThread) != null);
                }
                catch (Exception ex)
                {
                    // log
                }
            });

            if (!mres.Wait(15 * 1000)) throw new TimeoutException();
        }
    }
    catch (Exception ex)
    {
        // log
    }
}
finally
{
    dispatcher.Invoke(() => { if (wb != null) wb.Dispose(); });
}

每次我运行这个程序,都会因为LoadCompleted从未触发而导致超时异常。我尝试验证调度程序是否正常运行和传输,但不确定如何操作。我从静态构造函数钩取了一些调度程序的事件并获取了一些输出信息,所以我认为它正在工作。
该代码在wb.NavigateToStream(ms);断点处得到执行。
这是调度程序应用不当吗?还是wb.LoadCompleted未触发是由于其他原因?
谢谢!

wb.LoadCompleted 没有被触发是因为你在不同于创建 WebBrowser 对象的线程上添加了该事件处理程序。尝试将其移动到 dispatcher.Invoke(() => { .. }) 委托内部。这可能仍然不是一个好的设计,但至少事件应该会被触发。 - noseratio - open to work
@Noseratio - 感谢您的建议。我不认为事件处理程序需要在同一线程上添加,但我刚刚尝试了一下,它并不允许事件触发-没有变化。一旦我让它工作起来,我会尝试有和没有的情况,以便查看结果。 - Jason Kleban
2个回答

1
这是您的代码的修改版本,可以作为控制台应用程序运行。几个要点:
  • 您需要一个父窗口来使用WPF WebBrowser。它可以像下面这样是隐藏的窗口,但它必须被实际创建(即具有活动的HWND句柄)。否则,WB永远不会完成文档的加载(wb.Document.readyState == "interactive"),并且LoadCompleted永远不会触发。我之前不知道这种行为,这与WinForms版本的WebBrowser控件不同。请问为什么您选择了WPF进行此类项目?

  • 确实需要在创建WB控件的同一线程上添加wb.LoadCompleted事件处理程序(这里是分派程序的线程)。在内部,WPF WebBrowser只是围绕公寓线程WebBrowser ActiveX控件的包装器,该控件通过IConnectionPointContainer接口公开其事件。规则是,对公寓线程COM对象的所有调用都必须在(或代理到)最初创建对象的线程上进行,因为这就是这种对象所期望的。在这个意义上,IConnectionPointContainer方法与WB的其他方法没有区别。

  • 一个小问题,StreamWriter会自动关闭它初始化的流(除非在构造函数中明确告诉它不要这样做),因此不需要使用using来包装流。

代码已准备好进行编译和运行(它需要一些额外的程序集引用:PresentationFramework、WindowsBase、System.Windows、System.Windows.Forms、Microsoft.mshtml)。
using System;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Controls;
using System.IO;
using System.Runtime.InteropServices;
using mshtml;    

namespace ConsoleWpfApp
{
    class Program
    {
        static Dispatcher dispatcher = null;
        static ManualResetEventSlim dispatcherReady = new ManualResetEventSlim();

        static void StartUIThread()
        {
            var thread = new Thread(() =>
            {
                Debug.Print("UI Thread: {0}", Thread.CurrentThread.ManagedThreadId);
                try
                {
                    dispatcher = Dispatcher.CurrentDispatcher;
                    dispatcherReady.Set();
                    Dispatcher.Run();
                }
                catch (Exception ex)
                {
                    Debug.Print("UI Thread exception: {0}", ex.ToString());
                }
                Debug.Print("UI Thread exits");
            });
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }

        static void DoWork()
        {
            Debug.Print("Worker Thread: {0}", Thread.CurrentThread.ManagedThreadId);

            dispatcherReady.Wait(); // wait for the UI tread to initialize

            var mres = new ManualResetEventSlim();
            WebBrowser wb = null;
            Window window = null; 

            try
            {    
                var ms = new MemoryStream();
                using (var sw = new StreamWriter(ms, Encoding.Unicode)) // StreamWriter automatically closes the steam
                {
                    sw.Write("<b>Hello, World!</b>");
                    sw.Flush();
                    ms.Seek(0, SeekOrigin.Begin);

                    // GO!
                    dispatcher.Invoke(() => // could do InvokeAsync here as then we wait anyway
                    {
                        Debug.Print("Invoke Thread: {0}", Thread.CurrentThread.ManagedThreadId);
                        // create a hidden window with WB
                        window = new Window()
                        {
                            Width = 0,
                            Height = 0,
                            Visibility = System.Windows.Visibility.Hidden,
                            WindowStyle = WindowStyle.None,
                            ShowInTaskbar = false,
                            ShowActivated = false
                        };
                        window.Content = wb = new WebBrowser();
                        window.Show();
                        // navigate
                        wb.LoadCompleted += (s, e) =>
                        {
                            Debug.Print("wb.LoadCompleted fired;");
                            mres.Set(); // singal to the Worker thread
                        };
                        wb.NavigateToStream(ms);
                    });

                    // wait for LoadCompleted
                    if (!mres.Wait(5 * 1000))
                        throw new TimeoutException();

                    dispatcher.Invoke(() =>
                    {   
                        // Show the HTML
                        Console.WriteLine(((HTMLDocument)wb.Document).documentElement.outerHTML);
                    });    
                }
            }
            catch (Exception ex)
            {
                Debug.Print(ex.ToString());
            }
            finally
            {
                dispatcher.Invoke(() => 
                {
                    if (window != null)
                        window.Close();
                    if (wb != null) 
                        wb.Dispose();
                });
            }
        }

        static void Main(string[] args)
        {
            StartUIThread();
            DoWork();
            dispatcher.InvokeShutdown(); // shutdown UI thread
            Console.WriteLine("Work done, hit enter to exit");
            Console.ReadLine();
        }
    }
}

没问题。我相信你有使用WPF的理由,但是如果使用WinForms的WebBrowser可能需要更少的资源。或者,也许更适合使用专门用于离屏渲染的 Awesomium - noseratio - open to work
1
耶!它可以工作了!谢谢!是啊,这可能会转到Awesomium,但这更便宜。 - Jason Kleban

0

也许Webbrowser控件需要桌面交互来呈现内容:

enter image description here

我的感觉是使用WPF控件,尤其是Webbrowser-Control(即IE ActiveX控件的包装器),不是最好的选择。有其他渲染引擎可能更适合这个任务:在C#中使用Chrome作为浏览器?

嗨,马丁。我甚至还没有将它作为服务运行。虽然它可以以那种方式运行,但现在我只是将其作为用户进程在我的凭据下使用ServiceBase harness运行 - 因此它应该已经拥有访问权限(暂时)。我也会调查一下是否可以使用Chrome,但首先找出这个问题会很不错。 - Jason Kleban

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接