8

I have a WinForms application containing Button and a RichTextBox controls. After user clicks on Button, an IO demanding operation is executed. To prevent blocking of the UI thread I have implemented the async/await pattern. I would also like to report progress of this operation into RichTextBox. This is how the simplified logic looks like:

private async void LoadData_Click(Object sender, EventArgs e)
{
    this.LoadDataBtn.Enabled = false;

    IProgress<String> progressHandler = new Progress<String>(p => this.Log(p));

    this.Log("Initiating work...");

    List<Int32> result = await this.HeavyIO(new List<Int32> { 1, 2, 3 }, progressHandler);

    this.Log("Done!");

    this.LoadDataBtn.Enabled = true;
}

private async Task<List<Int32>> HeavyIO(List<Int32> ids, IProgress<String> progress)
{
    List<Int32> result = new List<Int32>();

    foreach (Int32 id in ids)
    {
        progress?.Report("Downloading data for " + id);

        await Task.Delay(500); // Assume that data is downloaded from the web here.

        progress?.Report("Data loaded successfully for " + id);

        Int32 x = id + 1; // Assume some lightweight processing based on downloaded data.

        progress?.Report("Processing succeeded for " + id);

        result.Add(x);
    }

    return result;
}

private void Log(String message)
{
    message += Environment.NewLine;
    this.RichTextBox.AppendText(message);
    Console.Write(message);
}

After operation gets successfully completed, the RichTextBox contains following text:

Initiating work...
Downloading data for 1
Data loaded successfully for 1
Processing succeeded for 1
Downloading data for 2
Data loaded successfully for 2
Processing succeeded for 2
Downloading data for 3
Done!
Data loaded successfully for 3
Processing succeeded for 3

As you can see the progress for 3rd work item is reported after Done!.

My question is, what is causing the delayed progress reporting and how can I achieve that flow of LoadData_Click will continue only after all progress has been reported?

4
  • What is IProgress<String> & Progress? Where is the source for them? They seem pretty important to your question. Commented Nov 24, 2017 at 11:38
  • 2
    They are part of the async-await framework, please see: learn.microsoft.com/en-us/dotnet/api/… Commented Nov 24, 2017 at 11:39
  • @Enigmativity those are standard classes (reside in "mscorlib" dll), starting from .NET 4.5 Commented Nov 24, 2017 at 11:40
  • 2
    @Markkknk - So, after reading the docs, thank you, the issue seems to be that progress?.Report( is pushing the execution of the p => this.Log(p) on to the SychronizationContext. That means it has to wait until the UI message loop is idle before it can execute that code. I suggest you try to remove the progress calls and write to the log directly. I suspect the problem will go away then. Commented Nov 24, 2017 at 11:44

2 Answers 2

8

Progress class will capture current synchronization context when created and then will post callbacks to that context (this is stated in documentation of that class, or you can look at source code). In your case that means that WindowsFormsSynhronizationContext is captured, and posting to it is rougly the same as doing Control.BeginInvoke().

await also captures current context (unless you use ConfigureAwait(false)) and will post continuation of method to it. For iterations except last, UI thread is released on await Task.Delay(500); and so can process your report callbacks. But on last iteration of your foreach loop the following happens:

// context is captured
await Task.Delay(500); // Assume that data is downloaded from the web here.
// we are now back on UI thread
progress?.Report("Data loaded successfully for " + id);
// this is the same as BeginInvoke - this puts your callback in UI thread
// message queue
Int32 x = id + 1; // Assume some lightweight processing based on downloaded data.
// this also puts callback in UI thread queue and returns
progress?.Report("Processing succeeded for " + id);
result.Add(x);

So, in last iteration, your callbacks are put into UI thread message queue, but they cannot be executed right now, because you are executing code in UI thread at this same moment. When code reaches this.Log("done") - it's written to your log control (no BeginInvoke is used here). Then after your LoadData_Click method ends - only at this point UI thread is released from executing your code and message queue may be processed, so your 2 callbacks waiting there are resolved.

Given all that information - just Log directly as Enigmativity said in comment - there is no need to use Progress class here.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your explanation. The problem is, that in my non-simplified solution the IO logic (the HeavyIO method) resides in separate worker-like class. For the time being I have solved my issue by introducing reference to my Log method by adding Action parameter to the HeavyIO method. I am marking your answer as accepted. Thanks!
@Markkknk well at least knowing what's going on you can make informed decision how to resolve this in your real-world case.
4

Your code is completely right, the only thing you need is to add

await Task.Yield(); 

as last sentence of HeavyIO method, just before returning result.

The reason is, as previosly said -- you need to allow progress report to be handled by UI thread, and Task.Yield() does exactly that.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.