System.Windows.Controls.WebBrowser, System.Windows.Threading.Dispatcher and Windows Service

I am trying to display some html content in a bitmap in a windows service.

I am using System.Windows.Controls.WebBrowser to do the rendering. The main rendering setting works as a separate process with the WPF window that hosts the control, but as a service, at least I do not fire LoadCompleted events.

I know that I at least need a dispatcher or other message flow for this WPF control. Maybe I'm doing it right, and additional tricks / incompatibilities are needed for the WebBrowser control. Here is what I have:

I believe that only one dispatcher should work and that it can work throughout its entire life cycle. I believe that Dispatcher.Run () is the actual loop and therefore needs its own thread, which it can block. And this thread should be [STAThread] in this scenario. Therefore, in the corresponding static constructor, I have the following:

 var thread = new Thread(() => { dispatcher = Dispatcher.CurrentDispatcher; Dispatcher.Run(); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); 

where dispatcher is a static field. Again, I think there can only be one, but I'm not sure that I should use Dispatcher.CurrentDispatcher() from anywhere and get the correct link.

The rendering operation is as follows. I create, move, and delete the WebBrowser on dispatcher stream, but the destination of the event handler and mres.Wait I think everything can happen in the rendering request processing operation. I got The calling thread cannot access this object because a different thread owns it , but now I do not do this setting.

 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(); }); } 

When I run this, every time LoadCompleted never fires, I get my timeout. I tried to verify that the dispatcher is working and working correctly. I don’t know how to do this, but I connected some dispatcher events from a static constructor, and I get some fingerprints from it, so I think it works.

The code hits the wb.NavigateToStream(ms); breakpoint wb.NavigateToStream(ms); .

Is this a bad dispatcher application? Is wb.LoadCompleted abandoned due to something else?

Thanks!

+3
source share
2 answers

Here is a modified version of your code that works as a console application. A few points:

  • WPF WebBrowser requires a parent window. It may be a hidden window, as shown below, but it must be physically created (i.e., have a real-time HWND handle). Otherwise, WB will never finish loading the document ( wb.Document.readyState == "interactive" ), and LoadCompleted will never be launched. I did not know about this behavior, and it differs from the WebBrowser WinForms control version. May I ask why you chose WPF for such a project?

  • You need to add the wb.LoadCompleted event wb.LoadCompleted in the same thread as the created WB control (dispatcher thread here). Internally, WPF WebBrowser is just a wrapper for the WebBrowser ActiveX control with apartments, which exposes its events through the IConnectionPointContainer interface. The rule is that all calls to a threaded COM object in an apartment must be made (or approximated) by the thread originally created by the object, because this is what such objects expect. In this sense, the IConnectionPointContainer methods are no different from other WB methods.

  • Small, StreamWriter automatically closes the initialized stream (unless it is explicitly said that this should not be done in the constructor), so there is no need to wrap the stream with using .

The code is ready to compile and run (additional assembly references are required for this: 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(); } } } 
+1
source

Perhaps Webbrowser Control content management requires Desktop Interaction:

enter image description here

My feeling is that using WPF controls and, in particular, Webbrowser-Control (= Wrap around an IE ActiveX control) is not a good idea .. There are other rendering mechanisms that may be better suited for this task: Use chrome in quality browser in c #?

0
source

Source: https://habr.com/ru/post/1497441/


All Articles