4

I am familiar with how to write common delegates and subscribing to them, but is there a way to have the usefulness of delegates (subscribing and unsubscribing from other classes) with the awaitable functionality?

For example, with a simple delegate like this:

public delegate void SimpleDelegate();
public static SimpleDelegate OnDelegateInvoke;

I then can subscribe to it from another class like:

    public void SomeFunction(){};

    OnDelegateInvoke += SomeFunction();

The behaviour I want is for the OnDelegateInvoke call to be awaitable, so it awaits there until all subscribed functions complete:

     await OnDelegateInvoke?.Invoke();
     await DoSomethingThatNeedsAboveCompleted();

I tried writing the delegates with a Task return type but to my understanding that wouldn't work since there are multiple functions returning multiple tasks, so the await would only wait for the first Task to complete.

Since thinking about this I am also not sure if this completely breaks the paradigm of why delegates are useful, so answers about that are also appreciated.

Calvin So
  • 43
  • 3
  • 1
    Do you need to wait for all handler to complete or you are ok with just starting them (which is what you get for free)? (I'm pretty sure you need to manually enumerate all handlers and call/await them individually... but I'm not an expert of delegates) – Alexei Levenkov Jul 30 '21 at 18:25
  • Delegates may not be the appropriate data type to use for the problem you are trying to solve. By design, you don't know when a caller will add a listener to them, or when they will be invoked. As a result, trying to wrap them in a call similar to an async/await operation or a Task.Run is problematic; you won't be able to catch all invocations, and if you don't take care, you could write code that blocks for a *very* long time. – Mike Hofer Jul 30 '21 at 19:46
  • I was wondering if they makes sense and it does seem pretty dangerous. The reason why I wanted to make this approach is because these delegates sit in a SceneLoading context in Unity, and I want to be able to call other functions that prepare the scene from delegates fired during the scene load process; however, not every scene has the same list of functions that need to be called to prepare the scene. Is there an alternative solution to this? – Calvin So Jul 30 '21 at 19:49
  • Related: [How to 'await' raising an EventHandler event](https://stackoverflow.com/questions/12451609/how-to-await-raising-an-eventhandler-event) and [How do I await events in C#?](https://stackoverflow.com/questions/27761852/how-do-i-await-events-in-c) – Theodor Zoulias Jul 30 '21 at 20:20

1 Answers1

2

You could do this with delegates returning a Task:

public delegate Task SimpleAsyncDelegate();
public static SimpleAsyncDelegate OnAsyncDelegateInvoke;

To invoke and await all added delegates, we invoke them one-by-one and await the produced Tasks:

var delegateTasks = OnAsyncDelegateInvoke.GetInvocationList()
    .Cast<SimpleAsyncDelegate>()
    .Select(del => del.Invoke());
await Task.WhenAll(delegateTasks);

For an example with and without awaiting see this fiddle.


To address the question in the comments, I think the closest you get to a more generic way of awaiting all delegates, is a helper class similar to the following:

public class DelegateHelper
{
    public static async Task WhenAllDelegates(Delegate del)
    {
        var delegateTasks = del.GetInvocationList()
            .Select(del => del.DynamicInvoke())
            .Where(obj => obj is Task)
            .Cast<Task>();
        await Task.WhenAll(delegateTasks);
    }
}

You can use it like so:

await DelegateHelper.WhenAllDelegates(OnAsyncDelegateInvoke);

NOTE: For the generic purpose we need to use DynamicInvoke, which according to this answer, performs significantly worse. Whether this performance is really an issue depends on the use-case, so you can test to see if it's worth it.

Xerillio
  • 4,855
  • 1
  • 17
  • 28
  • This works! One question I have is, is there a way to set it up as a general class that helps run async delegates? – Calvin So Jul 30 '21 at 22:47
  • @CalvinSo I've updated my answer. Be aware of the performance impact with this solution though, if you need to use it in a performance critical part of your code. – Xerillio Jul 31 '21 at 10:21
  • thanks for that answer! That makes sense. I will probably steer away from using DynamicInvoke then, but this is super informative! Thank you! – Calvin So Aug 04 '21 at 17:55