Introduce InvokeAsync on Control
See original GitHub issueRationale
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:
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:
Issue Analytics
- State:
- Created 3 years ago
- Reactions:11
- Comments:46 (29 by maintainers)
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.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