3

我正在尝试将一些 html 内容呈现到 Windows 服务中的位图。

我正在使用 System.Windows.Controls.WebBrowser 来执行渲染。基本的渲染设置作为一个独立的进程工作,带有一个托管控件的 WPF 窗口,但作为一项服务,至少我没有触发 LoadCompleted 事件。

我知道对于这个 WPF 控件,我至少需要一个 Dispatcher 或其他消息泵循环。也许我做得对,WebBrowser 控件只需要额外的技巧/不兼容。这是我所拥有的:

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

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

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

哪里dispatcher是静态字段。同样,我认为只能有一个,但我不确定我是否应该能够Dispatcher.CurrentDispatcher()在任何地方使用并获得正确的参考。

渲染操作如下。我创建、导航和处理WebBrowserondispatcher的线程,但事件处理程序分配和mres.Wait我认为可能都发生在渲染请求处理操作上。我得到The calling thread cannot access this object because a different thread owns it了,但现在有了这个设置,我没有。

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);断点。

这是 Dispatcher 的糟糕应用吗?wb.LoadCompleted 的未触发是由于其他原因吗?

谢谢!

4

2 回答 2

1

这是您的代码的修改版本,可用作控制台应用程序。几点:

  • 您需要 WPF WebBrowser 的父窗口。它可能是一个像下面这样的隐藏窗口,但它必须是物理创建的(即有一个实时的 HWND 句柄)。否则,WB 永远不会完成加载文档 ( wb.Document.readyState == "interactive"),也LoadCompleted永远不会被解雇。我不知道这种行为,它与 WinForms 版本的 WebBrowser 控件不同。请问你为什么选择WPF来做这种项目?

  • 您确实需要在wb.LoadCompleted创建 WB 控件的同一线程上添加事件处理程序(此处为调度程序的线程)。在内部,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();
        }
    }
}
于 2013-08-14T02:15:27.387 回答
0

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

在此处输入图像描述

我的感觉是使用 WPF 控件,特别是 Webbrowser-Control(=围绕 IE ActiveX 控件的包装器)并不是最好的主意。还有其他渲染引擎可能更适合此任务:使用 chrome 作为浏览器在 C# 中?

于 2013-08-13T16:36:27.423 回答