Alex Rivera | Logout

How to avoid spaghetti code when using completion events?

Asked 2011-12-05T16:31:40.040
14

Somehow I cannot believe that I am the first one to run into that problem (and I don't want to believe that I am the only one stupid enough not to see a solution directly), but my search-fu was not strong enough.

I regularly run into a situation, when I need to do a few time-consuming steps one after the other. The workflow looks like

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

I don't want to block the UI during each step, so every method does return immediately, and raises an event once it has finished. Now hilarity ensues, since the above code block mutates into

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

This reads too much like Continuation-passing style for my taste, and there has to be a better way to structure this code. But how?

Edit
Report

1 Answer

2

You can put everything into a BackgroundWorker. The following code would only work properly if you change the methods GetData, Process, and AskUserAbout to run synchronously.

Something like this:

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
    {
     
answered 2011-12-05T17:18:04.600

Your Answer