async/await debugging in WinForms: system.invalidoperationexception cross-thread operation not valid

749 Views Asked by At

When starting/running your aplication from within Visual Studio 2017 by Ctrl+F5 (Start Without Debugging) and using async/await for winforms' controls events processing. e.g., button click event processing, you can access these controls properties for read/write operations, but when you start your application by F5 you get runtime error message:

system.invalidoperationexception cross-thread operation not valid: 
Control '{{controlName}}' accessed from a thread other than the thread it was created on.'

To solve this issue you have to use well known

if (this.InvokeRequired) ...

code construction.

Question: Is there any/more elegant way to avoid using .InvokeRequired without conditional compilation presented in the following code sample fragment:

#define DEBUG_TRACE

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsAppToTestAsyncAwait
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }
        private
            async
            void cmdTest_Click(object sender, EventArgs e)
        {
            await Task.Run(() =>
            {
#if DEBUG_TRACE
                inv(()=>
#endif
                txtTest.Text = ""
#if DEBUG_TRACE
                )
#endif
                ;
                for (long i = 1; i < 100000000; i++)
                {
                    if (i % 10000000 == 1)
#if DEBUG_TRACE
                        inv(() =>
#endif
                        txtTest.Text =
                            txtTest.Text +
                                i.ToString() + System.Environment.NewLine
#if DEBUG_TRACE
                                )
#endif
                                ;
                }

            });
        }

 #if DEBUG_TRACE
        private void inv(Action a)
        {
            if (this.InvokeRequired) this.Invoke (a); else a();
        }
 #endif
 }
}

Update

Question: Would in the following code sample statsProgress code construction be the most optimal/recommended solution?

private async void cmdTest_Click(object sender, EventArgs e)
{
    double runningSum = 0;
    long totalCount = 0;
    double average = 0;

    IProgress<long> progress = new Progress<long>(i =>
    {
        txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
    });

    IProgress<object> statsProgress = new Progress<object>(o =>
    {
        txtRunningSum.Text = runningSum.ToString();
        txtTotalCount.Text = totalCount.ToString();
        txtAverage.Text = average.ToString();
    });

    txtTest.Text = "";

    await Task.Run(() =>
    {
        for (long i = 1; i < 100000000; i++)
        {
            runningSum += i;
            totalCount += 1;
            average = runningSum / totalCount;

            if (i % 10000000 == 1)  progress?.Report(i);
            // in general case there could be many updates of controls' values
            // from within this awaited Task with every update issued/fired
            // on different steps of this long running cycle
            if (i % (10000000 / 2) == 1) statsProgress?.Report(default(object));
        }
    });
}

Update 2

Here is the final solution:

internal struct UpdateStats
{
    internal double RunningSum;
    internal long TotalCount;
    internal double Average;
}
private async void cmdTest_Click(object sender, EventArgs e)
{
    UpdateStats stats = new UpdateStats();
    IProgress<long> progress = new Progress<long>(i =>
    {
        txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
    });

    IProgress<UpdateStats> statsProgress = new Progress<UpdateStats>(o =>
    {
        txtRunningSum.Text = o.RunningSum.ToString();
        txtTotalCount.Text = o.TotalCount.ToString();
        txtAverage.Text = o.Average.ToString();
    });

    txtTest.Text = "";

    await Task.Run(() =>
    {
        const int MAX_CYCLE_COUNT = 100000000;
        for (long i = 1; i <= MAX_CYCLE_COUNT; i++)
        {
            stats.RunningSum += i;
            stats.TotalCount += 1;
            stats.Average = stats.RunningSum / stats.TotalCount;

            if (i % 10000000 == 1) progress?.Report(i);
            // in general case there could be many updates of controls' values
            // from within this awaited Task with every update issued/fired
            // on different steps of this long running cycle
            if (i % (10000000 / 2) == 1) statsProgress?.Report(stats);
        }

        progress?.Report(MAX_CYCLE_COUNT);
        statsProgress?.Report(stats);
    });
}
1

There are 1 best solutions below

8
On BEST ANSWER

For progress updates, use IProgress<T>/Progress<T>:

private async void cmdTest_Click(object sender, EventArgs e)
{
  IProgress<int> progress = new Progress<int>(i =>
  {
      txtTest.Text = txtTest.Text + i.ToString() + System.Environment.NewLine;
  });

  txtTest.Text = "";
  await Task.Run(() =>
  {
    for (long i = 1; i < 100000000; i++)
    {
      if (i % 10000000 == 1)
        progress?.Report(i);
    }
  });
}