如何从单独的任务更新winforms UI?

时间:2017-02-06 20:33:14

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

我有一个现有系统,有几个长时间运行的操作。我希望找到一种简单的方法将这些操作包装在任务中,并为用户提供繁忙的状态通知。

这是使用.net 3.5而我正在使用TPL后端口。不幸的是,async / await不能在这里使用。

我试图用下面显示的简单测试形式提出解决方案,并遇到了一些问题。

以下方法仅显示前2-3个任务,之后UI不会更新,直到所有任务完成。这是为什么?

private void RunTaskUpdateBefore (string msg)
{
UpdateUI (msg);
Task task = Task.Factory.StartNew (() =>
{
    // simulate some work being done
    Thread.Sleep (1000);
});
task.Wait ();  
}

我希望这种方法有效,但在所有任务完成之前不会更新UI

private void ChildTaskExternalWait (string msg)
{
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext ();

Task task = Task.Factory.StartNew (() =>
{
    Task.Factory.StartNew (() =>
    {
        UpdateUI (msg);
    }, System.Threading.CancellationToken.None, TaskCreationOptions.None, uiScheduler);

    // simulate some work being done
    Thread.Sleep (1000);
});
task.Wait (); // I assume this is the problem
}

以下方法效果很好,但我不确定是否可以在我的项目中使用它。它是一个相当大的复杂系统,操作发生在许多不同的类中,并由我们无法修改的遗留系统控制。我假设我必须尝试创建某种全局任务队列才能做到这一点?

private Task RunTaskInternalWaitOnPrevious (string msg, Task t1 = null)
{
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext ();
Task task = Task.Factory.StartNew (() =>
{
    if (t1 != null)
        {
        t1.Wait ();
        }
    Task.Factory.StartNew (() =>
    {
        UpdateUI (msg);
    }, System.Threading.CancellationToken.None, TaskCreationOptions.None, uiScheduler);

    // simulate some work being done
    Thread.Sleep (2000);
});

return task;
}

我的测试表单实现:

private void button1_Click (object sender, EventArgs e)
{
// Only displays the first 2-3 tasks. After that UI doesn't get updated until all are complete
RunTaskUpdateBefore ("Task 1");
RunTaskUpdateBefore ("Task 2");
RunTaskUpdateBefore ("Task 3");
RunTaskUpdateBefore ("Task 4");
RunTaskUpdateBefore ("Task 5");
RunTaskUpdateBefore ("Task 6");
RunTaskUpdateBefore ("Task 7");

// this works with any number of tasks
// Unfortunately I'm not sure if I can use it in my application (see above notes)
//var t1 = RunTaskInternalWaitOnPrevious ("Task 1");
//var t2 = RunTaskInternalWaitOnPrevious ("Task 2", t1);
// and so on 
}
private void UpdateUI (string msg)
    {
    this.textBox1.Text += msg;
    this.textBox1.Text += System.Environment.NewLine;
    }

更新/澄清

*我的任务无法并行运行 *我想告知用户每一步都在进行工作 *任务运行时不会发生其他UI输入。

任务不会集中到特定的代码段,而是遍布整个地方。对不起,我不确定如何描述我的情况。

3 个答案:

答案 0 :(得分:2)

在此代码示例中,button1_Click同步运行并将阻止UI线程直到它完成,因此即使在它调用的方法中创建的任务想要更新UI线程,我怀疑这些尝试是排队等到一切都完成后。在您看来,await和async旨在解决此问题。在此之前,BackgroundWorkers可以用来做同样的事情。我将向您的表单添加BackgroundWorker,将所有代码从button1_Click移动到BackgroundWorker的DoWork方法,并在单击Button1时启动后台工作程序。我相信BackgroundWorkers已被弃用,但与.Net 3.5配合使用

如果您不熟悉BackgroundWorker类,请参阅以下快速教程:https://www.dotnetperls.com/backgroundworker

插入Application.DoEvents以暂停执行button1_Click直到UI可以自行更新是另一种选择,但这种方法存在许多潜在问题。请参阅此主题以获取一长串公平警告:Use of Application.DoEvents()

答案 1 :(得分:0)

当运行多个提供您要显示的进度信息的线程时,您可以按照以下步骤操作:

  • 定义包含任务ID和进度的UpdateInfo类 信息,
  • 创建一个变量UpdateInfos = new List< UpdateInfo>(),
  • 如果要从任务发送进度信息,请创建并填写 一个UpdateInfo,然后将其推送到列表中,即 Lock(UpdateInfos){ UpdateInfos.Add(...);}
  • 以主窗体创建一个计时器(例如每250ms),并在计时器事件中弹出所有UpdateInfo并相应地刷新HMI。

答案 2 :(得分:-1)

您可以在任务完成后立即使用continueWith更新线程。

var task1 = new Task(() => Thread.Sleep(1000)); //replace with actual task

var task2 = task1.ContinueWith(t => UpdateUI(msg), CancellationToken.None, TaskContinuationOptions.None, uiScheduler);

task1.Start();