How to keep thread alive until WebBrowser event fires - c#

I have a thread with WebBrowser instance and I have attached to it an DocumentCompleted event. But from what I have observed, my event isn't raised, because thread ends before it happens. When I put MessageBox.Show on the end of thread, it gives time for the event to be raised. But how can I make thread wait for it without MessageBox?
WebBrowser browser;
Thread t = new Thread(() =>
{
string url = string.Format("webpage.com");
browser = new WebBrowser();
browser.ScriptErrorsSuppressed = true;
browser.Navigate(url);
browser.DocumentCompleted += browser_DocumentCompleted;
browser.DocumentCompleted += (o, a) =>
{
//MessageBox.Show("In DocumentCompleted.");
List<Status> statusy = new List<Status>();
IHTMLDocument2 currentDoc = (IHTMLDocument2)browser.Document.DomDocument;
//parsing the html doc
string Statuses = "";
foreach (Status status in statusy)
{
Statuses += String.Format("{0} {1} - {2} --> {3}{4}", status.Date, status.Time, status.Centre, status.Message, Environment.NewLine);
}
MessageBox.Show(Statuses);
};
MessageBox.Show("In thread!!!!");
});
t.SetApartmentState(ApartmentState.STA);
t.Start();

You must call Application.Run(). Not just to ensure your thread doesn't end too soon, it is also required to get WebBrowser to raise its events. Using the message loop is a standard way in which heavily threaded components, like WebBrowser, ensure that its events are raised on the same thread that created the object. And it implements the STA contract.
The message loop that MessageBox.Show() uses under the hood to make itself modal is why it works right now when you use MessageBox. Not otherwise fundamentally different from the message loop that Application.Run() implements.
Use Application.ExitThread() to get the thread to end. It must be called on the same thread that called Application.Run(). That won't be a problem when you do it in the DocumentCompleted event handler.

