I have a WPF application running a background task which uses async/await. The task is updating the app's status UI as it progresses. During the process, if a certain condition has been met, I am required to show a modal window to make the user aware of such event, and then continue processing, now also updating the status UI of that modal window.
This is a sketch version of what I am trying to achieve:
async Task AsyncWork(int n, CancellationToken token)
{
// prepare the modal UI window
var modalUI = new Window();
modalUI.Width = 300; modalUI.Height = 200;
modalUI.Content = new TextBox();
using (var client = new HttpClient())
{
// main loop
for (var i = 0; i < n; i++)
{
token.ThrowIfCancellationRequested();
// do the next step of async process
var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);
// update the main window status
var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
((TextBox)this.Content).AppendText(info);
// show the modal UI if the data size is more than 42000 bytes (for example)
if (data.Length < 42000)
{
if (!modalUI.IsVisible)
{
// show the modal UI window
modalUI.ShowDialog();
// I want to continue while the modal UI is still visible
}
}
// update modal window status, if visible
if (modalUI.IsVisible)
((TextBox)modalUI.Content).AppendText(info);
}
}
}
The problem with modalUI.ShowDialog() is that it is a blocking call, so the processing stops until the dialog is closed. It would not be a problem if the window was modeless, but it has to be modal, as dictated by the project requirements.
Is there a way to get around this with async/await?
This can be achieved by executing modalUI.ShowDialog() asynchronously (upon a future iteration of the UI thread's message loop). The following implementation of ShowDialogAsync does that by using TaskCompletionSource (EAP task pattern) and SynchronizationContext.Post.
Such execution workflow might be a bit tricky to understand, because your asynchronous task is now spread across two separate WPF message loops: the main thread's one and the new nested one (started by ShowDialog). IMO, that's perfectly fine, we're just taking advantage of the async/await state machine provided by C# compiler.
Although, when your task comes to the end while the modal window is still open, you probably want to wait for user to close it. That's what CloseDialogAsync does below. Also, you probably should account for the case when user closes the dialog in the middle of the task (AFAIK, a WPF window can't be reused for multiple ShowDialog calls).
The following code works for me:
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace WpfAsyncApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Content = new TextBox();
this.Loaded += MainWindow_Loaded;
}
// AsyncWork
async Task AsyncWork(int n, CancellationToken token)
{
// prepare the modal UI window
var modalUI = new Window();
modalUI.Width = 300; modalUI.Height = 200;
modalUI.Content = new TextBox();
try
{
using (var client = new HttpClient())
{
// main loop
for (var i = 0; i < n; i++)
{
token.ThrowIfCancellationRequested();
// do the next step of async process
var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);
// update the main window status
var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
((TextBox)this.Content).AppendText(info);
// show the modal UI if the data size is more than 42000 bytes (for example)
if (data.Length < 42000)
{
if (!modalUI.IsVisible)
{
// show the modal UI window asynchronously
await ShowDialogAsync(modalUI, token);
// continue while the modal UI is still visible
}
}
// update modal window status, if visible
if (modalUI.IsVisible)
((TextBox)modalUI.Content).AppendText(info);
}
}
// wait for the user to close the dialog (if open)
if (modalUI.IsVisible)
await CloseDialogAsync(modalUI, token);
}
finally
{
// always close the window
modalUI.Close();
}
}
// show a modal dialog asynchronously
static async Task ShowDialogAsync(Window window, CancellationToken token)
{
var tcs = new TaskCompletionSource<bool>();
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
{
RoutedEventHandler loadedHandler = (s, e) =>
tcs.TrySetResult(true);
window.Loaded += loadedHandler;
try
{
// show the dialog asynchronously
// (presumably on the next iteration of the message loop)
SynchronizationContext.Current.Post((_) =>
window.ShowDialog(), null);
await tcs.Task;
Debug.Print("after await tcs.Task");
}
finally
{
window.Loaded -= loadedHandler;
}
}
}
// async wait for a dialog to get closed
static async Task CloseDialogAsync(Window window, CancellationToken token)
{
var tcs = new TaskCompletionSource<bool>();
using (token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: true))
{
EventHandler closedHandler = (s, e) =>
tcs.TrySetResult(true);
window.Closed += closedHandler;
try
{
await tcs.Task;
}
finally
{
window.Closed -= closedHandler;
}
}
}
// main window load event handler
async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var cts = new CancellationTokenSource(30000);
try
{
// test AsyncWork
await AsyncWork(10, cts.Token);
MessageBox.Show("Success!");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}
}
[EDITED] Below is a slightly different approach which uses Task.Factory.StartNew to invoke modalUI.ShowDialog() asynchronously. The returned Task can be awaited later to make sure the user has closed the modal dialog.
async Task AsyncWork(int n, CancellationToken token)
{
// prepare the modal UI window
var modalUI = new Window();
modalUI.Width = 300; modalUI.Height = 200;
modalUI.Content = new TextBox();
Task modalUITask = null;
try
{
using (var client = new HttpClient())
{
// main loop
for (var i = 0; i < n; i++)
{
token.ThrowIfCancellationRequested();
// do the next step of async process
var data = await client.GetStringAsync("http://www.bing.com/search?q=item" + i);
// update the main window status
var info = "#" + i + ", size: " + data.Length + Environment.NewLine;
((TextBox)this.Content).AppendText(info);
// show the modal UI if the data size is more than 42000 bytes (for example)
if (data.Length < 42000)
{
if (modalUITask == null)
{
// invoke modalUI.ShowDialog() asynchronously
modalUITask = Task.Factory.StartNew(
() => modalUI.ShowDialog(),
token,
TaskCreationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
// continue after modalUI.Loaded event
var modalUIReadyTcs = new TaskCompletionSource<bool>();
using (token.Register(() =>
modalUIReadyTcs.TrySetCanceled(), useSynchronizationContext: true))
{
modalUI.Loaded += (s, e) =>
modalUIReadyTcs.TrySetResult(true);
await modalUIReadyTcs.Task;
}
}
}
// update modal window status, if visible
if (modalUI.IsVisible)
((TextBox)modalUI.Content).AppendText(info);
}
}
// wait for the user to close the dialog (if open)
if (modalUITask != null)
await modalUITask;
}
finally
{
// always close the window
modalUI.Close();
}
}
Related
I have a button click event handler in which I need to have 3 sec delay to make some flag true ..so it takes time to completely execute the function now meantime if the user click on the button again then this is making flag true for second click also...so I want to cancel the first click event as soon as I receive another click request.
This is my code :
private async void ClickEventHandler(ClickEvent obj)
{
int indexOfSelectedItem = this.List.IndexOf(this.List.FirstOrDefault(x => Convert.ToDouble(x.Value) == obj.Value));
if (indexOfSelectedItem > -1)
{
for (int i = 0; i < indexOfSelectedItem; i++)
{
var item = this.List.ElementAtOrDefault(0);
this.List.RemoveAt(0);
this.List.Add(item);
}
this.IsScrollEnabled = false;
await Task.Delay(3000);
this.IsScrollEnabled = true;
}
}
Yes I need to cancel the execution of the first method and call the method again ..so that it will wait for 3 sec after clicking it on second time
A simple example with a cancellation token:
private CancellationTokenSource tokenSource = new();
private async void ClickEventHandler(ClickEvent obj)
{
// Since this method is called from the clicker,
// it always starts on the main thread. Therefore,
// there is no need for additional Thread-Safe.
tokenSource.Cancel();
tokenSource = new();
CancellationToken token = tokenSource.Token;
int indexOfSelectedItem = this.List.IndexOf(this.List.FirstOrDefault(x => Convert.ToDouble(x.Value) == obj.Value));
if (indexOfSelectedItem > -1)
{
for (int i = 0; i < indexOfSelectedItem; i++)
{
var item = this.List.ElementAtOrDefault(0);
this.List.RemoveAt(0);
this.List.Add(item);
}
this.IsScrollEnabled = false;
try
{
// If the cancellation is during the Delay, then an exception will be exited.
await Task.Delay(3000, token);
this.IsScrollEnabled = true;
}
catch (Exception)
{
// Here, if necessary, actions in case the method is canceled.
}
}
}
P.S. In the example, the token is checked only in the Delay(...) method. If you need to check somewhere else, then insert a call to the method token.ThrowIfCancellationRequested(); into this place.
you could change a variable when the button gets clicked and change it back after it is done and then add an if statement checking the variable
As like xceing said , you can have a variable to check if already clicked and yet to complete the process.
Here is a sample
//keep a variable to check already clicked and yet to complete the action
bool clickEventInProgress = false;
private async void ClickEventHandler(ClickEvent obj)
{
//check it before start processing click action
if (!clickEventInProgress)
{
clickEventInProgress = true;
int indexOfSelectedItem = this.List.IndexOf(this.List.FirstOrDefault(x => Convert.ToDouble(x.Value) == obj.Value));
if (indexOfSelectedItem > -1)
{
for (int i = 0; i < indexOfSelectedItem; i++)
{
var item = this.List.ElementAtOrDefault(0);
this.List.RemoveAt(0);
this.List.Add(item);
}
this.IsScrollEnabled = false;
await Task.Delay(3000);
this.IsScrollEnabled = true;
}
clickEventInProgress = false;
}
}
you can make the variable false, one the process completed . So that next click operation will work fine.
It's not clear what exactly you are doing. A general solution could execute the cancellable task using Task.Run and then cancel it using a CancellationTokenSource. It's important to pass the associated CancellationToken to the Task API (and any asynchronous API that supports cancellation in general) too in order to enable full cancellation support e.g. cancellation of Task.Delay:
MainWindow.xaml
<Window>
<Button Content="Go!"
Click="OnClick" />
</Window>
MainWindow.xaml.cs
partial class MainWindow : Window
{
private CancellationTokenSource CancellationTokenSource { get; set; }
private SemaphoreSlim Semaphore { get; } = new SemaphoreSlim(1, 1);
public MainWindow() => InitializeComponent();
private async void OnClick(object sender, RoutedEventArgs e)
{
// If there is nothing to cancel, the reference is NULL
this.CancellationTokenSource?.Cancel();
// Wait for the previous operation to be cancelled.
// If there is nothing to cancel the SemaphoreSlim has a free slot
// and the execution continues.
await this.Semaphore.WaitAsync();
try
{
using (this.CancellationTokenSource = new CancellationTokenSource())
{
await RunCancellableOperationAsync(this.CancellationTokenSource.Token);
}
}
catch (OperationCanceledException)
{
// Invalidate disposed object to make it unusable
this.CancellationTokenSource = null;
}
finally // Cancellation completed
{
this.Semaphore.Release();
}
}
private async Task RunCancellableOperationAsync(CancellationToken cancellationToken)
{
// Execute blocking code concurrently to enable cancellation
await Task.Run(() =>
{
for (int index = 0; index < 1000; index++)
{
// Abort the iteration if requested
cancellationToken.ThrowIfCancellationRequested();
// Simulate do something
Thread.Sleep(5000);
}
}, cancellationToken);
// Support cancellation of the delay
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
}
}
I have the following api call in my MainActivity.cs -
public async Task DoSomething()
{
var progressDialog = ShowLoading(_instance);
await Task.Factory.StartNew(() =>
{
var objectResponse = string.Empty;
string _apiUrl = "https://my-web-service-endpoint";
var request = (HttpWebRequest)WebRequest.Create(_apiUrl);
request.Method = "GET";
using (var response = _request.HttpWebResponse(request))
{
objectResponse = _request.HttpWebResponseBody(response);
}
progressDialog.Dismiss();
});
}
I am calling this method from a button click in SliderControl.cs -
public void loadTestView(View view)
{
Button btnDoSomething = view.FindViewById<Button>(Resource.Id.btnDoSomething);
if (btnDoSomething != null)
{
btnDoSomething .Click += async (sender, e) =>
{
await MainActivity.GetInstance().DoSomething();
};
}
}
However loadTestView is not called in the MainActivity.cs rather it is called when a used swipes onto a specific view in the application and handled in the SliderControl.cs , it is set like -
public override Java.Lang.Object InstantiateItem(ViewGroup container, int position)
{
var selectedView = Resource.Layout.Locator;
if (position == 0)
{
selectedView = Resource.Layout.Main;
}
if (position == 1)
{
selectedView = Resource.Layout.TestView;
}
View view = LayoutInflater.From(container.Context).Inflate(selectedView, container, false);
container.AddView(view);
if (position == 0)
{
loadMainView(view);
}
else if (position == 1)
{
loadTestView(view);
}
return view;
}
When I set a different buttons click event logic in the MainActivity.cs everything working accordingly, however when setting the button click login as it is above, I am met with the error -
Only the original thread that created a view hierarchy can touch its views.
The odd thing is the loader does actually come into view, however when I step into the await Task.Factory.StartNew(() => line the error hits.
I have tried changing the line to use Task.Run instead but same error, and also found some info on Device.BeginInvokeOnMainThread hwoever believe this is for Xamarin Forms. How can I get this to work?
You should use the UI Thread to update the UI / or any task that involve the UI:
Xamarin Forms Example:
Device.BeginInvokeOnMainThread (() => {
label.Text = "Async operation completed";
});
John wrote the great examples describing how to use Async/Await pattern. You may want to read more here https://johnthiriet.com/configure-await/#
Update: For Xamarin Native -
Problem:
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
lblTest.Text = "updated in thread";
// Doesn't work because you can't modify UILabel on background thread!
})).Start();
Solution:
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
InvokeOnMainThread (() => {
label1.Text = "updated in thread";
// this works! We are invoking in Main thread.
});
})).Start();
this program reads a list of web site then saves them.
i found it runs good for the first 2 url requests. then goes very slow (about 5 min per request)
the time spend on row 1 and row 2 are only 2 second.
Then all other will be about 5 min each.
When i debug , i see it actually tooks long in wb.Navigate(url.ToString());
public static async Task<bool> test()
{
long totalCnt = rows.Count();
long procCnt = 0;
foreach (string url in rows)
{
procCnt++;
string webStr = load_WebStr(url).Result;
Console.WriteLine(DateTime.Now+ "["+procCnt + "/" + totalCnt+"] "+url);
}
return true;
}
public static async Task<string> load_WebStr(string url)
{
var tcs = new TaskCompletionSource<string>();
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 webBrowser_Async(url);
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();
}
}
public static async Task<string> webBrowser_Async(string url)
{
string result = "";
using (var wb = new WebBrowser())
{
wb.ScriptErrorsSuppressed = true;
TaskCompletionSource<bool> tcs = null;
WebBrowserDocumentCompletedEventHandler documentCompletedHandler = (s, e) =>
tcs.TrySetResult(true);
tcs = new TaskCompletionSource<bool>();
wb.DocumentCompleted += documentCompletedHandler;
try
{
wb.Navigate(url.ToString());
// await for DocumentCompleted
await tcs.Task;
}
catch
{
Console.WriteLine("BUG!");
}
finally
{
wb.DocumentCompleted -= documentCompletedHandler;
}
// the DOM is ready
result = wb.DocumentText;
}
return result;
}
I recognize a slightly modified version of the code I used to answer quite a few WebBrowser-related questions. Was it this one? It's always a good idea to include a link to the original source.
Anyhow, the major problem in how you're using it here is perhaps the fact that you create and destroy an instance of WebBrowser control for every URL from your list.
Instead, you should be re-using a single instance of WebBrowser (or a pool of WebBrowser objects). You can find both versions here.
I am trying to update a progressbar in a multithreaded environment. I know that a lot of questions already treat that question but none of the proposed solution have worked for me.
Here is the backbone of my code :
public static void DO_Computation(//parameters) {
//Intialisation of parameters
Parallel.For(struct initialisation with local data) {
//business logic
//Call to update_progressbar (located in an another class, as the DO_Computation function is in Computation.cs class (not deriving from Form).
WinForm.Invoke((Action)delegate {Update_Progress_Bar(i);}); //WinForm is a class that exposes the progressbar.
}
}
This is not working (the progressbar is freezing when arriving at 100%, which is normal (we can refer to the microsoft article in this matter (indeed, this is not a thread-safe operating method)).
The Microsoft site stiplates to wrap the Parallel.For loop into a Task routine as follows:
public static void DO_Computation(//parameters) {
//Intialisation of parameters
Task.Factory.StartNew(() =>
{
Parallel.For(struct initialosation with local data) {
//business logic
//Call to update_progressbar (ocated in an another class, as the DO_Computation function is in Computation.cs class (not deriving from Form).
WinForm.Invoke((Action)delegate {Update_Progress_Bar(i);}); //WinForm is a class that exposes the progressbar.
..
}
});
});
However this is not working as well, when debugging the thread is getting out of the Task scope directly.
EDIT 2:
Basically, my problem is divided in 3 parts: Computation.cs (where DO_Computation is exposed), WinForm which is the form containing the progress bar, and MainWindow which is the form that contains the button which when clicked opens the form with the progress bar.
I do not clearly understand what is the use of "Task" in this case.
Because it is going out of the Task scope without performing any Parallel.For work
Any ideas?
Many Thanks,
EDIT 3:
I upgraded my code with the help of Noseratio (thans a lot to him). However I have the same problem which is the code inside task is never executed. My code now looks like :
DoComputation method
//Some Initilasations here
Action enableUI = () =>
{
frmWinProg.SetProgressText("Grading Transaction...");
frmWinProg.ChangeVisibleIteration(true);
};
Action<Exception> handleError = (ex) =>
{
// error reporting
MessageBox.Show(ex.Message);
};
var cts = new CancellationTokenSource();
var token = cts.Token;
Action cancel_work = () =>
{
frmWinProg.CancelTransaction();
cts.Cancel();
};
var syncConext = SynchronizationContext.Current;
Action<int> progressReport = (i) =>
syncConext.Post(_ => frmWinProg.SetIteration(i,GrpModel2F.NumOfSim, true), null);
var task = Task.Factory.StartNew(() =>
{
ParallelLoopResult res = Parallel.For<LocalDataStruct>(1,NbSim, options,
() => new DataStruct(//Hold LocalData for each thread),
(iSim, loopState, DataStruct) =>
//Business Logic
if (token.IsCancellationRequested)
{
loopState.Stop();
}
progressReport(iSim);
//Business Logic
return DataStruct;
},
(DataStruct) =>
//Assiginig Results;
});//Parallel.For end
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
task.ContinueWith(_ =>
{
try
{
task.Wait();
}
catch (Exception ex)
{
while (ex is AggregateException && ex.InnerException != null)
ex = ex.InnerException;
handleError(ex);
}
enableUI();
}, TaskScheduler.FromCurrentSynchronizationContext
());
Note that the Do_Computation function is itself called from a Form that runs a BackGroundWorker on it.
Use async/await, Progress<T> and observe cancellation with CancellationTokenSource.
A good read, related: "Async in 4.5: Enabling Progress and Cancellation in Async APIs".
If you need to target .NET 4.0 but develop with VS2012+ , you still can use async/await, Microsoft provides the Microsoft.Bcl.Async library for that.
I've put together a WinForms example illustrating all of the above. It also shows how to observe cancellation for Parallel.For loop, using ParallelLoopState.Stop():
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication_22487698
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
IEnumerable<int> _data = Enumerable.Range(1, 100);
Action _cancelWork;
private void DoWorkItem(
int[] data,
int item,
CancellationToken token,
IProgress<int> progressReport,
ParallelLoopState loopState)
{
// observe cancellation
if (token.IsCancellationRequested)
{
loopState.Stop();
return;
}
// simulate a work item
Thread.Sleep(500);
// update progress
progressReport.Report(item);
}
private async void startButton_Click(object sender, EventArgs e)
{
// update the UI
this.startButton.Enabled = false;
this.stopButton.Enabled = true;
try
{
// prepare to handle cancellation
var cts = new CancellationTokenSource();
var token = cts.Token;
this._cancelWork = () =>
{
this.stopButton.Enabled = false;
cts.Cancel();
};
var data = _data.ToArray();
var total = data.Length;
// prepare the progress updates
this.progressBar.Value = 0;
this.progressBar.Minimum = 0;
this.progressBar.Maximum = total;
var progressReport = new Progress<int>((i) =>
{
this.progressBar.Increment(1);
});
// offload Parallel.For from the UI thread
// as a long-running operation
await Task.Factory.StartNew(() =>
{
Parallel.For(0, total, (item, loopState) =>
DoWorkItem(data, item, token, progressReport, loopState));
// observe cancellation
token.ThrowIfCancellationRequested();
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
// update the UI
this.startButton.Enabled = true;
this.stopButton.Enabled = false;
this._cancelWork = null;
}
private void stopButton_Click(object sender, EventArgs e)
{
if (this._cancelWork != null)
this._cancelWork();
}
}
}
Updated, here's how to do the same without async/await:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication_22487698
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
IEnumerable<int> _data = Enumerable.Range(1, 100);
Action _cancelWork;
private void DoWorkItem(
int[] data,
int item,
CancellationToken token,
Action<int> progressReport,
ParallelLoopState loopState)
{
// observe cancellation
if (token.IsCancellationRequested)
{
loopState.Stop();
return;
}
// simulate a work item
Thread.Sleep(500);
// update progress
progressReport(item);
}
private void startButton_Click(object sender, EventArgs e)
{
// update the UI
this.startButton.Enabled = false;
this.stopButton.Enabled = true;
Action enableUI = () =>
{
// update the UI
this.startButton.Enabled = true;
this.stopButton.Enabled = false;
this._cancelWork = null;
};
Action<Exception> handleError = (ex) =>
{
// error reporting
MessageBox.Show(ex.Message);
};
try
{
// prepare to handle cancellation
var cts = new CancellationTokenSource();
var token = cts.Token;
this._cancelWork = () =>
{
this.stopButton.Enabled = false;
cts.Cancel();
};
var data = _data.ToArray();
var total = data.Length;
// prepare the progress updates
this.progressBar.Value = 0;
this.progressBar.Minimum = 0;
this.progressBar.Maximum = total;
var syncConext = SynchronizationContext.Current;
Action<int> progressReport = (i) =>
syncConext.Post(_ => this.progressBar.Increment(1), null);
// offload Parallel.For from the UI thread
// as a long-running operation
var task = Task.Factory.StartNew(() =>
{
Parallel.For(0, total, (item, loopState) =>
DoWorkItem(data, item, token, progressReport, loopState));
// observe cancellation
token.ThrowIfCancellationRequested();
}, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
task.ContinueWith(_ =>
{
try
{
task.Wait(); // rethrow any error
}
catch (Exception ex)
{
while (ex is AggregateException && ex.InnerException != null)
ex = ex.InnerException;
handleError(ex);
}
enableUI();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
catch (Exception ex)
{
handleError(ex);
enableUI();
}
}
private void stopButton_Click(object sender, EventArgs e)
{
if (this._cancelWork != null)
this._cancelWork();
}
}
}
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;
}