通过嵌套方法使用 TPL 时无法更新 UI 个元素

Unable to update UI elements when using TPL through nested methods

我正在尝试使用 TPL 从多种方法更新 UI 元素。当执行第一个方法时,元素会更新,但是当调用嵌套在第一个方法中的子方法时,我得到一个 InvalidOperationException-当前的 SynchronizationContext 不能用作 TaskScheduler。我什至尝试将代码转换为 async - await 模式但没有成功。

编辑 1:无法重现 InvalidOperationException。我现在收到此错误 - 当前的 SynchronizationContext 不能用作 TaskScheduler。

编辑 2:重现了 InvalidOperationException。这是由于当前的 SynchronizationContext 可能未用作 TaskScheduler 引起的。堆栈跟踪如下:

at System.Threading.Tasks.SynchronizationContextTaskScheduler..ctor()
at System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext() at IntegratedTracker.IntegratedTracker.ProgressReporter..ctor() in
at IntegratedTracker.IntegratedTracker.UploadToDb() in
at System.Threading.Tasks.Task.InnerInvoke() at System.Threading.Tasks.Task.Execute()

此外,我注意到如果我在 button_click 事件中调用 UpdateValuesInDb 方法,更新会流向 UI。

我的代码连同我所做的研究一起在下面提交,这些研究在代码的注释中。

private void btnUploadToDb_Click(object sender, EventArgs e)
{
    UploadToDb();
}

private static void UploadToDb()
{
    Task.Factory.StartNew(()=>
    {
        for (int i = 0; i <= maxRecords - 1; i++)
        {
            // Code for inserting into Db.

            // Update progress to progressbar and label on UI form.
            // Borrowed from Stephen Cleary's article
            // http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html
            progressReporter.ReportProgress(() => { 
                // Can this be done in a better way than enclosing in a 
                // ReportProgress before entering the for loop?                     
                progressbar.maximum=maxRecords; 
                label.text="Uploading " + i;
                progressbar.value=i;
            });
        }

        progressReporter.ReportProgress(() => {
            label.Text = "Updating values in DB, please wait...";
        });

        // When this call is made the subsequent updates to the main form 
        // were not successful due to 'InvalidOperationException'.
        UpdateValuesInDb();     
    });
}

private void UpdateValuesInDb();
{
    // Code for updating the values.

    // The below method should be called in sequence and labels updated in the same sequence.

    // Report progress to UI for values of type 1.
    // label.text="Moving values of type 1..."; // This has not been implemented because of cross thread exceptions
                            // This is what I'd like to achieve.
    MoveValuesToNewDbDeleteFromSourceDb(values); // This call has to complete first

    // Report progress to UI for values of type 2.
    // label.text="Moving values of type 2..."; // This has not been implemented because of cross thread exceptions
                            // This is what I'd like to achieve.
    MoveValuesToNewDbDeleteFromSourceDb(values); // This call has to complete second.

    // Report progress to UI for values of type 3.
    label.text="Moving values of type 3...";    // This has not been implemented because of cross thread exceptions
                            // This is what I'd like to achieve.
    MoveValuesToNewDbDeleteFromSourceDb(values); // This call has to complete third.

    // Report progress to UI for values of type 4.
    // label.text="Moving values of type 4..."; // This has not been implemented because of cross thread exceptions
                            // This is what I'd like to achieve.
    MoveValuesToNewDbDeleteFromSourceDb(values); // This call is the final.
}


private void MoveValuesToNewDbDeleteFromSourceDb(string values)
{
    var progressReporter = new ProgressReporter();
    Task.Factory.StartNew(() => {
        for (int i = 0; i <= dt.Rows.Count - 1; i++)
        {
            // Tried using Stephen Cleary's code here but it fails with 
                    progressReporter.ReportProgress(() =>
                    {
                        progressBar.Maximum = maxRecords - 1;
                        Label.Text = "Uploading " + i;
                        progressBar.Value = i;
                    });
            // Need to update label and progress in for loop as mentioned above.
        }
    });
}

我什至尝试将现有代码转换为使用 async-await 方法,但它在编译期间失败,表明它不可等待。 这是我尝试更改的内容:

private async void btnUploadToDb_Click(object sender, EventArgs e)
{
    await UploadToDb(); // Got the error Type System.Threading.Tasks.Task is not awaitable.
}

private async Task UploadToDb()
{
    // Codes is same as the previous one, I've just used the async 
    // modifier, though I do not know what to await here.

    // Tried to assign await as follows:
    await   Task.Factory.StartNew(() => {
        for (int i = 0; i <= dt.Rows.Count - 1; i++)
        {
            // Need to update label and progress bar here.
        }
    });

    // The above results in a compile error:
    // 'Type System.Threading.Tasks.Task is not awaitable'.
}

我什至搜索了有关 TPL、初学者 TPL 等的教程,但是 none 这对解决我的问题很有帮助。

