问题
I'm trying to await a callback that is fired when a button is pressed. The important point is that I want to wait for the callback from a simple await
without reshaping the code. In other words I want to achieve the following:
internal async Task BatchLogic()
{
ProgressMessage = "Batch Logic Starts";
await OnCallbackFired();
ProgressMessage = "Batch Logic Ends";
}
my attempt is
internal async Task BatchLogic()
{
ProgressMessage = "Batch Logic Starts";
await Task.Factory.FromAsync(beginMethod, endMethod, state);
ProgressMessage = "Batch Logic Ends";
}
with the following defintions
private object state = null;
private void endMethod(IAsyncResult obj)
{
IsBusy = false;
}
private AsyncCallback callback;
private IAsyncResult beginMethod(AsyncCallback callback, object state)
{
return Task.FromResult(true);
}
When the button is pressed, the following code is executed:
private async void RunNext()
{
isBusy = true;
await workToDo();
isBusy = false; // the first 3 lines are not relevant
callback = new AsyncCallback(endMethod);
callback.Invoke(Task.FromResult(true));
}
The problem is that the endMethod
is called from the callback.Invoke
but the Factory.FromAsync
never returns, very likely because I've not understood how to use it and I've not found an example corresponding to what I'm trying to achieve.
回答1:
To await
something, it needs to be a Task
(this is an oversimplification, but that's the important part) (C# can await
any object that exposes a TaskAwaiter GetAwaiter()
method, but 95% of the time C# developers will be consuming Task
/Task<T>
objects)..
To create a Task
you use TaskCompletionSource
. This is how you can create a Task
representation around other conceptual "tasks", like the IAsyncResult
-style APIs, threads, overlapped IO, and so on.
Assuming this is WinForms, then do this:
class MyForm : Form
{
// Note that `TaskCompletionSource` must have a generic-type argument.
// If you don't care about the result then use a dummy null `Object` or 1-byte `Boolean` value.
private TaskCompletionSource<Button> tcs;
private readonly Button button;
public MyForm()
{
this.button = new Button() { Text = "Click me" };
this.Controls.Add( this.button );
this.button.Click += this.OnButtonClicked;
}
private void OnButtonClicked( Object sender, EventArgs e )
{
if( this.tcs == null ) return;
this.tcs.SetResult( (Button)sender );
}
internal async Task BatchLogicAsync()
{
this.tcs = new TaskCompletionSource<Button>();
ProgressMessage = "Batch Logic Starts";
Button clickedButton = await this.tcs.Task;
ProgressMessage = "Batch Logic Ends";
}
}
回答2:
Here is an extension method you can use. Unsubscribing from the Click
event is important to avoid memory leaks, because event publishers keep references of event subscribers.
public static Task OnClickAsync(this Control source)
{
var tcs = new TaskCompletionSource<bool>();
source.Click += OnClick;
return tcs.Task;
void OnClick(object sender, EventArgs e)
{
source.Click -= OnClick;
tcs.SetResult(true);
}
}
Usage example:
await Button1.OnClickAsync();
回答3:
This is the fix for my initial code, that makes it work as expected. I had to define a closure for the callback
AsyncCallback callback;
Then I had to pass the callback
to the closure from the beginMethod
:
private IAsyncResult beginMethod(AsyncCallback callback, object state)
{
this.callback = callback;
return Task.FromResult(true);
}
and finally invoke it from the event (i.e. the Command
method in the MVVM for WPF)
private async void RunNext()
{
IsBusy = true;
ProgressMessage = "Wait 10 seconds...";
await workToDo();
ProgressMessage = "Work done!";
IsBusy = false;
if (callback != null)
{
callback.Invoke(Task.FromResult(true));
}
}
来源:https://stackoverflow.com/questions/58340162/how-to-await-an-event-click