模态进度表显示IProgress并支持取消WinForms的异步任务

时间:2014-03-05 03:05:26

标签: c# .net winforms task-parallel-library async-await

我一直在尝试使用可重复使用的模态进度窗口(即,progressForm.ShowDialog())来显示正在运行的异步任务的进度,包括启用取消。
我已经看到一些实现启动异步任务通过挂钩窗体上的Activated事件处理程序,但我需要先启动任务,然后显示将显示其进度的模式对话框,然后在完成时关闭模式对话框或取消已完成(注意 - 我希望在取消完成后表格关闭 - 表示从任务继续中关闭)。

我目前有以下内容 - 虽然这项工作 - 但是这方面有问题 - 还是可以更好地完成?

我确实读过我需要运行这个CTRL-F5,而不需要调试(以避免AggregateException在延续中停止调试器 - 并让它在生产代码中的try catch中捕获)

ProgressForm.cs - 具有ProgressBar(progressBar1)和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 - 包含WinForms应用程序(以及控制台应用程序)消耗逻辑的类文件

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 - 带按钮的表格(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();
        }

    }
}

修改后的代码 - 建议使用TaskCompletionSource ...

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

1 个答案:

答案 0 :(得分:5)

  

我认为我遇到的最大问题是使用等待(而不是   ContinueWith)意味着我不能使用ShowDialog,因为它们都是阻塞的   调用。如果我先调用ShowDialog,那么代码就会被阻止,   并且进度表需要实际启动异步方法(其中   是我想要避免的)。如果我打电话等待   首先是MyService.DoSomethingWithResultAsync,然后是阻止和我   然后无法显示我的进度表。

ShowDialog确实是一个阻塞API,因为它在对话框关闭之前不会返回。但它是继续抽取消息的意义上的非阻塞,尽管是在新的嵌套消息循环中。我们可以将此行为与async/awaitTaskCompletionSource

结合使用
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;
}