【问题标题】:Async/Await with a WinForms ProgressBar使用 WinForms ProgressBar 进行异步/等待
【发布时间】:2013-08-01 02:09:35
【问题描述】:

我过去曾使用 BackgroundWorker 处理过这类事情,但我想使用 .NET 4.5 的新异步/等待方法。我可能会叫错树。请指教。

目标:创建一个组件,该组件将执行一些长时间运行的工作,并在工作时显示一个带有进度条的模态表单。组件在执行长时间运行的工作时将获取窗口句柄以阻止交互。

状态:见下面的代码。我以为我做得很好,直到我尝试与窗户互动。如果我不理会事情(即不要碰!),一切都会“完美地”运行,但如果我只点击任一窗口,程序就会在长时间运行的工作结束后挂起。实际交互(拖动)被忽略,就像 UI 线程被阻塞一样。

问题:我的代码可以很容易地修复吗?如果是这样,怎么做?或者,我应该使用不同的方法(例如 BackgroundWorker)吗?

代码(Form1 是一个标准表单,带有一个 ProgressBar 和一个公共方法 UpdateProgress,用于设置 ProgressBar 的值):

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApplication1
{
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Starting..");
        var mgr = new Manager();
        mgr.GoAsync();
        Console.WriteLine("..Ended");
        Console.ReadKey();
    }
}

class Manager
{
    private static Form1 _progressForm;

    public async void GoAsync()
    {
        var owner = new Win32Window(Process.GetCurrentProcess().MainWindowHandle);
        _progressForm = new Form1();
        _progressForm.Show(owner);

        await Go();

        _progressForm.Hide();
    }

    private async Task<bool> Go()
    {
        var job = new LongJob();
        job.OnProgress += job_OnProgress;
        job.Spin();
        return true;
    }

    void job_OnProgress(int percent)
    {
        _progressForm.UpdateProgress(percent);
    }
}

class LongJob
{
    public event Progressed OnProgress;
    public delegate void Progressed(int percent);

    public void Spin()
    {
        for (var i = 1; i <= 100; i++)
        {
            Thread.Sleep(25);
            if (OnProgress != null)
            {
                OnProgress(i);
            }
        }
    }
}

class Win32Window : IWin32Window
{
    private readonly IntPtr _hwnd;
    public Win32Window(IntPtr handle)
    {
        _hwnd = handle;
    }
    public IntPtr Handle
    {
        get
        {
            return _hwnd;
        }
    }
}
}

【问题讨论】:

    标签: c# multithreading winforms asynchronous async-await


    【解决方案1】:

    asyncawait 关键字并不意味着“在后台线程上运行”。我有一个async/await intro on my blog,描述了他们做什么的意思。您必须将 CPU 绑定操作显式置于后台线程上,例如 Task.Run

    此外,Task-based Asynchronous Pattern 文档描述了使用async 代码的常用方法,例如进度报告。

    class Manager
    {
      private static Form1 _progressForm;
    
      public async Task GoAsync()
      {
        var owner = new Win32Window(Process.GetCurrentProcess().MainWindowHandle);
        _progressForm = new Form1();
        _progressForm.Show(owner);
    
        var progress = new Progress<int>(value => _progressForm.UpdateProgress(value));
        await Go(progress);
    
        _progressForm.Hide();
      }
    
      private Task<bool> Go(IProgress<int> progress)
      {
        return Task.Run(() =>
        {
          var job = new LongJob();
          job.Spin(progress);
          return true;
        });
      }
    }
    
    class LongJob
    {
      public void Spin(IProgress<int> progress)
      {
        for (var i = 1; i <= 100; i++)
        {
          Thread.Sleep(25);
          if (progress != null)
          {
            progress.Report(i);
          }
        }
      }
    }
    

    请注意,Progress&lt;T&gt; 类型可以正确处理线程封送处理,因此无需在Form1.UpdateProgress 内进行封送处理。

    【讨论】:

    • 您的更改没有产生预期的结果。由于 OP 在Console 应用程序中运行,因此没有SynchronizationContext。我认为需要引入 Progress&lt;T&gt; 才能正常工作?
    • Windows 窗体组件在创建时将建立一个WinFormsSynchronizationContext,而Progress&lt;T&gt; 不需要SynchronizationContext(尽管它确实可以更好地工作)。
    • 我在传递给Progress&lt;T&gt; 的委托中设置了一个断点 - 它永远不会被击中 - 尽管工作正在旋转,但 UI 只是挂起。我认为Application.Run() 需要建立SynchronizationContext - _progressForm.Show 建立一个吗?我不确定。
    • 我相信有一个SynchronizationContext 但没有主循环。 Main 必须调用 Application.RunShowDialog 或类似的名称。
    • 是的,你是对的。 SynchronizationContext 在那里 - 但没有循环来处理 Posted/Send 到它。因此我的断点不会命中。谢谢。
    【解决方案2】:
    private async void button1_Click(object sender, EventArgs e)
    {
        IProgress<int> progress = new Progress<int>(value => { progressBar1.Value = value; });
        await Task.Run(() =>
        {
            for (int i = 0; i <= 100; i++)
                progress.Report(i);
        });
    }
    

    如果我错了,请纠正我,但这似乎是更新进度条的最简单方法。

    【讨论】:

    • 完美运行
    【解决方案3】:

    @StephenCleary 的回答是正确的。不过,我不得不对他的答案进行一些修改,以获得我认为 OP 想要的行为。

    public void GoAsync() //no longer async as it blocks on Appication.Run
    {
        var owner = new Win32Window(Process.GetCurrentProcess().MainWindowHandle);
        _progressForm = new Form1();
    
        var progress = new Progress<int>(value => _progressForm.UpdateProgress(value));
    
        _progressForm.Activated += async (sender, args) =>
            {
                await Go(progress);
                _progressForm.Close();
            };
    
        Application.Run(_progressForm);
    }
    

    【讨论】:

    • 如果 Go(progress) 抛出异常,则永远不会调用 _progressForm.Close() -> 模态对话框将永远挂起
    • 并且最好将“_progressForm.Activated”更改为“_progressForm.Shown”,因为_progressForm可能在他的一生中被多次激活.. -> Go(progress) 将被多次调用..
    猜你喜欢
    • 2019-02-10
    • 2019-07-29
    • 1970-01-01
    • 2015-07-27
    • 1970-01-01
    • 2017-05-11
    • 2020-09-11
    • 1970-01-01
    相关资源
    最近更新 更多