reve_etrange
reve_etrange

Reputation: 2571

Control.Invoke() vs IProgress / ReportProgress

The UI can be passed information from an asynchronous task using IProgress or BackgroundWorker.ReportProgress:

class Approach1 : UserControl
{
    enum Stage { INIT, STATUS, DATA, TIME, ... }
    struct ProgressObject
    {
         int StatusCode;
         int SecondsRemaining;
         IList<int> Data;
         Stage CurrentStage;
    }

    TextBox Status;

    async Task DoWork1(IProgress<ProgressObject> progress) 
    {
         await Task.Run( () =>
         {
             progress.Report(new ProgressObject(0, 0, null, Stage.INIT));
             int code = DoSomething();
             progress.Report(new ProgressObject(code, 0, null, Stage.STATUS));
             IList<int> Data = ...;
             progress.Report(new ProgressObject(0, 0, Data, Stage.DATA));
             int Seconds = ...;
             progress.Report(new ProgressObject(0, time, null, Stage.TIME));
         });
    }

    void ReportProgress(ProgressObject progress)
    {
        switch (progress.CurrentStage)
        {
            case Stage.CODE:
                Status.Text = DecodeStatus(progress.StatusCode);
                break;
            // And so forth...
        }
    }

    async void Caller1(object sender, EventArgs e)
    {
        var progress = new Progress<ProgressObject>(ReportProgress);
        await DoWork2(progress);
    }
}

However, this can also be done by passing a delegate to a UI object's BeginInvoke method (Invoke if we want to block):

class Approach2 : UserControl
{
    Textbox Status;

    int StatusCode
    {
        set
        {
            BeginInvoke(new Action( () => Status.Text = DecodeStatus(value));
        }
    }

    // Imagine several other properties/methods like the above:
    int SecondsRemaining;
    void UpdateData(IList<int> data);

    async Task DoWork2()
    {
        await Task.Run( () =>
        {
            StatusCode = DoSomething();
            UpdateData(...);
            SecondsRemaining = ...;
        });
    }

    async void Caller2(object sender, EventArgs e)
    {
        await DoWork1();
    }
}        

Should the dedicated progress reporting mechanisms be preferred over Invoke? If, so why? Are there any likely 'gotchas' arising from either approach?

IMHO, the Invoke way is simpler / requires less code compared to, say, a ReportProgress accepting a progress struct with several fields, especially if progress is reported at multiple stages of the task and the reporting method thus needs to branch to the appropriate reporting for a given stage.

Upvotes: 0

Views: 641

Answers (1)

Hans Passant
Hans Passant

Reputation: 942267

You should have gotten a cue from your struggles to make Approach2 actually compile. Took a while, didn't it? I saw you repeatedly editing the snippet. Another cue you got was that the only way to get there was to derive your class from UserControl.

Which is the problem with Begin/Invoke(), it can only work when you have access to a Control object. Code inside a library often (and should) have no idea what the UI looks like. It might not even be implemented in Winforms, could be used in a WPF or Universal app for example.

Progress<> works with those GUI class libraries as well, it uses a more universal way to properly synchronize. Provided by the SynchronizationContext.Current property, it relies on the GUI class library to install a provider. The one that Winforms installs, WindowsFormsSynchronizationContext, automatically calls BeginInvoke() in its Post() method and Invoke() in its Send() method. Also the mechanism that makes async/await code independent from the UI implementation.

There is one disadvantage to Progress<>, it can completely fail to get the job done in a very hard to diagnose way. The object must be created by code that runs on the UI thread of an app. If it is not then SynchronizationContext.Current doesn't have a value and the ProgressChanged event is going to fire on an arbitrary threadpool thread. Kaboom if you try to update the UI with an event handler, you won't know why because the exception occurs in code that's far removed from the bug.

But sure, if you hardcode your class to derive from System.Windows.Forms.UserControl then you have little use for Progress<>. Other than the feel-good feeling that you'll have less work to do when you ever port it to another GUI class library.

Upvotes: 2

Related Questions