The main problem is that the web browser control requires a proper message loop to work properly. The thread you're launching has no such message loop, so the web browser can't really do much.
The easiest solution would be to simply host the browser control in a form - this gives you easy control over the lifetime of the browser, and an easy way to maintain the message loop (that's what Application.Run does).
If that's not applicable for you (that is, you don't want to show any form at all), you'll need to make a form-less message loop. The simplest example using your code:
WebBrowser browser;
Thread t = new Thread(() =>
{
string url = string.Format("google.com");
browser = new WebBrowser();
browser.ScriptErrorsSuppressed = true;
browser.Navigate(url);
browser.DocumentCompleted += (o, a) =>
{
MessageBox.Show(browser.Document.Title);
Application.ExitThread();
};
Application.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
If you need to wait on the event from some other thread, there's plenty of ways to synchronize. A simple way to pass data at the same time and with a nice Task-based interface is the TaskCompletionSource class. For example, if I want to await the title of the document asynchronously, it's as simple as this:
WebBrowser browser;
var tcs = new TaskCompletionSource<string>();
Thread t = new Thread(() =>
{
string url = string.Format("google.com");
browser = new WebBrowser();
browser.ScriptErrorsSuppressed = true;
browser.Navigate(url);
browser.DocumentCompleted += (o, a) =>
{
tcs.SetResult(browser.Document.Title);
Application.ExitThread();
};
Application.Run();
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
Console.WriteLine(await tcs.Task);
Of course, this assumes the callee is an async method, but there's plenty of other things you can do with the task - for example, register a continuation.
You don't need to keep a reference to the Thread instance - started threads are roots, so they will never be collected.

How about setting a flag in the DocumentCompleted event handler, and then waiting for the flag to be set where you are currently doing the MessageBox.Show()?

Use a flag. In the DocumentCompleted event handler, set the flag to false.
Then use a while statement like:
bool Flag = true;
browser.DocumentCompleted += (o, a) =>
{
//MessageBox.Show("In DocumentCompleted.");
List<Status> statusy = new List<Status>();
IHTMLDocument2 currentDoc = (IHTMLDocument2)browser.Document.DomDocument;
//parsing the html doc
string Statuses = "";
foreach (Status status in statusy)
{
Statuses += String.Format("{0} {1} - {2} --> {3}{4}", status.Date, status.Time, status.Centre, status.Message, Environment.NewLine);
}
MessageBox.Show(Statuses);
Flag = false;
};
while (Flag) { }

Related

Execute simple Logic without "locking" the UI

I have a dialog with some TextBoxes and a button. When this button is clicked I want to change the button's Text, disable the button, start some Logic and then close the Window.
That logic has no return, it just sets a static variable. But it can take a minute because it connects to a DB.
How can I stop the WinForms UI from freezing? I'm looking for a simple approach, if possible no new Classes and Files.
Here's what I would do:
private async void ButtonClick()
{
//here, you're on the UI Thread
button1.Enabled = false;
await Task.Run(() => DoTheWork());
//you're back on the UI Thread
button1.Enabled = true;
}
private void DoTheWork()
{
//this will be executed on a different Thread
}
You could use a backgroundworker:
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerSupportsCancellation = false;
worker.WorkerReportsProgress = false;
worker.DoWork += (s, e) =>
{
//perform action
};
worker.RunWorkerCompleted += (s, e) =>
{
//close window
};
worker.RunWorkerAsync();
See this for more details.
when button is clicked:
button.Enabled = false;
button.Text = "new text";
Task.Factory.StartNew(() =>
{
// do your tasks here and close the window.
// type your code normally like calling methods. setting variables etc...
StaticVariable = ExampleUsingMethod();
});
if your variable needs to be assigned to UI later then you need dispatcher.for example if you want to change button inside the new thread.
Task.Factory.StartNew(() =>
{
Dispatcher.Invoke(() =>
{
button.Enabled = false;
button.Text = "new text";
}
StaticVariable = ExampleUsingMethod();
});
You can have a thread that performs the calculation.
This may help: Threads in CSharp

New thread never completes waiting on WebBrowser

In creating jpg images, this code uses threading. However, the Thread.Join() sometimes hangs on creating particular images. I have researched, and it seems as if I should be using BeginInvoke() instead. How could I rewrite the following code from using Thread.Join() to BeginInvoke()?
public Bitmap Generate()
{
var m_thread = new Thread(_Generate);
m_thread.SetApartmentState(ApartmentState.STA);
m_thread.Start();
m_thread.Join();
return m_Bitmap;
}
private void _Generate()
{
var browser = new WebBrowser {ScrollBarsEnabled = false };
browser.ScriptErrorsSuppressed = true;
browser.Navigate(m_Url);
browser.DocumentCompleted += WebBrowser_DocumentCompleted;
while (browser.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}
browser.Dispose();
}
Looking at your code I see one issue. You've registered to the DocumentCompleted event after the Navigate() call. So theoretically it's possible that the event have been fired before you've registered your handler.
Try to swap the two lines and see whether you get your problem fixed.
I believe that'll be the case if the image has already been retrieved and was cached.

Implicit function evaluation: webbrowser in thread, body always null [duplicate]

Problem Scope:
I'm writing an aplication to save the HTML's retrieved from the Bing and Google searches. I know there are classes to execute the Web Requests using stream such as this example, but since Google and Bing both use Javascript and Ajax to render the results into the HTML, there's no way i can simply read the stream and use get to the result i need.
The solution to this, is to use the WebBrowser class and navigate to the url i want, so that the Browser itself will handle all the Javascript and Ajax scripting executions.
MultiThreading:
In order to make it more efficient, i have the same Form aplication firing a thread for each service (one for Bing, and one for Google).
Problem:
Since i need the WebBrowser, i have instantiated one for each thread (which are 2, at this moment). According to Microsoft, there is a known bug that prevents the DocumentCompleted event from firing if the WebBrowser is not visible and is not added to a visible form aswell (for more information, follow this link).
Real Problem:
The main issue is that, the DocumentCompleted event of the browser, never fires. Never.
I have wrote a proper handler for the DocumentCompleted event that never gets the callback. For handling the wait needed for the Browser event to fire, i have implemented a AutoResetEvent with a high timeout (5 minutes), that will dispose the webbrowser thread if it does not fire the event i need after 5 minutes.
At the moment, i have the Browser created and added into a WindowsForm, both are visible, and the event is still not firing.
Some Code:
// Creating Browser Instance
browser = new WebBrowser ();
// Setting up Custom Handler to "Document Completed" Event
browser.DocumentCompleted += DocumentCompletedEvent;
// Setting Up Random Form
genericForm = new Form();
genericForm.Width = 200;
genericForm.Height = 200;
genericForm.Controls.Add (browser);
browser.Visible = true;
As for the Navigation i have the Following (method for the browser) :
public void NavigateTo (string url)
{
CompletedNavigation = false;
if (browser.ReadyState == WebBrowserReadyState.Loading) return;
genericForm.Show (); // Shows the form so that it is visible at the time the browser navigates
browser.Navigate (url);
}
And, for the call of the Navigation i have this :
// Loading URL
browser.NavigateTo(URL);
// Waiting for Our Event To Fire
if (_event.WaitOne (_timeout))
{
// Success
}
{ // Error / Timeout From the AutoResetEvent }
TL:DR:
My WebBrowser is instantiated into a another STAThread, added to a form, both are visible and shown when the Browser Navigation fires, but the DocumentCompleted event from the Browser is never fired, so the AutoResetEvent always times out and i have no response from the browser.
Thanks in Advance and sorry for the long post
Although this seems a strange way, here is my attempt.
var tasks = new Task<string>[]
{
new MyDownloader().Download("http://www.stackoverflow.com"),
new MyDownloader().Download("http://www.google.com")
};
Task.WaitAll(tasks);
Console.WriteLine(tasks[0].Result);
Console.WriteLine(tasks[1].Result);
public class MyDownloader
{
WebBrowser _wb;
TaskCompletionSource<string> _tcs;
ApplicationContext _ctx;
public Task<string> Download(string url)
{
_tcs = new TaskCompletionSource<string>();
var t = new Thread(()=>
{
_wb = new WebBrowser();
_wb.ScriptErrorsSuppressed = true;
_wb.DocumentCompleted += _wb_DocumentCompleted;
_wb.Navigate(url);
_ctx = new ApplicationContext();
Application.Run(_ctx);
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
return _tcs.Task;
}
void _wb_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
//_tcs.TrySetResult(_wb.DocumentText);
_tcs.TrySetResult(_wb.DocumentTitle);
_ctx.ExitThread();
}
}

Calling method on correct thread when control is created in new Thread()

I've created a new WebBrowser() control in a new Thread().
The problem I'm having, is that when invoking a delegate for my WebBrowser from the Main Thread, the call is occurring on the Main Thread. I would expect this to happen on browserThread.
private static WebBrowser defaultApiClient = null;
delegate void DocumentNavigator(string url);
private WebApi() {
// Create a new thread responsible
// for making API calls.
Thread browserThread = new Thread(() => {
defaultApiClient = new WebBrowser();
// Setup our delegates
documentNavigatorDelegate = new DocumentNavigator(defaultApiClient.Navigate);
// Anonymous event handler
defaultApiClient.DocumentCompleted += (object sender, WebBrowserDocumentCompletedEventArgs e) => {
// Do misc. things
};
Application.Run();
});
browserThread.SetApartmentState(ApartmentState.STA);
browserThread.Start();
}
DocumentNavigator documentNavigatorDelegate = null;
private void EnsureInitialized() {
// This always returns "false" for some reason
if (defaultApiClient.InvokeRequired) {
// If I jump ahead to this call
// and put a break point on System.Windows.Forms.dll!System.Windows.Forms.WebBrowser.Navigate(string urlString, string targetFrameName, byte[] postData, string additionalHeaders)
// I find that my call is being done in the "Main Thread".. I would expect this to be done in "browserThread" instead
object result = defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl);
}
}
I've tried invoking the method a myriad of ways:
// Calls on Main Thread (as expected)
defaultApiClient.Navigate(WebApiUrl);
// Calls on Main Thread
defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl);
// Calls on Main Thread
defaultApiClient.BeginInvoke(documentNavigatorDelegate, WebApiUrl);
// Calls on Main Thread
documentNavigatorDelegate.Invoke(WebApiUrl);
// Calls on random Worker Thread
documentNavigatorDelegate.BeginInvoke(WebApiUrl, new AsyncCallback((IAsyncResult result) => { .... }), null);
Update
Let me break down my end-goal a little bit to make things more clear: I have to make calls using WebBrowser.Document.InvokeScript(), however Document is not loaded until after I call WebBrowser.Navigate() and THEN the WebBrowser.DocumentComplete event fires. Essentially, I cannot make my intended call to InvokeScript() until after DocumentComplete fires... I would like to WAIT for the document to load (blocking my caller) so I can call InvokeScript and return my result in a synchronous fashion.
Basically I need to wait for my document to complete and the way I would like to do that is with a AutoResetEvent() class which I will trigger upon DocumentComplete being fired... and I need all this stuff to happen in a separate thread.
The other option I see is doing something like this:
private bool initialized = false;
private void EnsureInitialized(){
defaultApiClient.Navigate(WebApiUrl);
while(!initialized){
Thread.Sleep(1000); // This blocks so technically wouldn't work
}
}
private void defaultApiClient_DocumentComplete(object sender, WebBrowserDocumentCompletedEventArgs e){
initialized = true;
}
This is by design. The InvokeRequired/BeginInvoke/Invoke members of a control require the Handle property of the control to be created. That is the primary way by which it can figure out to what specific thread to invoke to.
But that did not happen in your code, the Handle is normally only created when you add a control to a parent's Controls collection and the parent was displayed with Show(). In other words, actually created the host window for the browser. None of this happened in your code so Handle is still IntPtr.Zero and InvokeRequired returns false.
This is not actually a problem. The WebBrowser class is special, it is a COM server under the hood. COM handles threading details itself instead of leaving it up to the programmer, very different from the way .NET works. And it will automatically marshal a call to its Navigate() method. This is entirely automatic and doesn't require any help. A hospitable home for the COM server is all that's needed, you made one by creating an STA thread and pumping a message loop with Application.Run(). It is the message loop that COM uses to do the automatic marshaling.
So you can simply call Navigate() on your main thread and nothing goes wrong. The DocumentCompleted event still fires on the helper thread and you can take your merry time tinkering with the Document on that thread.
Not sure why any of this is a problem, it should work all just fine. Maybe you were just mystified about its behavior. If not then this answer could help you with a more universal solution. Don't fear the nay-sayers too much btw, displaying UI on a worker thread is filled with traps but you never actually display any UI here and never create a window.
This answer is based on the updated question and the comments:
Basically I need to wait for my document to complete and the way I
would like to do that is with a AutoResetEvent() class which I will
trigger upon DocumentComplete being fired... and I need all this stuff
to happen in a separate thread.
...
I am aware that the main UI will be frozen. This will happen only once
during the lifetime of the application (upon initialization). I'm
struggling to find another way to do what I'm looking to accomplish.
I don't think you should be using a separate thread for this. You could disable the UI (e.g. with a modal "Please wait..." dialog) and do the WebBrowser-related work on the main UI thread.
Anyhow, the code below shows how to drive a WebBrowser object on a separate STA thread. It's based on the related answer I recently posted, but is compatible with .NET 4.0. With .NET 4+, you no longer need to use low-level synchronization primitives like AutoResetEvent. Use TaskCompletionSource instead, it allows to propagate the result and possible exceptions to the consumer side of the operation.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFroms_21790151
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
this.Load += MainForm_Load;
}
void MainForm_Load(object senderLoad, EventArgs eLoad)
{
using (var apartment = new MessageLoopApartment())
{
// create WebBrowser on a seprate thread with its own message loop
var webBrowser = apartment.Invoke(() => new WebBrowser());
// navigate and wait for the result
var bodyHtml = apartment.Invoke(() =>
{
WebBrowserDocumentCompletedEventHandler handler = null;
var pageLoadedTcs = new TaskCompletionSource<string>();
handler = (s, e) =>
{
try
{
webBrowser.DocumentCompleted -= handler;
pageLoadedTcs.SetResult(webBrowser.Document.Body.InnerHtml);
}
catch (Exception ex)
{
pageLoadedTcs.SetException(ex);
}
};
webBrowser.DocumentCompleted += handler;
webBrowser.Navigate("http://example.com");
// return Task<string>
return pageLoadedTcs.Task;
}).Result;
MessageBox.Show("body content:\n" + bodyHtml);
// execute some JavaScript
var documentHtml = apartment.Invoke(() =>
{
// at least one script element must be present for eval to work
var scriptElement = webBrowser.Document.CreateElement("script");
webBrowser.Document.Body.AppendChild(scriptElement);
// inject and run some script
var scriptResult = webBrowser.Document.InvokeScript("eval", new[] {
"(function(){ return document.documentElement.outerHTML; })();"
});
return scriptResult.ToString();
});
MessageBox.Show("document content:\n" + documentHtml);
// dispose of webBrowser
apartment.Invoke(() => webBrowser.Dispose());
webBrowser = null;
}
}
// MessageLoopApartment
public class MessageLoopApartment : IDisposable
{
Thread _thread; // the STA thread
TaskScheduler _taskScheduler; // the STA thread's task scheduler
public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
/// <summary>MessageLoopApartment constructor</summary>
public MessageLoopApartment()
{
var tcs = new TaskCompletionSource<TaskScheduler>();
// start an STA thread and gets a task scheduler
_thread = new Thread(startArg =>
{
EventHandler idleHandler = null;
idleHandler = (s, e) =>
{
// handle Application.Idle just once
Application.Idle -= idleHandler;
// return the task scheduler
tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};
// handle Application.Idle just once
// to make sure we're inside the message loop
// and SynchronizationContext has been correctly installed
Application.Idle += idleHandler;
Application.Run();
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
_taskScheduler = tcs.Task.Result;
}
/// <summary>shutdown the STA thread</summary>
public void Dispose()
{
if (_taskScheduler != null)
{
var taskScheduler = _taskScheduler;
_taskScheduler = null;
// execute Application.ExitThread() on the STA thread
Task.Factory.StartNew(
() => Application.ExitThread(),
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler).Wait();
_thread.Join();
_thread = null;
}
}
/// <summary>Task.Factory.StartNew wrappers</summary>
public void Invoke(Action action)
{
Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
}
public TResult Invoke<TResult>(Func<TResult> action)
{
return Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
}
public Task Run(Action action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task Run(Func<Task> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
}
}
}

Trycatching invoking of the UI thread

I've made an app that runs around the clock with three Backgroundworkers running in different intervals.
In their DoWork i do some Dispatcher.BeginInvoke so it updateds some charts. The problem is that its crashing during the night and I'm unsure why. I've wrapped the Dispatcher.BeginInvoke in try/catch, but since I'm invoking the UI thread, I'm thinking I maybe do the try catch INSIDE the Dispatcher.BeginInvoke instead.
Does it matter?
test if the EventArgs Error property is not null in the RunWorkerCompleted method
backgroundWorker.RunWorkerCompleted += (s, e) =>
{
if (e.Error != null)
{
// handle error
}
}
The try/catch will only work with the stuff that's happening in its current thread. If an error happened in another thread, the worker will finish but you don't know why unless you inspect the Error property.
A callback queued with Dispatcher.BeginInvoke is asynchronous. You should observe all exceptions inside the delegate you pass into Dispatcher.BeginInvoke, because they are not getting propagated anywhere outside it (except as Application.Current.DispatcherUnhandledException, AppDomain.CurrentDomain.UnhandledException events and as DispatcherOperation.Task.Exception property, see below). If they go unhanded, they will crash the app inside the core Dispatcher event loop on the UI thread.
This includes RunWorkerCompletedEventArgs.Error too. An exception thrown inside Dispatcher.BeginInvoke delegate will not be available there as Error, upon RunWorkerCompletedEvent event.
Here is a simple example illustrating the problem. Note how e.Error is null inside RunWorkerCompleted:
// UI Thread
// prepare the message window
var window = new Window
{
Content = new TextBlock { Text = "Wait while I'm doing the work..." },
Width = 200,
Height = 100
};
// run the worker
var dispatcher = Dispatcher.CurrentDispatcher;
var worker = new BackgroundWorker();
worker.DoWork += (s, e) =>
{
// do the work
Thread.Sleep(1000);
// update the UI
dispatcher.BeginInvoke(new Action(() =>
{
throw new ApplicationException("Catch me if you can!");
}));
// do more work
Thread.Sleep(1000);
};
worker.RunWorkerCompleted += (s, e) =>
{
// e.Error will be null
if (e.Error != null)
MessageBox.Show("Error: " + e.Error.Message);
// close the message window
window.Close();
};
// start the worker
worker.RunWorkerAsync();
// show the modal message window
// while the worker is working
window.ShowDialog();
To solve the problem, observe the exceptions with something like this:
var step = 0; // progress
// do the work on a background thread
// ..
var lastStep = step++;
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// do the UI update
}
catch(Exception ex)
{
// log or report the error here
MessageBox.Show("Error during step #" +
lastStep + ": " + ex.ToString());
}
}));
Alternatively, you can keep track of all DispatcherOperation returned by Dispatcher.BeginInvoke:
var invokes = new List<DispatcherOperation>();
// do the work on a background thread
// ..
invokes.Add(Dispatcher.BeginInvoke(new Action(() =>
{ /* update the UI */ }))));
Then you can examine DispatcherOperation.Task.Exception of each invocation you've queued with Dispatcher.BeginInvoke. I don't think this is feasible though, unless you can prevent the invokes list from growing endlessly.

Categories

Resources