question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Introduce InvokeAsync on Control

See original GitHub issue

Rationale

With the arrival of the WebView2 control, which introduces APIs for WinForms which can only be called asynchronously and therefore need to be awaited, and also with the option from .NET 5 on to project UWP/WinUI APIs - many of which can also only be called asynchronously - there is also a new requirement to await methods, which can only be executed from the UI Thread.

The EnsureCoreWebView2Async method of the WebView2 control has this requirement for example. It can only be called from the UI thread. When the current thread is not the UI thread, then there is no easy way to call this method, because async methods are not compatible with the current Invoke method’s signature to delegate calls to the UI thread.

Other scenarios are, when asynchronous methods like NavigateTo are signaling their completion by raising an event, and those methods - which also needs to be called of on the UI Thread - should be awaited e.g. via a TaskCompletionSource.

Proposed API

Add a method to Control with the following signatures:

public Task InvokeAsync(
    Func<Task> invokeDelegate,
    TimeSpan timeOutSpan = default,
    CancellationToken cancellationToken = default,
    params object[] args) 

to invoke awaiting a method on the UI thread which doesn’t return any results (so its return type is - because it’s awaitable - of type Task), and…

public async Task<T> InvokeAsync<T>(
    Func<Task<T>> invokeDelegate,
    TimeSpan timeOutSpan = default,
    CancellationToken cancellationToken = default,
    params object[] args)

to return a result value.

Real World Sample

Consider in a typical WinForms LOB App DataBinding-Scenario, we have a couple of Extender methods for a business logic to aggregate reports (Revenue numbers), which extends BindingList:

// Sync version:
bindingList.AddCustomerReportItem(customerId)

// Async version:
bindingList.AddCustomerReportItemAsync(customerId)

Both methods need to be called on the UI Thread, since both are updating the UI via Binding. However, aggregating the result to generate a Report Item does take a while, since a lot of data has to be taken into account.

The typical Code-behind WinForms solution would look like this:

            // Simulate getting the Data from a Report engine,
            // and displaying them in the Grid via Binding.
            var bindingList = new BindingList<CustomerReportItem>();
            customerReportItemBindingSource.DataSource = bindingList;

            // We use the ReportEngine...
            var reportEngine = new ReportEngine();

            // ...register to be called back, when it's finished, and 
            // update the UI inside it's EventHandler.
            reportEngine.ReportCompilationCompleted += (sender, eventArgs) =>
              {
                  foreach (var customerId in eventArgs.CustomerIds)
                  {
                      bindingList.AddCustomerReportItem(customerId);
                  }
              };

            // We're kicking of the Report compilation
            // and waiting for the result event to arrive.
            reportEngine.CompileReport();

The problem here: The code crashes with a Cross-Thread exception, because we’re trying to add to a BindingSource, which causes the UI to get updated, but we’re doing this in the EventHandler of ReportCompilationCompleted, and that’s not raised on the UI Thread.

Note: This is not a stilted scenario. There are other events in real live scenarios, where this happens. WebView2.NavigateTo, Timer Ticks, EndInvokes in WinForm-Components which often raise events, Notification Events from UWP, etc.

So, what we can do in our case, is just marshal that call to the UI thread. But that does not help a lot, since the Task we’re running on is utilizing the UI thread to full capacity. The App becomes unresponsive:

AsyncInvokeDemo1

So, instead we could now use the Async version of AddCustomerReportItem in the EventHandler, like this:

    // ...register to be called back, when it's finished, and 
    // update the UI inside it's EventHandler.
    reportEngine.ReportCompilationCompleted += async (sender, eventArgs) =>
    {
        foreach (var customerId in eventArgs.CustomerIds)
        {
            // OK, so this time: Let's Invoke that to marshal it
            // to the UI thread. But...
            await bindingList.AddCustomerReportItemAsync(customerId);
        }
    };

But again, if we do it, we’re hitting the Cross-Thread Exception. And this is the Problem now:

We should be able to do the Invoke now just with the WinForms standard tools. The call would look like this:

    // ...register to be called back, when it's finished, and 
    // update the UI inside it's EventHandler.
    reportEngine.ReportCompilationCompleted += async (sender, eventArgs) =>
    {
        foreach (var customerId in eventArgs.CustomerIds)
        {
            // OK, so this time: Let's Invoke that to marshal it
            // to the UI thread. But...
            await asyncControl.InvokeAsync(() => bindingList.AddCustomerReportItemAsync(customerId));
        }
    };

And the result would look like this: AsyncInvokeDemo2

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:11
  • Comments:46 (29 by maintainers)

github_iconTop GitHub Comments

4reactions
Eiloncommented, Jun 24, 2021

We would LOVE to have this as well for Blazor Desktop scenarios. Blazor Desktop supports running Blazor web apps natively in WinForms apps in WebView2. There’s a lot of async stuff in WebView2, and Blazor is fully async. When the new BlazorWebView WinForms control is disposed, we need a way to call IAsyncDisposable.DisposeAsync() on its dependencies, but there’s no good way to do that in WinForms.

4reactions
RussKiecommented, Mar 10, 2021

It is great to see the excitement and passion everyone has for making Windows Forms more async/await-friendly. However it looks like the discussion is going off the topic, which is requirements/feasibility/necessity for Control.InvokeAsync API.

In the interest of retaining the discussion focused on the subject, may I please ask you to asks unrelated to this topic questions at https://github.com/dotnet/winforms/discussions, and if you have any API proposals (unrelated to this topic) - please start those separately.

Thank you

Read more comments on GitHub >

github_iconTop Results From Across the Web

Thread safety using InvokeAsync
The Blazor InvokeAsync , introduced on the class ComponentBase , will ensure that race conditions do not occur by synchronizing thread execution per-user ......
Read more >
Using async/await with Dispatcher.BeginInvoke()
uses the Dispatcher.Invoke(Action callback) override form of Dispatcher.Invoke , which accepts an async void lambda in this particular case.
Read more >
Invoking and Sharing View Components in ASP.NET Core
For my money, the simplest method is to simply call the InvokeAsync method from the View's Component property, passing the name of your...
Read more >
How to pass parameters to a view component
Passing parameters to view components using InvokeAsync​​ Our component is all set up to conditionally show or hide the register link, all that ......
Read more >
View Components in Asp.Net Core
View Components implement either Invoke or InvokeAsync. InvokeAsync is attempted to be found first, and if that doesn't exist, Invoke is attempted to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found