How should i properly invoke a WebBrowser using multiplethreads? - c#

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

Related

WPF WebBrowser scroll on reload

I have a System.Windows.Controls.WebBrowser. It has some html that is coming from another document that the user is editing. When the html changes, what I want to do is update the WebBrowser's html, then scroll the WebBrowser back to wherever it was. I am successfully cacheing the scroll offset (see How to retrieve the scrollbar position of the webbrowser control in .NET). But I can't get a callback when the load is complete. Here is what I have tried:
// constructor
public HTMLReferenceEditor()
{
InitializeComponent();
WebBrowser browser = this.EditorBrowser;
browser.LoadCompleted += Browser_LoadCompleted;
//browser.Loaded += Browser_Loaded; // commented out as it doesn't fire when the html changes . . .
}
private void Browser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
CommonDebug.LogLine("LoadCompleted");
this.ScrollWebBrowser();
}
private void ScrollWebBrowser()
{
WebBrowser browser = this.EditorBrowser;
ReferenceHierarchicalViewModel rhvm = this.GetReferenceHierarchichalViewModel();
int? y = rhvm.LastKnownScrollTop; // this is the cached offset.
browser?.ScrollToY(y);
}
The LoadCompleted callbacks are firing all right. But the scrolling is not happening. I suspect the callbacks are coming too soon. But it is also possible that my scroll method is wrong:
public static void ScrollToY(this WebBrowser browser, int? yQ)
{
if (yQ.HasValue)
{
object doc = browser?.Document;
HTMLDocument castDoc = doc as HTMLDocument;
IHTMLWindow2 window = castDoc?.parentWindow;
int y = yQ.Value;
window?.scrollTo(0, y);
CommonDebug.LogLine("scrolling", window, y);
// above is custom log method; prints out something like "scrolling HTMLWindow2Class3 54", which
// at least proves that nothing is null.
}
}
How can I get the browser to scroll? Incidentally, I don't see some of the callback methods others have mentioned, e.g. DocumentCompleted mentioned here does not exist for me. Detect WebBrowser complete page loading. In other words, for some reason I don't understand, my WebBrowser is different from theirs. For me, the methods don't exist.

Load html with WebBrowser Control while Blocking UI Thread

