Load html with WebBrowser Control while Blocking UI Thread - c#

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.

Related

UI is unresponsive in winforms when using async methods

So I have a few methods that I want to call when my form loads (ideally in the constructor but since async/await doesn't work in the constructor I am using the Form_Load event). Originally I was using a separate thread to do this work which was working great. Everything was getting done and the UI was responsive while the work was being done. However, I have read that using async/await is "better", "less resource intensive" and is just generally preferred over creating separate threads. I guess the reasoning is that using async/await uses fewer threads?
But when I use this method as illustrated below, the UI is frozen/unresponsive while the function that takes a few seconds is running.
In my Form_Load event I am calling a synchronous method:
private void Form_Load(object sender, EventArgs e)
{
CheckForDriver();
}
And then here is my CheckForDriver function:
private void CheckForDriver()
{
System.Management.SelectQuery query = new SelectQuery("Win32_SystemDriver") {
Condition = "Description = 'my driver'" };
ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
ManagementObjectCollection drivers = searcher.Get();
if (drivers.Count > 0) // valid driver, go to next page
{
wizardControl.SelectedTab = startPage;
task = QueryDeviceAsync(false, new List<Button>());
}
}
where task is a field defined as private Task task;
And here is the QueryDeviceAsync function, the part that takes some time is the switcher.GetDeviceAndSize() function.
private async Task QueryDeviceAsync(bool enableForm, List<Button> buttons)
{
lastBackEnable = backBtn.Enabled;
lastNextEnable = nextBtn.Enabled;
EnableButtons(false, false);
this.Enabled = enableForm;
if (buttons != null)
{
foreach (Button button in buttons)
{
button.Enabled = false;
}
}
await Task.Run(() => switcher.GetDeviceAndSize()); // this function takes a few seconds and this is where the UI becomes unresponsive.
ThreadFinished?.Invoke(buttons);
}
and then in the ThreadFinished event handler, I am doing await task; to wait for the QueryDeviceAsync function to finish, at which time I update some UI stuff based on what the switcher.GetDeviceAndSize function did. I was also confused about whether I can/should update UI stuff in an async method, such as when I am disabling the buttons in the buttons list in the QueryDeviceAsync function. I know this doesn't work in a second thread and has to be done on the thread that they were created in, but this runs without issues.
My main problem is that the form is still unresponsive while I'm using these async functions. It works fine when I use a separate thread so I'm inclined to just go back to that but I thought I would try to figure this method out.
In this case you need to offload the blocking synchronous work to a worker thread. for example:
var search = new ManagementObjectSearcher(Query.ToString());
await Task.Run(() => search.Get());

C# .netcore WPF Displaying window with details of background process failure

I have a WPF application. A window launches and asks the user to select a launch mode, "Demo" or "Live".
If they click "Live" then an indeterminate progress bar comes up and the application will run a mixture of synchronous and asynchronous tasks which acquire data from a COM+ service while the progress bar is animated.
if the requests for data are unsuccessful a window comes up and the window with the progress bar remains on screen. The unsuccessful window shows high level failure details and gives two options: retry, quit.
the problem is that data calls are blocking the UI so even though the progressbar is set to visible, it is not actually visible until the button click event has returned (either by the data being loaded successfully or not).
If i want to run my data requests in Tasks then the UI is unblocked and the button click event is returned, the progress bar is visible and animated but if the data request fails I have no way to launch the unsucessfull window. I cannot use exceptions thrown in the data request thread to notify the main thread without awaiting the data request Task but cannot await the task as it will freeze the UI thread and not show a responsive UI while data is loaded.
I clearly have an antipattern. What is a good way to display a window when a background task fails without blocking the UI thread?
App.xaml.cs
private void Application_Startup(object sender, StartupEventArgs e)
{
launch.ShowDialog(); //this window has two buttons for live or demo launch. on selection itll show an indeterminate progressbar
loadingWindow.ShowDialog(); //this shows the window but doesnt stop exectution to the next line
}
Launch.xaml.cs
private void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible; //ProgressBarVisibility prop is model bound to the progress bars visibility
// Visibility is not updated yet because the UI is currently blocked by the lines below...
try
{
LoadConfigs();
}
catch
{
Close(ExitReasons.CONFIGLOADERROR);
}
// this is where the data requests might fail and i need to
//open a new window that says which task failed (getUser or getRecord etc...) and give the
//user an option to return or close. If i make this task async then i unblock the UI but
//cannot try catch any of the errors thrown in the call chain withouth awaiting the call
//which blocks the UI....
PreCacheEHRData();
LaunchMainApp(); // if the data loads successfully the launch window is closed
//an a new window with the primary functionality is launched.
}
Thanks for your help
Call any long-running operation on a background thread by creating an awaitable Task, e.g.:
private async void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible;
try
{
await Task.Run(() => LoadConfigs());
}
catch
{
Close(ExitReasons.CONFIGLOADERROR);
}
await Task.Run(() => PreCacheEHRData());
LaunchMainApp();
}
Note that the methods that run on a background thread, i.e. LoadConfigs and PreCacheEHRData in the sample code above, cannot access any UI element.
You should do the I/O work on an awaited task. Wrap try/catch block with Task.Run, and do the UI calls inside the catch block on Application's dispatcher.
make btnDemo_click async void.
private async void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible;
var isDataLoadedSuccessfully = await Task.Run(() =>
{
try
{
LoadConfigs();
return true;
}
catch
{
// Application.Current.Dispatcher.Invoke(() =>
Dispatcher.Invoke(() =>
{
// here you can do UI-related calls..
Close(ExitReasons.CONFIGLOADERROR);
});
return false;
}
});
// next code will be on UI thread
ProgressBarVisibility = Visibility.Hidden;
if (isDataLoadedSuccessfully)
{
// await Task.Run(() => PreCacheEHRData());
PreCacheEHRData();
LaunchMainApp();
}
else
{
// do something
}
}

