I have been attempting to have a re-usable modal progress window (I.e. progressForm.ShowDialog()) to show progress from a running async task, including enabling cancellation.
I have seen some implementations that launch start the async task by hooking the Activated event handler on the form, but I need to start the task first, then show the modal dialog that will show it's progress, and then have the modal dialog close when completed or cancellation is completed (note - I want the form closed when cancellation is completed - signalled to close from the task continuation).
I currently have the following - and although this working - are there issues with this - or could this be done in a better way?
I did read that I need to run this CTRL-F5, without debugging (to avoid the AggregateException stopping the debugger in the continuation - and let it be caught in the try catch as in production code)
ProgressForm.cs
- Form with ProgressBar (progressBar1) and Button (btnCancel)
public partial class ProgressForm : Form
{
public ProgressForm()
{
InitializeComponent();
}
public event Action Cancelled;
private void btnCancel_Click(object sender, EventArgs e)
{
if (Cancelled != null) Cancelled();
}
public void UpdateProgress(int progressInfo)
{
this.progressBar1.Value = progressInfo;
}
}
Services.cs
- Class file containing logic consumed by WinForms app (as well as console app)
public class MyService
{
public async Task<bool> DoSomethingWithResult(
int arg, CancellationToken token, IProgress<int> progress)
{
// Note: arg value would normally be an
// object with meaningful input args (Request)
// un-quote this to test exception occuring.
//throw new Exception("Something bad happened.");
// Procressing would normally be several Async calls, such as ...
// reading a file (e.g. await ReadAsync)
// Then processing it (CPU instensive, await Task.Run),
// and then updating a database (await UpdateAsync)
// Just using Delay here to provide sample,
// using arg as delay, doing that 100 times.
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(arg);
progress.Report(i + 1);
}
// return value would be an object with meaningful results (Response)
return true;
}
}
MainForm.cs
- Form with Button (btnDo).
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
// start the task, and continue back on UI thread to close ProgressForm
Task<bool> responseTask
= MyService.DoSomethingWithResultAsync(100, token, progress)
.ContinueWith(p =>
{
if (!progressForm.IsDisposed) // don't attempt to close disposed form
progressForm.Close();
return p.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
Debug.WriteLine("Before ShowDialog");
// only show progressForm if
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.ShowDialog();
Debug.WriteLine("After ShowDialog");
bool response = false;
// await for the task to complete, get the response,
// and check for cancellation and exceptions
try
{
response = await responseTask;
MessageBox.Show("Result = " + response.ToString());
}
catch (AggregateException ae)
{
if (ae.InnerException is OperationCanceledException)
Debug.WriteLine("Cancelled");
else
{
StringBuilder sb = new StringBuilder();
foreach (var ie in ae.InnerExceptions)
{
sb.AppendLine(ie.Message);
}
MessageBox.Show(sb.ToString());
}
}
finally
{
// Do I need to double check the form is closed?
if (!progressForm.IsDisposed)
progressForm.Close();
}
}
}
Modified code - using TaskCompletionSource as recommended...
private async void btnDo_Click(object sender, EventArgs e)
{
bool? response = null;
string errorMessage = null;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
using (ProgressForm2 progressForm = new ProgressForm2())
{
progressForm.Cancelled +=
() => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Shown +=
(sX, eX) => dialogReadyTcs.TrySetResult(null);
var dialogTask = Task.Factory.StartNew(
() =>progressForm.ShowDialog(this),
cts.Token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
await dialogReadyTcs.Task;
Progress<int> progress = new Progress<int>(
(progressInfo) => progressForm.UpdateProgress(progressInfo));
try
{
response = await MyService.DoSomethingWithResultAsync(50, cts.Token, progress);
}
catch (OperationCanceledException) { } // Cancelled
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
progressForm.Close();
}
await dialogTask;
}
}
if (response != null) // Success - have valid response
MessageBox.Show("MainForm: Result = " + response.ToString());
else // Faulted
if (errorMessage != null) MessageBox.Show(errorMessage);
}
I think the biggest issue I have, is that using await (instead of
ContinueWith) means I can't use ShowDialog because both are blocking
calls. If I call ShowDialog first the code is blocked at that point,
and the progress form needs to actually start the async method (which
is what I want to avoid). If I call await
MyService.DoSomethingWithResultAsync first, then this blocks and I
can't then show my progress form.
The ShowDialog is indeed a blocking API in the sense it doesn't return until the dialog has been closed. But it is non-blocking in the sense it continues to pump messages, albeit on a new nested message loop. We can utilize this behavior with async/await and TaskCompletionSource:
private async void btnDo_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// Create the ProgressForm, and hook up the cancellation to it.
ProgressForm progressForm = new ProgressForm();
progressForm.Cancelled += () => cts.Cancel();
var dialogReadyTcs = new TaskCompletionSource<object>();
progressForm.Load += (sX, eX) => dialogReadyTcs.TrySetResult(true);
// show the dialog asynchronousy
var dialogTask = Task.Factory.StartNew(
() => progressForm.ShowDialog(),
token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
// await to make sure the dialog is ready
await dialogReadyTcs.Task;
// continue on a new nested message loop,
// which has been started by progressForm.ShowDialog()
// Create the progress reporter - and have it update
// the form directly (if form is valid (not disposed))
Action<int> progressHandlerAction = (progressInfo) =>
{
if (!progressForm.IsDisposed) // don't attempt to use disposed form
progressForm.UpdateProgress(progressInfo);
};
Progress<int> progress = new Progress<int>(progressHandlerAction);
try
{
// await the worker task
var taskResult = await MyService.DoSomethingWithResultAsync(100, token, progress);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (!(ex is OperationCanceledException))
MessageBox.Show(ex.Message); // report the error
}
if (!progressForm.IsDisposed && progressForm.Visible)
progressForm.Close();
// this make sure showDialog returns and the nested message loop is over
await dialogTask;
}
Related
Good day.
I'm having a problem exiting a task with the cancellation token.
My program freezes when I get to the token2.ThrowIfCancellationRequested();.
Following it with the breakpoints is shows that the token2 is cancelled, but the program doesn't revert back to the previous sub routine where I try and catch
try
{
Task.Run(() => SendData_DoWork(_tokenSource3));
}
catch (OperationCanceledException ex)
{
SetText("Communivation error with device");
SetText("");
}
finally
{
token.Dispose();
}
}//comms routine
//send Meter Address to communicate to meter
private void SendData_DoWork(CancellationTokenSource token)
{
var token2 = token.Token;
var _tokenSource4 = new CancellationTokenSource();
try
{
timer.Interval = 10000;
timer.Start();
timer.Elapsed += OnTimerElapsed;
NetworkStream stream = client.GetStream();
SerialConverter serialConverter = new SerialConverter();
Thread.Sleep(1000);
string newtext = null;
newtext = $"/?{address}!\r\n";
SetText("TX: " + newtext);
byte[] newData = stringSend(newtext);
stream.Write(newData, 0, newData.Length);
Thread.Sleep(50);
byte[] message = new byte[23];
int byteRead;
while (true)
{
byteRead = 0;
try
{
byteRead = stream.Read(message, 0, 23);
if (message[0] == (char)0x15)
{
token.Cancel();
}
}
catch
{
token.Cancel();
}
if ((byteRead == 0))
{
token.Cancel();
}
timer.Stop();
timer.Dispose();
ASCIIEncoding encoder = new ASCIIEncoding();
string newresponse = encoder.GetString(serialConverter.convertFromSerial(message));
SetText("RX: " + newresponse);
if (newresponse[0].ToString() == SOH)
{
token.Cancel();
}
if (newresponse != null)
{
/* NEXT SUB ROUTINE*/
}
else { break; }
}//while looop
}//try
catch (Exception ex)
{
token.Cancel();
}
if (token2.IsCancellationRequested)
{
timer.Stop();
timer.Dispose();
token2.ThrowIfCancellationRequested();
}
}//sendData subroutine
You are launching a Task, and ignoring the result; the only time Task.Run would throw is if the task-method is invalid, or enqueuing the operation itself failed. If you want to know how SendData_DoWork ended, you'll need to actually check the result of the task, by capturing the result of Task.Run and awaiting it (preferably asynchronously, although if we're talking async, SendData_DoWork should probably also be async and return a Task).
Your catch/finally will probably be exited long before SendData_DoWork even starts - again: Task.Run just takes the time required to validate and enqueue the operation; not wait for it to happen.
I think you have missunderstood how cancellation tokens are supposed to work. Your work method should take a CancellationToken, not a CancellationTokenSource. And it should call ThrowIfCancellationRequested inside the loop, not after. I would suspect that you would get some issues with multiple cancel calls to the same cancellation token.
Typically you would use a pattern something like like this:
public void MyCancelButtonHandler(...) => cts.Cancel();
public async void MyButtonHandler(...){
try{
cts = new CancellationTokenSource(); // update shared field
await Task.Run(() => MyBackgroundWork(cts.Token));
}
catch(OperationCancelledException){} // Ignore
catch(Exception){} // handle other exceptions
}
private void MyBackgroundWork(CancellationToken cancel){
while(...){
cancel.ThrowIfCancellationRequested();
// Do actual work
}
}
So in my particular case it seems like changing the sub-routines from private async void ... to private async Task fixes the particular issue that I'm having.
I have a WinForm, with a toolStripStatusLabel. There is a button, which spawns a new thread to perform its task. The status label needs to update during, and after this task is completed. The GUI elements are in the main thread. If I want to achieve this, can I place the relevant lines to update the label where the comments are below in the code snippet below? Also, I need to have another form open when this label is clicked. From my understanding of asynchronous coding, this should be straightforward, involving an event handler for the label, and the fact that control will return to the caller of the async method. Is this correct? I am relatively new to multithreaded and asynchronous programming, so I am quite confused.
// this is running in the main GUI thread
private async void Export_execute_Click(object sender, System.EventArgs args)
{
try
{
await System.Threading.Tasks.Task.Run(() => do_export(filename, classes, System.TimeZoneInfo.ConvertTimeToUtc(timestamp)));
// if this is successful, status label should be update (task successful)
}
catch (System.Exception e)
{
// status label should be updated (task failed)
}
}
If there is something literally awaitable in the Export method then I think to make it an async method would be better.
private async void Export_execute_Click(object sender, EventArgs e)
{
try
{
await ExportAsync("file1", "classA", DateTime.Now);
toolStripStatusLabel.Text = $"Export finished at {DateTime.Now}";
}
catch (Exception ex)
{
toolStripStatusLabel.Text = $"Export failed, {ex.ToString()}";
}
}
private async Task ExportAsync(string fileName, string classes, DateTime timestamp)
{
toolStripStatusLabel.Text = $"Export start at {timestamp}";
await Task.Delay(TimeSpan.FromSeconds(5));
toolStripStatusLabel.Text = $"Have first half done {timestamp}";
await Task.Delay(TimeSpan.FromSeconds(5));
}
private void toolStripStatusLabel_Click(object sender, EventArgs e)
{
Form2 frm2 = new Form2();
frm2.Show();
}
The standard way to report progress is to use the IProgress<T> interface. There is already an implementation of this interface that you can use (Progress<T>), and is generic so that you can supply any type of argument you want. In the example bellow the argument is a string. The key point is that the event Progress.ProgressChanged is running in the UI thread, so you don't have to worry about it.
// This will run in the UI thread
private async void Export_Execute_Click(object sender, EventArgs args)
{
try
{
var progress = new Progress<string>();
progress.ProgressChanged += ExportProgress_ProgressChanged;
// Task.Factory.StartNew allows to set advanced options
await Task.Factory.StartNew(() => Do_Export(filename, classes,
TimeZoneInfo.ConvertTimeToUtc(timestamp), progress),
CancellationToken.None, TaskCreationOptions.LongRunning,
TaskScheduler.Default);
toolStripStatusLabel.Text = $"Export completed successfully";
}
catch (Exception e)
{
toolStripStatusLabel.Text = $"Export failed: {e.Message}";
}
}
// This will run in the UI thread
private void ExportProgress_ProgressChanged(object sender, string e)
{
toolStripStatusLabel.Text = e;
}
// This will run in a dedicated background thread
private void Do_Export(string filename, string classes, DateTime timestamp,
IProgress<string> progress)
{
for (int i = 0; i < 100; i += 10)
{
progress?.Report($"Export {i}% percent done");
Thread.Sleep(1000);
}
}
How about a BackgroundWorker instead of your current Task? I prefer these because they allow easy communication between the main thread and the worker.
Note that Export_execute_Click is no longer marked as async in this scenario.
Example:
private void Export_execute_Click(object sender, System.EventArgs args) {
// Method level objects are accessible throughout this process
bool error = false;
// Process
BackgroundWorker worker = new BackgroundWorker {
WorkerReportsProgress = true
};
// This executes on main thread when a progress is reported
worker.ProgressChanged += (e, ea) => {
if (ea.UserState != null) {
// ea.UserState.ToString() contains the string progress message
}
};
// This executes as an async method on a background thread
worker.DoWork += (o, ea) => {
try {
var response = do_export(filename, classes, System.TimeZoneInfo.ConvertTimeToUtc(timestamp)));
if (response == whatever) {
worker.ReportProgress(0, "Response from do_export() was `whatever`");
} else {
worker.ReportProgress(0, "Response from do_export() was something bad");
error = true;
}
} catch (System.Exception e) {
worker.ReportProgress(0, $"do_export() failed: {e}");
}
};
// This executes on the main thread once the background worker has finished
worker.RunWorkerCompleted += async (o, ea) => {
// You can communicate with your UI normally again here
if (error) {
// You had an error -- the exception in DoWork() fired
} else {
// You're all set
}
// If you have a busy-indicator, here is the place to disable it
// ...
};
// I like to set a busy-indicator here, some sort of ajax-spinner type overlay in the main UI, indicating that the process is happening
// ...
// This executes the background worker, as outlined above
worker.RunWorkerAsync();
}
This question already has answers here:
Stop SQL query execution from .net Code
(3 answers)
Closed 8 years ago.
If you recommend use SqlCommand.Cancel(), please give a working example!
I need to show a wait form with a cancel button(if the user clicks this button, the query must stop running) during query execution.
My solution: (.net 4)
I pass to form constructor two parameters:
query execution task
cancelation action
Below is code of my loading form:
public partial class LoadingFrm : Form
{
//task for execute query
private Task execute;
//action for cancelling task
private Action cancel;
public LoadingFrm(Task e, Action c)
{
execute = e;
cancel = c;
InitializeComponent();
this.cancelBtn.Click += this.cancelBtn_Click;
this.FormBorderStyle = FormBorderStyle.None;
this.Load += (s, ea) =>
{
//start task
this.execute.Start();
//close form after execution of task
this.execute.ContinueWith((t) =>
{
if (this.InvokeRequired)
{
Invoke((MethodInvoker)this.Close);
}
else this.Close();
});
};
}
//event handler of cancel button
private void cancelBtn_Click(object sender, EventArgs e)
{
cancel();
}
}
Below code of my ExecuteHelper class:
public class ExecuteHelper
{
public static SqlDataReader Execute(SqlCommand command)
{
var cts = new CancellationTokenSource();
var cToken = cts.Token;
cToken.Register(() => { command.Cancel(); });
Task<SqlDataReader> executeQuery = new Task<SqlDataReader>(command.ExecuteReader, cToken);
//create a form with execute task and action for cancel task if user click on button
LoadingFrm _lFrm = new LoadingFrm(executeQuery, () => { cts.Cancel(); });
_lFrm.ShowDialog();
SqlDataReader r = null;
try
{
//(1) here
r = executeQuery.Result;
}
catch (AggregateException ae)
{
}
catch (Exception ex)
{
}
return r;
}
}
But I can't stop execution of SqlCommand. After some time method that call ExecuteHelper.Execute(command) get a result (SqlDataReader) from sql server with data? Why? Anybody can help? How i can cancel execution of sqlcommand?
And i have another question. Why if i click a cancel button of my form and cts.Cancel() was been called //(1) here i get executeQuery.IsCanceled = false although executeQuery.Status = Faulted.
Instead of calling ExecuteReader call ExecuteReaderAsync and pass in the CancellationToken.
If you are on .net 4.0 you can write your own ExecuteReaderAsync using a TaskCompletionSource. I haven't tested this code, but it should roughly be this:
public static class Extensions
{
public static Task<SqlDataReader> ExecuteReaderAsync(this SqlCommand command, CancellationToken token)
{
var tcs = new TaskCompletionSource<SqlDataReader>();
// whne the token is cancelled, cancel the command
token.Register( () =>
{
command.Cancel();
tcs.SetCanceled();
});
command.BeginExecuteReader( (r) =>
{
try
{
tcs.SetResult(command.EndExecuteReader(r));
}
catch(Exception ex)
{
tcs.SetException(ex);
}
}, null);
return tcs.Task;
}
}
You are using the SqlCommand.Cancel method to cancel any async operation in progress. Then you can use it like this:
public static SqlDataReader Execute(SqlCommand command)
{
SqlDataReader r = null;
var cts = new CancellationTokenSource();
var cToken = cts.Token;
var executeQuery = command.ExecuteReaderAsync(cToken).
.ContinueWith( t =>
{
if(t.IsCompleted)
{
r = t.Result;
}
}, TaskScheduler.Default);
//create a form with execute task and action for cancel task if user click on button
LoadingFrm _lFrm = new LoadingFrm(executeQuery, () => { cts.Cancel(); });
// Assuming this is blocking and that the executeQuery will have finished by then, otheriwse
// may need to call executeQuery.Wait().
_lFrm.ShowDialog();
return r;
}
I modified the Execute method to use ContunueWith rather than r.Result because Result is a blocking property and you'll not show the dialog until the query is complete.
As mentioned it is untested, but should be pretty close to what you need.
I am using this method to instantiate a web browser programmatically, navigate to a url and return a result when the document has completed.
How would I be able to stop the Task and have GetFinalUrl() return null if the document takes more than 5 seconds to load?
I have seen many examples using a TaskFactory but I haven't been able to apply it to this code.
private Uri GetFinalUrl(PortalMerchant portalMerchant)
{
SetBrowserFeatureControl();
Uri finalUri = null;
if (string.IsNullOrEmpty(portalMerchant.Url))
{
return null;
}
Uri trackingUrl = new Uri(portalMerchant.Url);
var task = MessageLoopWorker.Run(DoWorkAsync, trackingUrl);
task.Wait();
if (!String.IsNullOrEmpty(task.Result.ToString()))
{
return new Uri(task.Result.ToString());
}
else
{
throw new Exception("Parsing Failed");
}
}
// by Noseratio - http://stackoverflow.com/users/1768303/noseratio
static async Task<object> DoWorkAsync(object[] args)
{
_threadCount++;
Console.WriteLine("Thread count:" + _threadCount);
Uri retVal = null;
var wb = new WebBrowser();
wb.ScriptErrorsSuppressed = true;
TaskCompletionSource<bool> tcs = null;
WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) => tcs.TrySetResult(true);
foreach (var url in args)
{
tcs = new TaskCompletionSource<bool>();
wb.DocumentCompleted += documentCompletedHandler;
try
{
wb.Navigate(url.ToString());
await tcs.Task;
}
finally
{
wb.DocumentCompleted -= documentCompletedHandler;
}
retVal = wb.Url;
wb.Dispose();
return retVal;
}
return null;
}
public static class MessageLoopWorker
{
#region Public static methods
public static async Task<object> Run(Func<object[], Task<object>> worker, params object[] args)
{
var tcs = new TaskCompletionSource<object>();
var thread = new Thread(() =>
{
EventHandler idleHandler = null;
idleHandler = async (s, e) =>
{
// handle Application.Idle just once
Application.Idle -= idleHandler;
// return to the message loop
await Task.Yield();
// and continue asynchronously
// propogate the result or exception
try
{
var result = await worker(args);
tcs.SetResult(result);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
// signal to exit the message loop
// Application.Run will exit at this point
Application.ExitThread();
};
// 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();
});
// set STA model for the new thread
thread.SetApartmentState(ApartmentState.STA);
// start the thread and await for the task
thread.Start();
try
{
return await tcs.Task;
}
finally
{
thread.Join();
}
}
#endregion
}
Updated: the latest version of the WebBrowser-based console web scraper can be found on Github.
Updated: Adding a pool of WebBrowser objects for multiple parallel downloads.
Do you have an example of how to do this in a console app by any
chance? Also I don't think webBrowser can be a class variable because
I am running the whole thing in a parallell for each, iterating
thousands of URLs
Below is an implementation of more or less generic **WebBrowser-based web scraper **, which works as console application. It's a consolidation of some of my previous WebBrowser-related efforts, including the code referenced in the question:
Capturing an image of the web page with opacity
Loading a page with dynamic AJAX content
Creating an STA message loop thread for WebBrowser
Loading a set of URLs, one after another
Printing a set of URLs with WebBrowser
Web page UI automation
A few points:
Reusable MessageLoopApartment class is used to start and run a WinForms STA thread with its own message pump. It can be used from a console application, as below. This class exposes a TPL Task Scheduler (FromCurrentSynchronizationContext) and a set of Task.Factory.StartNew wrappers to use this task scheduler.
This makes async/await a great tool for running WebBrowser navigation tasks on that separate STA thread. This way, a WebBrowser object gets created, navigated and destroyed on that thread. Although, MessageLoopApartment is not tied up to WebBrowser specifically.
It's important to enable HTML5 rendering using Browser Feature
Control, as otherwise the WebBrowser obejcts runs in IE7 emulation mode by default.
That's what SetFeatureBrowserEmulation does below.
It may not always be possible to determine when a web page has finished rendering with 100% probability. Some pages are quite complex and use continuous AJAX updates. Yet we
can get quite close, by handling DocumentCompleted event first, then polling the page's current HTML snapshot for changes and checking the WebBrowser.IsBusy property. That's what NavigateAsync does below.
A time-out logic is present on top of the above, in case the page rendering is never-ending (note CancellationTokenSource and CreateLinkedTokenSource).
using Microsoft.Win32;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Console_22239357
{
class Program
{
// by Noseratio - https://stackoverflow.com/a/22262976/1768303
// main logic
static async Task ScrapeSitesAsync(string[] urls, CancellationToken token)
{
using (var apartment = new MessageLoopApartment())
{
// create WebBrowser inside MessageLoopApartment
var webBrowser = apartment.Invoke(() => new WebBrowser());
try
{
foreach (var url in urls)
{
Console.WriteLine("URL:\n" + url);
// cancel in 30s or when the main token is signalled
var navigationCts = CancellationTokenSource.CreateLinkedTokenSource(token);
navigationCts.CancelAfter((int)TimeSpan.FromSeconds(30).TotalMilliseconds);
var navigationToken = navigationCts.Token;
// run the navigation task inside MessageLoopApartment
string html = await apartment.Run(() =>
webBrowser.NavigateAsync(url, navigationToken), navigationToken);
Console.WriteLine("HTML:\n" + html);
}
}
finally
{
// dispose of WebBrowser inside MessageLoopApartment
apartment.Invoke(() => webBrowser.Dispose());
}
}
}
// entry point
static void Main(string[] args)
{
try
{
WebBrowserExt.SetFeatureBrowserEmulation(); // enable HTML5
var cts = new CancellationTokenSource((int)TimeSpan.FromMinutes(3).TotalMilliseconds);
var task = ScrapeSitesAsync(
new[] { "http://example.com", "http://example.org", "http://example.net" },
cts.Token);
task.Wait();
Console.WriteLine("Press Enter to exit...");
Console.ReadLine();
}
catch (Exception ex)
{
while (ex is AggregateException && ex.InnerException != null)
ex = ex.InnerException;
Console.WriteLine(ex.Message);
Environment.Exit(-1);
}
}
}
/// <summary>
/// WebBrowserExt - WebBrowser extensions
/// by Noseratio - https://stackoverflow.com/a/22262976/1768303
/// </summary>
public static class WebBrowserExt
{
const int POLL_DELAY = 500;
// navigate and download
public static async Task<string> NavigateAsync(this WebBrowser webBrowser, string url, CancellationToken token)
{
// navigate and await DocumentCompleted
var tcs = new TaskCompletionSource<bool>();
WebBrowserDocumentCompletedEventHandler handler = (s, arg) =>
tcs.TrySetResult(true);
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
{
webBrowser.DocumentCompleted += handler;
try
{
webBrowser.Navigate(url);
await tcs.Task; // wait for DocumentCompleted
}
finally
{
webBrowser.DocumentCompleted -= handler;
}
}
// get the root element
var documentElement = webBrowser.Document.GetElementsByTagName("html")[0];
// poll the current HTML for changes asynchronosly
var html = documentElement.OuterHtml;
while (true)
{
// wait asynchronously, this will throw if cancellation requested
await Task.Delay(POLL_DELAY, token);
// continue polling if the WebBrowser is still busy
if (webBrowser.IsBusy)
continue;
var htmlNow = documentElement.OuterHtml;
if (html == htmlNow)
break; // no changes detected, end the poll loop
html = htmlNow;
}
// consider the page fully rendered
token.ThrowIfCancellationRequested();
return html;
}
// enable HTML5 (assuming we're running IE10+)
// more info: https://stackoverflow.com/a/18333982/1768303
public static void SetFeatureBrowserEmulation()
{
if (System.ComponentModel.LicenseManager.UsageMode != System.ComponentModel.LicenseUsageMode.Runtime)
return;
var appName = System.IO.Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
Registry.SetValue(#"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION",
appName, 10000, RegistryValueKind.DWord);
}
}
/// <summary>
/// MessageLoopApartment
/// STA thread with message pump for serial execution of tasks
/// by Noseratio - https://stackoverflow.com/a/22262976/1768303
/// </summary>
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();
}
}
}
I suspect running a processing loop on another thread will not work out well, since WebBrowser is a UI component that hosts an ActiveX control.
When you're writing TAP over EAP wrappers, I recommend using extension methods to keep the code clean:
public static Task<string> NavigateAsync(this WebBrowser #this, string url)
{
var tcs = new TaskCompletionSource<string>();
WebBrowserDocumentCompletedEventHandler subscription = null;
subscription = (_, args) =>
{
#this.DocumentCompleted -= subscription;
tcs.TrySetResult(args.Url.ToString());
};
#this.DocumentCompleted += subscription;
#this.Navigate(url);
return tcs.Task;
}
Now your code can easily apply a timeout:
async Task<string> GetUrlAsync(string url)
{
using (var wb = new WebBrowser())
{
var navigate = wb.NavigateAsync(url);
var timeout = Task.Delay(TimeSpan.FromSeconds(5));
var completed = await Task.WhenAny(navigate, timeout);
if (completed == navigate)
return await navigate;
return null;
}
}
which can be consumed as such:
private async Task<Uri> GetFinalUrlAsync(PortalMerchant portalMerchant)
{
SetBrowserFeatureControl();
if (string.IsNullOrEmpty(portalMerchant.Url))
return null;
var result = await GetUrlAsync(portalMerchant.Url);
if (!String.IsNullOrEmpty(result))
return new Uri(result);
throw new Exception("Parsing Failed");
}
I'm trying to take benefit from Noseratio's solution as well as following advices from Stephen Cleary.
Here is the code I updated to include in the code from Stephen the code from Noseratio regarding the AJAX tip.
First part: the Task NavigateAsync advised by Stephen
public static Task<string> NavigateAsync(this WebBrowser #this, string url)
{
var tcs = new TaskCompletionSource<string>();
WebBrowserDocumentCompletedEventHandler subscription = null;
subscription = (_, args) =>
{
#this.DocumentCompleted -= subscription;
tcs.TrySetResult(args.Url.ToString());
};
#this.DocumentCompleted += subscription;
#this.Navigate(url);
return tcs.Task;
}
Second part: a new Task NavAjaxAsync to run the tip for AJAX (based on Noseratio's code)
public static async Task<string> NavAjaxAsync(this WebBrowser #this)
{
// get the root element
var documentElement = #this.Document.GetElementsByTagName("html")[0];
// poll the current HTML for changes asynchronosly
var html = documentElement.OuterHtml;
while (true)
{
// wait asynchronously
await Task.Delay(POLL_DELAY);
// continue polling if the WebBrowser is still busy
if (webBrowser.IsBusy)
continue;
var htmlNow = documentElement.OuterHtml;
if (html == htmlNow)
break; // no changes detected, end the poll loop
html = htmlNow;
}
return #this.Document.Url.ToString();
}
Third part: a new Task NavAndAjaxAsync to get the navigation and the AJAX
public static async Task NavAndAjaxAsync(this WebBrowser #this, string url)
{
await #this.NavigateAsync(url);
await #this.NavAjaxAsync();
}
Fourth and last part: the updated Task GetUrlAsync from Stephen with Noseratio's code for AJAX
async Task<string> GetUrlAsync(string url)
{
using (var wb = new WebBrowser())
{
var navigate = wb.NavAndAjaxAsync(url);
var timeout = Task.Delay(TimeSpan.FromSeconds(5));
var completed = await Task.WhenAny(navigate, timeout);
if (completed == navigate)
return await navigate;
return null;
}
}
I'd like to know if this is the right approach.
I have the async code that implements cancellation token. It's working but Im not pretty sure if this is the right way to do it so I just want feedback about it.
Here is the actual code:
/// <summary>
///
/// </summary>
private async void SaveData() {
if (GetActiveServiceRequest() != null)
{
var tokenSource = new System.Threading.CancellationTokenSource();
this.ShowWizardPleaseWait("Saving data...");
var someTask = System.Threading.Tasks.Task<bool>.Factory.StartNew(() =>
{
bool returnVal = false;
// Set sleep of 7 seconds to test the 5 seconds timeout.
System.Threading.Thread.Sleep(7000);
if (!tokenSource.IsCancellationRequested)
{
// if not cancelled then save data
App.Data.EmployeeWCF ws = new App.Data.EmployeeWCF ();
returnVal = ws.UpdateData(_employee.Data);
ws.Dispose();
}
return returnVal;
}, tokenSource.Token);
if (await System.Threading.Tasks.Task.WhenAny(someTask, System.Threading.Tasks.Task.Delay(5000)) == someTask)
{
// Completed
this.HideWizardPleaseWait();
if (someTask.Result)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
else
{
this.DialogResult = System.Windows.Forms.DialogResult.Abort;
}
btnOK.Enabled = true;
this.Close();
}
else
{
tokenSource.Cancel();
// Timeout logic
this.HideWizardPleaseWait();
MessageBox.Show("Timeout. Please try again.")
}
}
}
Does async / await / cancellation code is well implemented?
Thanks and appreciate the feedback.
In general, you should use ThrowIfCancellationRequested. That will complete the returned Task in a canceled state, rather than in a "ran to completion successfully" state with a false result.
Other points:
Avoid async void. This should be async Task unless it's an event handler.
Prefer Task.Run over TaskFactory.StartNew.
Use using.
If you're just using CancellationTokenSource as a timeout, then it has special capabilities for that. Creating a separate task via Task.Delay and Task.WhenAny isn't necessary.
Here's what the updated code would look like:
private async Task SaveData()
{
if (GetActiveServiceRequest() != null)
{
var tokenSource = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5));
var token = tokenSource.Token;
this.ShowWizardPleaseWait("Saving data...");
var someTask = System.Threading.Tasks.Task.Run(() =>
{
// Set sleep of 7 seconds to test the 5 seconds timeout.
System.Threading.Thread.Sleep(7000);
// if not cancelled then save data
token.ThrowIfCancellationRequested();
using (App.Data.EmployeeWCF ws = new App.Data.EmployeeWCF())
{
return ws.UpdateData(_employee.Data);
}
}, token);
try
{
var result = await someTask;
// Completed
this.HideWizardPleaseWait();
if (result)
{
this.DialogResult = System.Windows.Forms.DialogResult.OK;
}
else
{
this.DialogResult = System.Windows.Forms.DialogResult.Abort;
}
btnOK.Enabled = true;
this.Close();
}
catch (OperationCanceledException)
{
// Timeout logic
this.HideWizardPleaseWait();
MessageBox.Show("Timeout. Please try again.")
}
}
}