使用完成事件时如何避免意大利面条代码?

时间:2011-12-05 16:31:40

标签: c# events coding-style

不知怎的,我无法相信我是第一个遇到这个问题的人(而且我不想相信我是唯一一个不能直接看到解决方案的傻瓜),但是我的搜索不是足够强大。

当我需要一个接一个地执行一些耗时的步骤时,我经常遇到这种情况。工作流程看起来像

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

我不希望在每个步骤中阻止UI,因此每个方法都会立即返回,并在完成后引发一个事件。现在随之而来的是欢闹,因为上面的代码块变异为

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

对于我的口味,这读起来太像Continuation-passing style,并且必须有更好的方法来构造这段代码。但是如何?

3 个答案:

答案 0 :(得分:7)

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

答案 1 :(得分:4)

Task Parallel Library对此类代码非常有用。请注意,TaskScheduler.FromCurrentSynchronizationContext()可用于在UI线程上运行任务。

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

答案 2 :(得分: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
    }
}