Reputation: 81
Sometimes we want the event publisher's actions post firing to depend on some state set by the event handlers. A common example is an event that takes CancelEventArgs
. The event publisher takes action after firing, conditioned on the value of the Cancel property. If the handler is long running, we don't want to block the UI thread (users can be doing other things while they are waiting). But async handlers may return before the state is set, and the publisher will not have the correct value when it needs it. I solved this by simply providing a mutable Task property in the event argument. It's the responsibility of the handler to set its value, and the publisher to await on it after firing. This seems to work fine.
One objection might be that stateful event args are arguably bad practice, unless you assume only one handler. You could use higher-order functions instead of events, and that would handle the objection above by enforcing one "handler".
One thing that I'm not sure about is how async/await deals with multiple awaiters.
Answer is yes. This feels like it's going be an issue for nested async methods generally, whenever the caller and callee both have actions after the await.
Thanks!
class Publisher
{
void RaiseMyEvent()
{
var e = new MyEventArgs();
OnRaiseMyEvent(e);
if (e.Task != null) await e.HandlerTask;
if (e.Cancel)
{
// Do one thing
}
else
{
// Do the other
}
}
}
class Subscriber
{
void MyEventHandler(object sender, CancelEventArgs e)
{
// Notify user to wait on process
e.Task = SomeAsyncMethod();
await e.Task;
e.Cancel = GetOutcome();
// Clear any notification
}
bool GetOutcome() { }
}
Actually we can avoid a race by ensuring that any needed state values in the event args needed by the firing method are set prior to the continuation in the handler:
class Subscriber
{
void MyEventHandler(object sender, CancelEventArgs e)
{
// Notify user to wait on process
e.Task = Task.Run(() =>
{
//Do stuff
e.Cancel = GetOutcome();
}
await e.Task;
// Clear any notification
}
bool GetOutcome() { }
}
Both continuations execute on the UI thread, but we don't care about the order.
Upvotes: 3
Views: 802
Reputation: 456887
Sometimes we want the event publisher's actions post firing to depend on some state set by the event handlers.
I call these "command events", as opposed to "notification events", and cover a few approaches to async command events on my blog.
After quite a bit of experience, I have come to the conclusion that command events are an anti-pattern. Events in .NET are designed as notification events, and making them behave differently is awkward at best. To use the GoF terminology, .NET events are used to implement the Observer design pattern, but these "command events" are actually an implementation of the Template Method design pattern.
Consider this quote about the Template Method design pattern (pg 328):
It's important for template methods to specify which operations are hooks (may be overridden) and which are abstract operations (must be overridden)
That's a good identifying quality of command events! And if you find yourself writing a .NET event that either requires a handler or can't have more than one handler, then it's a good indication that .NET events are probably the wrong solution.
If you have a Template Method situation, then usually some form of this will suffice as a solution:
interface IDetails { Task ProcessAsync(); }
class Subject
{
private IDetails _details { get; }
public Subject(IDetails details) { _details = details; }
async Task SomeMethodAsync()
{
...
if (_details)
await _details.ProcessAsync();
}
}
Upvotes: 3