Как избежать кода спагетти при использовании событий завершения?

Как-то я не могу поверить, что я первый, кто столкнулся с этой проблемой (и я не хочу верить, что я достаточно глуп, чтобы не видеть решение напрямую), но мой поиск-фу не был достаточно сильный.

Я регулярно сталкиваюсь с ситуацией, когда мне нужно сделать несколько трудоемких шагов один за другим. Рабочий процесс выглядит как

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();

Это очень похоже на стиль продолжения прохождения для моего вкуса, и должен быть лучший способ структурировать этот код. Но как?

Ответы

Ответ 1

Правильный способ состоял бы в том, чтобы сконструировать ваши компоненты синхронно и выполнить целую цепочку в фоновом потоке.

Ответ 2

Параллельная библиотека задач может быть полезна для такого кода. Обратите внимание, что TaskScheduler.FromCurrentSynchronizationContext() может использоваться для запуска задачи в потоке пользовательского интерфейса.

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

Ответ 3

Вы можете поместить все в 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 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
    }
}