Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid spaghetti code when using completion events?

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?

like image 940
Jens Avatar asked Dec 05 '11 16:12

Jens


People also ask

What causes spaghetti code?

Spaghetti code is a pejorative phrase for unstructured and difficult-to-maintain source code. Spaghetti code can be caused by several factors, such as volatile project requirements, lack of programming style rules, and software engineers with insufficient ability or experience.

What does pasta mean in code?

Spaghetti code is a slang term used to refer to a tangled web of programming source code where control within a program jumps all over the place and is difficult to follow. Spaghetti code normally has a lot of GOTO statements and is common in old programs, which used such statements extensively.


3 Answers

The correct way would be to design your components in a synchronous way and execute the complete chain in a background thread.

like image 196
Daniel Hilgarth Avatar answered Oct 05 '22 05:10

Daniel Hilgarth


The Task Parallel Library can be useful for such code. Note that TaskScheduler.FromCurrentSynchronizationContext() can be used to run the task on the UI thread.

Task<Data>.Factory.StartNew(() => GetData())
            .ContinueWith(t => Process(t.Result))
            .ContinueWith(t => AskUserAbout(t.Result), TaskScheduler.FromCurrentSynchronizationContext());
like image 29
jasonp Avatar answered Oct 05 '22 04:10

jasonp


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
    {
        // the work is finished! the result is in args.Result
    }
}
like image 24
Daniel Gabriel Avatar answered Oct 05 '22 03:10

Daniel Gabriel