Form is displaying before async function completes

I've got a WinForms project that scans a given network and returns valid IP addresses. Once all the addresses are found, I create a user control for each and place it on the form. My functions to ping ip addresses use async and Task which I thought would "wait" to execute before doing something else, but it doesn't. My form shows up blank, then within 5 seconds, all the user controls appear on the form.
Declarations:
private List<string> networkComputers = new List<string>();
Here's the Form_Load event:
private async void MainForm_Load(object sender, EventArgs e)
{
//Load network computers.
await LoadNetworkComputers();
LoadWidgets();
}
The LoadNetworkComputers function is here:
private async Task LoadNetworkComputers()
{
try
{
if (SplashScreenManager.Default == null)
{
SplashScreenManager.ShowForm(this, typeof(LoadingForm), false, true, false);
SplashScreenManager.Default.SetWaitFormCaption("Finding computers");
}
else
Utilities.SetSplashFormText(SplashForm.SplashScreenCommand.SetLabel, "Scanning network for computers. This may take several minutes...");
networkComputers = await GetNetworkComputers();
}
catch (Exception e)
{
MessageBox.Show(e.Message + Environment.NewLine + e.InnerException);
}
finally
{
//Close "loading" window.
SplashScreenManager.CloseForm(false);
}
}
And the last 2 functions:
private async Task<List<string>> GetNetworkComputers()
{
networkComputers.Clear();
List<string> ipAddresses = new List<string>();
List<string> computersFound = new List<string>();
for (int i = StartIPRange; i <= EndIPRange; i++)
ipAddresses.Add(IPBase + i.ToString());
List<PingReply> replies = await PingAsync(ipAddresses);
foreach(var reply in replies)
{
if (reply.Status == IPStatus.Success)
computersFound.Add(reply.Address.ToString());
}
return computersFound;
}
private async Task<List<PingReply>> PingAsync(List<string> theListOfIPs)
{
var tasks = theListOfIPs.Select(ip => new Ping().SendPingAsync(ip, 2000));
var results = await Task.WhenAll(tasks);
return results.ToList();
}
I'm really stuck on why the form is being displayed before the code in the MainForm_Load event finishes.
EDIT
I forgot to mention that in the LoadNetworkComputers it loads a splash form which lets the user know that the app is running. It's when the form shows up behind that, that I'm trying to avoid. Here's a screenshot (sensitive info has been blacked out):
The reason one would use async-await is to enable callers of functions to continue executing code whenever your function has to wait for something.
The nice thing is that this will keep your UI responsive, even if the awaitable function is not finished. For instance if you would have a button that would LoadNetworkComputers and LoadWidgets you would be glad that during this relatively long action your window would still be repainted.
Since you've defined your Mainform_Loadas async, you've expressed that you want your UI to continue without waiting for the result of LoadNetWorkComputers.
In this interview with Eric Lippert (search in the middle for async-await) async-await is compared with a a cook making dinner. Whenever the cook finds that he has to wait for the bread to toast, he starts looking around to see if he can do something else, and starts doing it. After a while when the bread is toasted he continues preparing the toasted bread.
By keeping the form-load async, your form is able to show itself, and even show an indication that the network computers are being loaded.
An even nicer method would be to create a simple startup-dialog that informs the operator that the program is busy loading network computers. The async form-load of this startup-dialog could do the action and close the form when finished.
public class MyStartupForm
{
public List<string> LoadedNetworkComputers {get; private set;}
private async OnFormLoad()
{
// start doing the things async.
// keep the UI responsive so it can inform the operator
var taskLoadComputers = LoadNetworkComputers();
var taskLoadWidgets = LoadWidgets();
// while loading the Computers and Widgets: inform the operator
// what the program is doing:
this.InformOperator();
// Now I have nothing to do, so let's await for both tasks to complete
await Task.WhenAll(new Task[] {taskLoadComputers, taskLoadWidgets});
// remember the result of loading the network computers:
this.LoadedNetworkComputers = taskLoadComputers.Result;
// Close myself; my creator will continue:
this.Close();
}
}
And your main form:
private void MainForm_Load(object sender, EventArgs e)
{
// show the startup form to load the network computers and the widgets
// while loading the operator is informed
// the form closes itself when done
using (var form = new MyStartupForm())
{
form.ShowDialog(this);
// fetch the loadedNetworkComputers from the form
var loadedNetworkComputers = form.LoadedNetworkComputers;
this.Process(loadedNetworkComputers);
}
}
Now while loading, instead of your mainform the StartupForm is shown while the items are loaded.. The operator is informed why the main form is not showing yet. As soon as loading is finished, the StartupForm closes itself and loading of the main form continues
My form shows up blank, then within 5 seconds, all the user controls appear on the form.
This is by design. When the UI framework asks your app to display a form, it must do so immediately.
To resolve this, you'll need to decide what you want your app to look like while the async work is going on, initialize to that state on startup, and then update the UI when the async work completes. Spinners and loading pages are a common choice.

