0

I'm trying to construct a progress/cancel form for use within my WinForms application that runs any await-able "operation", while providing the user with some progress information and an opportunity to cancel the operation.

Because the form is shown using ShowDialog(), it's a modal form which nicely disables the form underneath - so I don't need to mess around with disabling all the controls on that other form.

They way I've implemented it, which I fully expect you to rip to shreds :-), is to await the result of the operation during the Form.Load event handler, and then close the form once the operation is either completed (whether that be because it ran to completion, was cancelled, or raise an exception).

public partial class ProgressForm<T> : Form { private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private Progress<string> _progress = new Progress<string>(); private Func<IProgress<string>, CancellationToken, Task<T>> _operation = null; private Exception _exception = null; private T _result = default(T); public static T Execute(Func<IProgress<string>, CancellationToken, Task<T>> operation) { using (var progressForm = new ProgressForm<T>()) { progressForm._operation = operation; progressForm.ShowDialog(); if (progressForm._exception != null) throw progressForm._exception; else return progressForm._result; } } public ProgressForm() { InitializeComponent(); this._progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString()); } private async void ProgressForm_Load(object sender, EventArgs e) { try { this._result = await this._operation(this._progress, this._cancellationTokenSource.Token); } catch (Exception ex) // Includes OperationCancelledException { this._exception = ex; } this.Close(); } private void CancelXButton_Click(object sender, EventArgs e) { if (this._cancellationTokenSource != null) this._cancellationTokenSource.Cancel(); } } 

This is called like this:

int numberOfWidgets = ProgressForm<int>.Execute(CountWidgets); 

...where CountWidgets() is an await-able thing (in this case a function returning Task<int>, with appropriate IProgress and CancellationToken parameters).

So far it works pretty well, but there's one "feature" that I'd like to add. Ideally, I'd like the form to remain invisible for (say) one second, so that if the operation completes really quickly there's no "flicker" as the form is shown and then immediately hidden again.

So, my question is how to introduce the 1s delay before the form is shown. Obviously, I still want to start the operation immediately, and yet as soon as I "await" the result of the operation, I'm no longer in control (so to speak) because control will be returned to the caller of the Form.Load event handler - which will continue the work of showing the form.

I suspect that essentially I really need a second thread, and that I need the operation to execute on that thread while I block the main UI thread. (I know that blocking the UI thread is frowned upon, but in this case I think it's actually what I need).

There are so many different ways of creating threads, etc. that I'm not sure how to do this in the new "async/await" world...

8
  • It's not really clear how it's all working - we don't have enough context of where the code you've shown fits into the rest of your code. Commented Sep 20, 2012 at 11:37
  • @JonSkeet: Full code for the form added. Commented Sep 20, 2012 at 11:51
  • So, you want 1 s delay, and what if the whole process takes, for example, 1.5 seconds? The same problem remains. You need to think about something else. Commented Sep 20, 2012 at 12:01
  • I would add this delay after the process finished, before closing the dialog, if execution time was too short. Commented Sep 20, 2012 at 12:04
  • 1
    We did this by using a singleton. So a calling display the progress form set's a timer up. Stoped dislaying the progress kills the form or the timer. Commented Sep 20, 2012 at 12:29

2 Answers 2

1

I think you'll have to separate out your "task runner" from your "dialog" in order to do this. First, a dialog that responds to progress and can issue a cancel:

public partial class ProgressForm : Form { private readonly CancellationTokenSource _cancellationTokenSource; public ProgressForm(CancellationTokenSource cancellationTokenSource, IProgress<string> progress) { InitializeComponent(); _cancellationTokenSource = cancellationTokenSource; progress.ProgressChanged += ((o, i) => this.ProgressLabel.Text = i.ToString()); } public static void ShowDialog(CancellationTokenSource cancellationTokenSource, IProgress<string> progress) { using (var progressForm = new ProgressForm(cancellationTokenSource, progress)) { progressForm.ShowDialog(); } } private void CancelXButton_Click(object sender, EventArgs e) { if (this._cancellationTokenSource != null) this._cancellationTokenSource.Cancel(); } } 

Next, the actual "task runner":

public static class FriendlyTaskRunner { public static async Task<T> Execute<T>(Func<CancellationToken, IProgress<string>, Task<T>> operation) { var cancellationTokenSource = new CancellationTokenSource(); var progress = new Progress<string>(); var timeout = Task.Delay(1000); var operationTask = operation(cancellationTokenSource.Token, progress); // Synchronously block for either the operation to complete or a timeout; // if the operation completes first, just return the result. var completedTask = Task.WhenAny(timeout, operationTask).Result; if (completedTask == operationTask) return await operationTask; // Kick off a progress form and have it close when the task completes. using (var progressForm = new ProgressForm(cancellationTokenSource, progress)) { operationTask.ContinueWith(_ => { progressForm.Close(); }); progressForm.ShowDialog(); } return await operationTask; } } 

Please note that synchronously blocking the UI thread may cause deadlocks - in this case, if operation attempts to sync back to the UI thread, it would be blocked until after the timeout - so it's not a true "deadlock" but quite inefficient.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks, that's awesome :-) It does suffer from the same problem as Hans Passant's answer (now deleted) where, because ShowDialog is not being called immediately, we don't get the side-benefit of disabling the underlying form for the duration of the operation. However, there are other ways to deal with that.
One question: if an exception is raised by the operationTask during the await Task.WhenAny() call, what kind of exception will be returned to the caller of that Execute() method? Do I need to start worrying about AggregateExceptions?
@Gary: I modified the code so that it (synchronously) blocks for up to a second and then will call ShowDialog without yielding. Personally, I would prefer disabling the calling form, displaying the dialog with a regular Show, and then registering a continuation to close the form. But this was easier.
@Gary: The code handles exceptions cleanly. Only once is Task.Result used, and that's used on the Task.WhenAny return value, which always completes in a successful state. BTW, your original code would lose the stack trace if operation threw an exception.
For anyone trying to use this code, you'll probably notice a bug: the ContinueWith part towards the end will execute the progressForm.Close() call on a random ThreadPool thread, and will then raise an InvalidOperationException because it can only execute on the UI thread. I was able to fix this by using TaskScheduler.FromCurrentSynchronizationContext() as the second parameter to ContinueWith.
|
1

I wouldn't recommend keeping parts of what you have if you want to delay display of the form. I would invoke the operation independently then create a Timer whose Tick event handler checks to see if the task is complete and if it is, does nothing. Otherwise it should create the form, passing the IProgress<T> and CancellationTokenSource, and task into the form. You can still await a task that has already be started. For the task to be started, it will need the progress object and the cancellation token before the form is created--so that needs to be created independently...

2 Comments

Thanks. You're the Nth person to recommend that I don't show the form until after the delay period. You're (all) probably right. The reason I was attracted to showing (or rather, creating) the form immediately is that it had the side-benefit of disabling the form underneath for the duration of the operation, which is useful. However, as I've said elsewhere, there are other ways to achieve the same thing.
You can easily disable the "parent" form. I would recommend disabling only a subset of controls (like the ones uses to spawn this operation). But, you can simply use Form.Enabled to disable it and re-enable it in the Tick handler or in the child form if it gets shown.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.