0

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);
    });
}
ShamilS
  • 1,410
  • 2
  • 20
  • 40

1 Answers1

3

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);
    }
  });
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you, but the question is not about (mass) progress updates - it's about any updates of controls while in debugging/tracing mode (F5) in form behind async/await event processing code (cycles) where there could me many write references to the owning form controls. Putting all that write references into IProgress<...> blocks doesn't make code looking any better in my opinion - after all in async/await form behind events there is no any need nor in .InvokeRequired/.Invoke nor in IProgress<...> while code is executed in runtime mode (Ctrl+F5) or in production environment? – ShamilS Aug 01 '17 at 08:00
  • Whether you're running in the debugger or not doesn't matter; `IProgress` is the appropriate choice for progress updates. The `Progress` implementation manages the UI context aspect, so no `Invoke` is required. – Stephen Cleary Aug 01 '17 at 16:02
  • Thank you. Still the subject is not clear for me. Let's say I have a WinForms DataGridView and a button (cmdUpdate) starting, when clicked, a long running cycle to update selected in DataGridView rows' cells with values collected in this cycle. cmdTest_Click method has async modifier. Five cells of every selected rows have to be updated if their new values differ from their current view. Update cycle's code is implemented within cmdUpdate_Click method as await Task.Run(() => .... (to be continued) – ShamilS Aug 01 '17 at 21:40
  • (continued) Question: What would be the appropriate way to update cells' values and to avoid "system.invalidoperationexception cross-thead operation not valid..." runtime error while updating them - to use .InvokeRequired /.Invoke or ... ? NB: Code lines updating every of five cells are not consequitive - every cell value update code line has a few update value calculation/preparation code lines. – ShamilS Aug 01 '17 at 21:41
  • I'm having a hard time following your comments. If you're need to update UI with *results*, then something like `myCellValue = await Task.Run(..);` would work. – Stephen Cleary Aug 01 '17 at 23:41
  • Please take your time, answer only when it would be most comfortable for you. Let's simplify it a bit - I have added **Update** section to my original post. It wouldn't be an off-topic as I would need to refactor and to trace/debug (F5) the legacy code constructions as the one enclosed into `statsProgress` in the added code sample. And such controls' values updates could be located in many places of legacy code, and should be issued/fired in different time moments of a long running awaited Task cycle, therefore these controls' updates cannot be refectored into one code block. – ShamilS Aug 02 '17 at 07:58
  • The `T` in `IProgress` should contain the update - e.g., `IProgress` where `MyUpdate` is `struct { double RunningSum; long TotalCount; double Average; }`. – Stephen Cleary Aug 02 '17 at 12:53
  • Based on your recommendations I have posted the final code solution in **Update 2**. Done. Thank you. – ShamilS Aug 02 '17 at 13:43