此外,我想知道我们是否需要处理创建的任务,或者这些任务是否像 using 块一样自动处理。如果在任务完成前遇到异常,是否需要处理任务?

我是 TPL 的新手,我正在为这些概念苦苦挣扎,因此非常感谢您的帮助。

忘了说,我正在使用 VS2010、.Net 4.0,这是用于 Windows Forms 应用程序。

您不想在 MoveValuesToNewDbDeleteFromSourceDb 方法中开始新任务,因为:

The below method should be called in sequence and labels updated in the same sequence.

为什么不将该方法更改为如下所示:

private void MoveValuesToNewDbDeleteFromSourceDb(string valuesType, string values)
{
    progressReporter.ReportProgress(() => {
        label.text = string.Format("Moving values of type {0}...", valuesType);
    });

    for (int i = 0; i <= dt.Rows.Count - 1; i++)
    {
        // Do your move values thing.

        // Report progress
        progressReporter.ReportProgress(() => {
           // ...
        });
    }
}

并这样称呼它:

private void UpdateValuesInDb();
{
    // Code for updating the values.

    // The below method should be called in sequence 
    // and labels updated in the same sequence.
    // for type "1" .. "4".
    MoveValuesToNewDbDeleteFromSourceDb("1", values); 
    MoveValuesToNewDbDeleteFromSourceDb("2", values); 
    MoveValuesToNewDbDeleteFromSourceDb("3", values); 
    MoveValuesToNewDbDeleteFromSourceDb("4", values); 
}

注:如博文所述,ProgressReporter为"deprecated":

Update, 2012-02-16: The information in this post is old. See the new post Reporting Progress from Async Tasks for a better solution.

第二个注意事项:我假设您的 progressReporter 实例是一个成员变量,它是在您的 UI 线程 上创建的 。如果不是这种情况,这就解释了您遇到问题的原因:将其设为成员变量,在创建表单时构造它。

最后,您可以使用更简单的实现来满足您的需求。

public class ProgressReporter
{
    private readonly SynchronizationContext _syncContext = SynchronizationContext.Current;

    public void ReportProgress(Action progressAction)
    {
        _syncContext.Post(new SendOrPostCallback(unused => progressAction()), null);
    }
}

如果您的应用程序 运行 在 STAThreadApartment 中,那么它可能会发生。

尝试使用 ConfigureAwait(false) 调用您的任务以防止使用默认的 SyncThreadContext。

虽然它可能会改变异步的行为。但如果我的假设是正确的,这可能会解决问题。

Update: You're on .Net 4.0 so ConfigureAwait is out of context.

问题可能是当您从嵌套方法创建任务时,您的 Synchrnoization 上下文可能会设置回 null。您可以从父方法获取当前同步上下文,并为所有嵌套方法使用相同的上下文。

我已经更新了你的代码:

 private static void UploadToDb()
 {
     var currentSyncContext = TaskScheduler.FromCurrentSynchronizationContext();

     Task.Factory.StartNew(()=>
     {
         for (int i = 0; i <= maxRecords - 1; i++)
         {
             progressReporter.ReportProgress(() => { 

                 progressbar.maximum=maxRecords; 
                 label.text="Uploading " + i;
                 progressbar.value=i;
             });
         }

                progressReporter.ReportProgress(() => {
             label.Text = "Updating values in DB, please wait...";
         });

     // When this call is made the subsequent updates to the main form 
     // were not successful due to 'InvalidOperationException'.
     UpdateValuesInDb(currentSyncContext); 

     });
}

private void UpdateValuesInDb(SynchronizationContext context);
{

    MoveValuesToNewDbDeleteFromSourceDb(values, context); 
    MoveValuesToNewDbDeleteFromSourceDb(values, context); 
    label.text="Moving values of type 3...";    
    MoveValuesToNewDbDeleteFromSourceDb(values, context); 
    MoveValuesToNewDbDeleteFromSourceDb(values, context); // This call is the final.
}


private void MoveValuesToNewDbDeleteFromSourceDb(string values, SynchronizationContext context)
{
    var progressReporter = new ProgressReporter();
    Task.Factory.StartNew(() => {
        for (int i = 0; i <= dt.Rows.Count - 1; i++)
        {
            // Tried using Stephen Cleary's code here but it fails with 
                    progressReporter.ReportProgress(() =>
                    {
                        progressBar.Maximum = maxRecords - 1;
                        Label.Text = "Uploading " + i;
                        progressBar.Value = i;
                    });
            // Need to update label and progress in for loop as mentioned above.
        }
    }, context);
}

ProgressReporter 必须在 UI 线程上创建。因此,您需要将行 var progressReporter = new ProgressReporter();MoveValuesToNewDbDeleteFromSourceDb 的顶部移动到 UploadToDb 的顶部。

并且强烈考虑升级您的Visual Studio版本。使用 async/awaitIProgress<T> 而不是 ProgressReporter.

要容易得多