Search code examples
c#winformsasync-awaitcode-analysis

How to prevent warning VSTHRD101 when using Control.BeginInvoke() to call an async method?


I'm refactoring some old WinForms code to use async/await, and I've hit a code analysis warning that I'm not sure how to fix properly.

The situation is that I have a method that is called from a non-UI thread which needs to update the UI. The old code uses the time-honoured technique of calling Control.BeginInvoke() to run the code on the UI thread.

The problem is that the method I want to invoke is now async and attempting to use Control.BeginInvoke() yields the warning VSTHRD101: "Avoid using async lambda for a void returning delegate type, because any exceptions not handled by the delegate will crash the process."

The following sample WinForms application demonstrates the problem. It's a simple form containing only a Button called "testBtn" and a Label called "label1".

The call to ControlBeginInvoke() inside the notRunningOnUiThread() method causes the warning as commented:

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

#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (disabled for brevity in this sample code)

namespace Net5WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = true; // For test purposes to prove that mustRunOnUiThreadAsync() runs on the UI thread.
        }

        // This method is only to simulate what's happening in the real code. 
        // Assume that you cannot change this.
        void testBtn_Click(object sender, EventArgs e)
        {
            _ = Task.Run(() => notRunningOnUiThread(this)); // Not my real code, but this simulates it.
        }

        // NOTE: In the real code this method is not a member of the Control,
        // but it does have access to the Control to update.
        void notRunningOnUiThread(Control ui) // Called from a non-UI thread and wants to call a method that updates the UI.
        {
            // The next line causes warning VSTHRD101:
            // "Avoid using async lambda for a void returning delegate type,
            // because any exceptions not handled by the delegate will crash the process."

            ui.BeginInvoke(new Action(async () => await mustRunOnUiThreadAsync()));
        }

        async Task mustRunOnUiThreadAsync()
        {
            label1.Text = "Before update";
            await Task.Delay(1000).ConfigureAwait(true);
            label1.Text = "After update";
        }
    }
}

If I suppress the warning, everything seems to run OK even if there are exceptions in mustRunOnUiThreadAsync() (if there's an exception, it's reported immediately and not from the finalizer for a Task).

My question is: Is it safe to suppress this warning? And if not, what's a good way to fix it?

The fundamental thing I want to solve is how to safely call an async UI method on the UI thread from a non-UI thread in a WinForms application, when only the ui Form is available and the current synchronisation context is not available.

(Note that I can't simply await mustRunOnUiThreadAsync() in notRunningOnUiThread(). That would cause a "Cross-thread operation not valid" InvalidOperationException to be thrown.)

I suppose one possible solution is to write a non-async method that calls the async method, using the advice from this answer. But is that the best approach for this particular case?


[EDIT]

I should have mentioned: In my actual code the notRunningOnUiThread() is not a member of the Control that needs to update the UI. Because notRunningOnUiThread() isn't running on a UI thread, I can't use System.Threading.SynchronizationContext since it is the wrong context. I do have the Control available for calling Control.BeginInvoke() however.

The actual situation is that I am servicing an RPC request which must update some UI using a method that happens to be async.


Solution

  • One idea could be to convert the mustRunOnUiThreadAsync from async Task to async void. Conceptually you could say that this method is the "event handler" of an "event" that is originated from the background thread, and so you are not violating Microsoft's guidelines about async void usage essentially. Example:

    async void mustRunOnUiThread()
    {
        label1.Text = "Before update";
        await Task.Delay(1000).ConfigureAwait(true);
        label1.Text = "After update";
    }
    

    ...and in the notRunningOnUiThread method:

    ui.BeginInvoke(new Action(() => mustRunOnUiThread()));
    

    This modification will not change the behavior of your current code in any way, except from making the VSTHRD101 warning go away.

    As a side note you could also consider converting the testBtn_Click handler to async void, so that the notRunningOnUiThread is not launched as a fire-and-forget Task:

    async void testBtn_Click(object sender, EventArgs e)
    {
        await Task.Run(() => notRunningOnUiThread(this));
    }