New thread is running as UI thread

I have a form "DisplayImagesForm" which calls a function loadImages() in the constructor:
public ImageScraperForm(string query, RichTextBox textBox)
{
InitializeComponent();
this.query = query;
loadImages();
}
Then the loadImages() function creates a new thread at the end:
{
...
Thread thread = new Thread(readNextImage);
thread.Start();
}
The problem is that the thread doesn't seem to be running as a different thread than UI thread. The readNextImage() method loads images from a server which takes some times - while loading the image it blocks the whole form. It's not normal, because the "thread" should be running separately from the UI thread. Also the readNextImage() function can modify the UI elements without Invoke((MethodInvoker)delegate - no exception is thrown.
If you're trying to download an image over the internet and display it in a WinForm control; you're going about it completely wrong.
Do not do any lengthy processing in your Form constructor; that is going to make your form unresponsive. If you are going to display something in the UI you should do that in your Form's Paint event handler. If you are trying to download something over the internet you should be using await, not a Thread. Threads use a CPU core and, since the internet is orders of magnitude slower than a CPU, you are going to be blocking it immediately.
The correct way of doing this is using await to load the file when you need it. If you need it when you start the Load event handler is a good choice.
private async void YourForm_Load(object sender, EventArgs e)
{
using (var c = new HttpClient())
using (var resp = await c.GetAsync(#"http://uri/to/image.jpg"))
using (var content = resp.Content)
using (var s = await content.ReadAsStreamAsync())
{
_img = new Bitmap(s);
}
YourControl.Invalidate();
}
private void YourForm_Paint(object sender, PaintEventArgs e)
{
if (_img != null)
DrawToYourControl(_img);
}

How should i properly invoke a WebBrowser using multiplethreads?

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

Categories