AnotherProgrammer
AnotherProgrammer

Reputation: 573

How to invoke an event asynchronously?

I'm building a control that will be used by other developers, and in a few places I have events that the developer can subscribe to do some potentially long operations.

My goal is to give them the option to use async/await, multithreading, backgroundworkers etc. if they choose, but still be able to complete the execution before the event invocation completes. Here's a psuedo-code example (I realize this doesn't compile, but hopefully the intention is clear):

My Code:

public event MyEventHandler MyEvent;
private void InvokeMyEvent()
{
    var args = new MyEventArgs();

    // Keep the UI responsive until this returns
    await MyEvent?.Invoke(this, args);

    // Then show the result
    MessageBox.Show(args.Result);
}

Developer's/subscriber's potential code:

// Option 1: The operation is very quick,
// so the dev doesn't mind doing it synchronously
private void myForm_MyEvent(object sender, MyEventArgs e)
{
    // Short operation, I'll just do it right here.
    string result = DoQuickOperation();
    e.Result = result;
}

// Option 2, The operation is long,
// so the dev wants to do it on another thread and keep the UI responsive.
private void myForm_MyEvent(object sender, MyEventArgs e)
{
    myForm.ShowProgressPanel();

    // Long operation, I want to multithread this!
    Task.Run(() =>
    {
        string result = DoVeryLongOperation();
        e.Result = result;
    })
    .ContinueWith((task) =>
    {
        myForm.HideProgressPanel();
    }
}

Is there any standard way to accomplish this? I was looking at the delegate's BeginInvoke and EndInvoke methods hoping they would help, but I didn't really come up with anything.

Any help or advice would be appreciated! Thanks!

Upvotes: 4

Views: 9288

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 457332

You may find my blog post on async events useful.

There is no strict convention, but there is a loose convention due to the way the WinRT/Win10 APIs handled this problem: deferrals. I have a DeferralManager type that helps out here:

class MyEventArgs : IDeferralSource
{
  private IDeferralSource _deferralSource;
  public MyEventArgs(IDeferralSource deferralSource) { _deferralSource = deferralSource; }
  IDisposable GetDeferral() => _deferralSource.GetDeferral();

  ... // Other properties
}

public event MyEventHandler MyEvent;
private async Task InvokeMyEventAsync()
{
  var deferralManager = new DeferralManager();
  var args = new MyEventArgs(deferralManager.DeferralSource);

  MyEvent?.Invoke(this, args);

  await deferralManager.WaitForDeferralsAsync();

  MessageBox.Show(args.Result);
}

This approach has the nice property that synchronous event handlers are exactly like normal, while allowing asynchronous event handlers. But it's not ideal; in particular, there's nothing that forces an asynchronous event handler to acquire a deferral - and if it doesn't, then it won't work as expected.

However, I must caution you that using events for this kind of a design is suspect in the first place. In .NET, events are an appropriate implementation of the Observer design pattern. But your code isn't using Observer; it's using the Template Method design pattern. The design problems you're running into are because you're attempting to use events to implement the Template Method design pattern. You can force it to fit (in a few different ways), but it won't be ideal.

Upvotes: 4

user743382
user743382

Reputation:

There is no standard way of doing this.

My personal preference, when I was in this situation, was to use a custom delegate type returning Task. Handlers which return synchronously can simply return Task.CompletedTask, they don't need to create a new task for this.

public delegate Task MyAsyncEventHandler(object sender, MyEventArgs e);
public event MyAsyncEventHandler MyEvent;
private async Task InvokeMyEvent()
{
    var args = new MyEventArgs();

    // Keep the UI responsive until this returns
    var myEvent = MyEvent;
    if (myEvent != null)
        await Task.WhenAll(Array.ConvertAll(
          myEvent.GetInvocationList(),
          e => ((MyAsyncEventHandler)e).Invoke(this, args)));

    // Then show the result
    MessageBox.Show(args.Result);
}

Note the capture of myEvent in a local variable to avoid threading issues, and the use of GetInvocationList() to ensure all tasks are awaited.

Note that in this implementation, if you have multiple event handlers, they are all scheduled at once so may execute in parallel. The handlers need to ensure they do not access args in thread-unsafe ways.

Depending on your use case, executing the handlers sequentially (await in a loop) could be more appropriate.

Upvotes: 8

Related Questions