I have a .Net 4.5 WinForms application that contains html stored in a database. The user is presented with the list of pages. As they click on a row the page is loaded into the WebBrowser. I want the process of loading the page to be synchronous with the main UI thread (IOW, I don't want the user to be able to do anything else until the page is loaded). The code for loading the html is:
public class DocLoader
{
private readonly WebBrowser browser;
readonly TaskCompletionSource<object> loadedTCS = new TaskCompletionSource<object>();
public DocLoader(WebBrowser browser)
{
this.browser = browser;
}
private void LoadedComplete(object sender, WebBrowserDocumentCompletedEventArgs e)
{
Debug.WriteLine("doc completed");
loadedTCS.TrySetResult(null);
browser.DocumentCompleted -= LoadedComplete;
}
public Task LoadDoc(string html)
{
browser.DocumentCompleted += LoadedComplete;
browser.DocumentText = html;
return loadedTCS.Task;
}
}
Calling the code like this:
await new DocLoader(webBrowser1).LoadDoc(html);
Will result in the message loop being able to sill process messages. This is undesirable. For example the user could click on another document in the list before the previous has completed loading.
Calling the code like this:
new DocLoader(webBrowser1).LoadDoc(html).Wait();
Results in the UI freezing and the document never loading, presumably because the DocumentCompleted event will not fire w/o the message loop running.
Is there anyway to accomplish this task and keep the process synchronous with the UI Thread?
The WebBrowser requires its parent thread to pump message to operate properly. In your case, its parent thread is the main UI thread, so you cannot block it with Wait. One hack might be to disable the whole UI, as browser.DocumentText asynchronous loading is reasonably fast:
Cursor.Current = Cursors.WaitCursor;
mainFrom.Enabled = false;
try
{
await new DocLoader(webBrowser1).LoadDoc(html);
}
finally
{
mainFrom.Enabled = true;
Cursor.Current = Cursors.Default;
}
This however is quite user-unfriendly. A proper approach would be to support cancellation in your WebBrowser asynchronous loading scenario. You'd need to cancel the pending operation if the user clicks another row (while the the previous row update is still pending), and start a new one.

Display WinForm when WebBrowser control finished loading

I have WinForm with WebBrowser control inside.
I am facing an annoying flicker from WebBrowser control. This is manifested that the body of WebBrowser control (or background) is shown for about 200ms before the web page is loaded and I could not get rid of it.
I want, when call myForm.Show() to see the window when WebBrowser completely finished loading and not to see this flickering.
Is there some way when calling myForm.Show() to display the WebForm page at the moment when WebBrowser control finished loading the page completely?
I know about DocumentCompleted event and read and tried some tricks mentioned here but still can not get rid of this filled blank background before web page is shown.
Any hints?
public class MyForm : Form
{
WebBrowser _Webbrowser = new WebBrowser();
TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
public MyForm(string url)
{
_Webbrowser.ScriptErrorsSuppressed = true;
this.Controls.Add(_Webbrowser);
_Webbrowser.DocumentCompleted += (s, e) => { _tcs.TrySetResult(null); };
_Webbrowser.Navigate(url);
}
public async new void Show()
{
await _tcs.Task;
base.Show();
}
}

ManualResetEvent wait doesn't release after being set

I'm downloading two JSON files from the webs, after which I want to allow loading two pages, but not before. However, the ManualResetEvent that is required to be set in order to load the page never "fires". Even though I know that it gets set, WaitOne never returns.
Method that launches the downloads:
private void Application_Launching(object sender, LaunchingEventArgs e)
{
PhoneApplicationService.Current.State["doneList"] = new List<int>();
PhoneApplicationService.Current.State["manualResetEvent"] = new ManualResetEvent(false);
Helpers.DownloadAndStoreJsonObject<ArticleList>("http://arkad.tlth.se/api/get_posts/", "articleList");
Helpers.DownloadAndStoreJsonObject<CompanyList>("http://arkad.tlth.se/api/get_posts/?postType=webbkatalog", "catalog");
}
The downloading method, that sets the ManualResetEvent
public static void DownloadAndStoreJsonObject<T>(string url, string objName)
{
var webClient = new WebClient();
webClient.DownloadStringCompleted += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Result))
{
var obj = ProcessJson<T>(e.Result);
PhoneApplicationService.Current.State[objName] = obj;
var doneList = PhoneApplicationService.Current.State["doneList"] as List<int>;
doneList.Add(0);
if (doneList.Count == 2) // Two items loaded
{
(PhoneApplicationService.Current.State["manualResetEvent"] as ManualResetEvent).Set(); // Signal that it's done
}
}
};
webClient.DownloadStringAsync(new Uri(url));
}
The waiting method (constructor in this case)
public SenastePage()
{
InitializeComponent();
if ((PhoneApplicationService.Current.State["doneList"] as List<int>).Count < 2)
{
(PhoneApplicationService.Current.State["manualResetEvent"] as ManualResetEvent).WaitOne();
}
SenasteArticleList.ItemsSource = (PhoneApplicationService.Current.State["articleList"] as ArticleList).posts;
}
If I wait before trying to access that constructor, it easily passes the if-statement and doesn't get caught in the WaitOne, but if I call it immediately, I get stuck, and it never returns...
Any ideas?
Blocking the UI thread must be prevented at all costs. Especially when downloading data: don't forget that your application is executing on a phone, which has a very instable network. If the data takes two minutes to load, then the UI will be freezed for two minutes. It would be an awful user experience.
There's many ways to prevent that. For instance, you can keep the same logic but waiting in a background thread instead of the UI thread:
public SenastePage()
{
// Write the XAML of your page to display the loading animation per default
InitializeComponent();
Task.Factory.StartNew(LoadData);
}
private void LoadData()
{
((ManualResetEvent)PhoneApplicationService.Current.State["manualResetEvent"]).WaitOne();
Dispatcher.BeginInvoke(() =>
{
SenasteArticleList.ItemsSource = ((ArticleList)PhoneApplicationService.Current.State["articleList"]).posts;
// Hide the loading animation
}
}
That's just a quick and dirty way to reach the result you want. You could also rewrite your code using tasks, and using Task.WhenAll to trigger an action when they're all finished.
Perhaps there is a logic problem. In the SenastePage() constructor you are waiting for the set event only if the doneList count is less than two. However, you don't fire the set event until the doneList count is equal to two. You are listening for the set event before it can ever fire.

Prevent form from freezing

I'd like to download a picture and afterwards show it in a picturebox.
at first I did like this:
WebClient client = new WebClient();
client.DownloadFile(url, localFile);
pictureBox2.Picture = localFile;
But that wasn't perfect because for the time while the download is performed the app is kinda freezing.
Then I changed to this:
public class ParamForDownload
{
public string Url { get; set; }
public string LocalFile { get; set; }
}
ParamForDownload param = new ParamForDownload()
{
Url = url,
LocalFile = localFile
};
ThreadStart starter = delegate { DownloadMap (param); };
new Thread(starter).Start();
pictureBox2.Picture = localFile;
private static void DownloadMap(ParamForDownload p)
{
WebClient client = new WebClient();
client.DownloadFile(p.Url, p.LocalFile);
}
But now I have to do something like a "wait for thread ending" because the file is accessed in the thread and to same time there's downloaded something to the file by the DownloadMap method.
What would be the best wait to solve that problem?
Basically, What was happening originally, was the UI Thread was completing the download, and because it was working away on that, it couldn't be refreshed or painted (working synchronously). Now what is happening is that you're starting the thread then the UI thread is continuing, then trying to assign the local file (which hasn't finished downloading) to the picture box. You should try either of the following:
You should use a background worker to complete your download task.
It has events that will be very handy. DoWork, where you can start the download.
There is also a RunWorkerCompleted event that is fired when the Work has completed. Where you can set the image there (pictureBox2.Picture = localFile;).
It's definitely worth checking out, I think it's the most appropriate way to complete what you are trying to achieve.
Or
If you want to stick with using a Thread. You could take out the Image assignment after you've done the Thread.Start(), and put this in to your Worker Thread function:
private delegate void MyFunctionCaller();
private static void DownloadMap(ParamForDownload p)
{
WebClient client = new WebClient();
client.DownloadFile(p.Url, p.LocalFile);
DownloadMapComplete(p);
}
private void DownloadMapComplete(ParamForDownload p)
{
if (InvokeRequired == true)
{
MyFunctionCaller InvokeCall = delegate { DownloadMapComplete(p); };
Invoke(InvokeCall);
}
else
{
pictureBox2.Picture = p.LocalFile;
}
}
The simplest solution would be to just use the Picturebox.LoadAsync() method and let the Picturebox worry about loading it in the background. If you need to check for errors, use the LoadCompleted event (of the picturebox).
A line like:
pictureBox1.LoadAsync(#"http://imgs.xkcd.com/comics/tech_support_cheat_sheet.png");
is all you need.
When the user initiates the process what you need to do is:
1) Update the UI to indicate something is happening. IE: Disable all the fields and put up a message saying "I am downloading the file, please wait...". Preferentially with some kind of progress indicator (sorry I am not sure if the WebClient supports progress etc but you need to update the UI as the download make take a while).
2) Attach an event handler to the WebClient's 'DownloadFileCompleted'.
3) Use the WebClient's DownloadFileAsync method to start the download on another thread. You don't need to spin threads up yourself.
4) When the 'DownloadFileCompleted' event is fired route the call back to the UI thread by using the form's invoke method. THIS IS VERY IMPORTANT.
See: http://weblogs.asp.net/justin_rogers/pages/126345.aspx
and: http://msdn.microsoft.com/en-us/library/zyzhdc6b.aspx
5) Once the event has been routed back onto the UI thread open the file and update the UI as required (IE: Re-enable fields etc).
Leather.
Not related to the threading issue, but if you don't have any other requirements for saving the photo to the disk you can do this:
WebClient client = new WebClient();
byte[] data = client.DownloadData(item.Url);
MemoryStream ms = new MemoryStream(data);
Bitmap img = new Bitmap(ms);
pictureBox.Image = img;

Categories