当使用完成事件时如何避免代码混乱?

14

我不敢相信我是第一个遇到这个问题的人(我也不想相信我是唯一一个太傻以至于不能直接看到解决方案的人),但我的搜索技能还不够强。

我经常遇到这样的情况:需要依次执行一些耗时的步骤。工作流程如下:

var data = DataGetter.GetData();
var processedData = DataProcessor.Process(data);
var userDecision = DialogService.AskUserAbout(processedData);
// ...

我不想在每个步骤中阻塞用户界面,因此每个方法都会立即返回,并在完成后触发一个事件。现在发生了一些有趣的事情,因为上面的代码块会发生变异。

DataGetter.Finished += (data) =>
    {
        DataProcessor.Finished += (processedData) =>
        {
            DialogService.Finished(userDecision) =>
                {
                    // ....
                }
                DialogService.AskUserAbout(processedData);
            }
        DataProcessor.Process(data);
    };
DataGetter.GetData();

对我来说,这读起来太像Continuation-passing style了,代码结构一定有更好的方式。但是该怎么做呢?


1
让我想起了"Continuation Tasks"(延续任务):http://www.blackwasp.co.uk/ContinuationTasks.aspx - Louis Kottmann
3
在C# 5中使用await会使这个过程变得非常容易。所以,是的,人们经常遇到这个问题,这并不好看——足够严重,以至于它被纳入了核心语言。 - Roman Starkov
3个回答

7

正确的方式是以同步方式设计您的组件并在后台线程中执行完整的链。


感谢您的输入!由于用户交互在非UI线程上(至少在WPF中)不易实现,这使得问题变得更加困难。但是我们可以通过这种方式减少代码混乱程度。 - Jens
我们在谈论什么类型的用户输入? - Daniel Hilgarth
您的用户可能会被提示执行任务(操作机器),完成后按“下一步”,或者被询问如何处理processedData。 - Jens
这个答案非常有帮助!我在进行对话框的同步调用时遇到的问题可以通过在 UI 线程上的 Closed 处理程序上设置 ManualResetEvent 并让后台线程等待该事件来解决。 - Jens

4

任务并行库对于这样的代码很有用。注意,TaskScheduler.FromCurrentSynchronizationContext() 可以用于在UI线程上运行任务。

Task<Data>.Factory.StartNew(() => GetData())
            .ContinueWith(t => Process(t.Result))
            .ContinueWith(t => AskUserAbout(t.Result), TaskScheduler.FromCurrentSynchronizationContext());

2
你可以把所有东西放到 BackgroundWorker 中。如果你将 GetData、Process 和 AskUserAbout 方法改为同步运行,以下代码才能正常工作。
像这样:
private BackgroundWorker m_worker;

private void StartWorking()
{
    if (m_worker != null)
        throw new InvalidOperationException("The worker is already doing something");

    m_worker = new BackgroundWorker();
    m_worker.CanRaiseEvents = true;
    m_worker.WorkerReportsProgress = true;

    m_worker.ProgressChanged += worker_ProgressChanged;
    m_worker.DoWork += worker_Work;
    m_worker.RunWorkerCompleted += worker_Completed;
}

private void worker_Work(object sender, DoWorkEventArgs args)
{
    m_worker.ReportProgress(0, "Getting the data...");
    var data = DataGetter.GetData();

    m_worker.ReportProgress(33, "Processing the data...");
    var processedData = DataProcessor.Process(data);

    // if this interacts with the GUI, this should be run in the GUI thread.
    // use InvokeRequired/BeginInvoke, or change so this question is asked
    // in the Completed handler. it's safe to interact with the GUI there,
    // and in the ProgressChanged handler.
    m_worker.ReportProgress(67, "Waiting for user decision...");
    var userDecision = DialogService.AskUserAbout(processedData);

    m_worker.ReportProgress(100, "Finished.");
    args.Result = userDecision;
}

private void worker_ProgressChanged(object sender, ProgressChangedEventArgs args)
{
    // this gets passed down from the m_worker.ReportProgress() call
    int percent = args.ProgressPercentage;
    string progressMessage = (string)args.UserState;

    // show the progress somewhere. you can interact with the GUI safely here.
}

private void worker_Completed(object sender, RunWorkerCompletedEventArgs args)
{
    if (args.Error != null)
    {
        // handle the error
    }
    else if (args.Cancelled)
    {
        // handle the cancellation
    }
    else
    {
        // the work is finished! the result is in args.Result
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接