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

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

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

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

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

Ответы [ 3 ]

7 голосов
/ 05 декабря 2011

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

4 голосов
/ 05 декабря 2011

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

Task<Data>.Factory.StartNew(() => GetData())
            .ContinueWith(t => Process(t.Result))
            .ContinueWith(t => AskUserAbout(t.Result), TaskScheduler.FromCurrentSynchronizationContext());
2 голосов
/ 05 декабря 2011

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