206

In my C#/XAML metro app, there's a button which kicks off a long-running process. So, as recommended, I'm using async/await to make sure the UI thread doesn't get blocked:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

Occasionally, the stuff happening within GetResults would require additional user input before it can continue. For simplicity, let's say the user just has to click a "continue" button.

My question is: how can I suspend the execution of GetResults in such a way that it awaits an event such as the click of another button?

Here's an ugly way to achieve what I'm looking for: the event handler for the continue" button sets a flag...

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

... and GetResults periodically polls it:

 buttonContinue.Visibility = Visibility.Visible;
 while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
 buttonContinue.Visibility = Visibility.Collapsed;

The polling is clearly terrible (busy waiting / waste of cycles) and I'm looking for something event-based.

Any ideas?

Btw in this simplified example, one solution would be of course to split up GetResults() into two parts, invoke the first part from the start button and the second part from the continue button. In reality, the stuff happening in GetResults is more complex and different types of user input can be required at different points within the execution. So breaking up the logic into multiple methods would be non-trivial.

Tim M.
  • 53,671
  • 14
  • 120
  • 163
Max
  • 9,220
  • 10
  • 51
  • 83

11 Answers11

299

You can use an instance of the SemaphoreSlim Class as a signal:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Alternatively, you can use an instance of the TaskCompletionSource<T> Class to create a Task<T> that represents the result of the button click:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;
dtb
  • 213,145
  • 36
  • 401
  • 431
  • I would have used a `ManualResetEvent`. Is there an advantage to using `SemaphoreSlim` or could you use either one? – Daniel Hilgarth Oct 12 '12 at 12:03
  • 9
    @DanielHilgarth `ManualResetEvent(Slim)` doesn't seem to support `WaitAsync()`. – svick Oct 12 '12 at 12:13
  • @svick: Good point. However, as `GetResult` already is `async` you could block inside this method without any problems, couldn't you? – Daniel Hilgarth Oct 12 '12 at 12:20
  • 3
    @DanielHilgarth No, you couldn't. `async` doesn't mean “runs on a different thread”, or something like that. It just means “you can use `await` in this method”. And in this case, blocking inside `GetResults()` would actually block the UI thread. – svick Oct 12 '12 at 12:24
  • @svick: I agree, `async` doesn't automatically create a new thread. But in combination with `await` it does, doesn't it? So in this specific example, it *wouldn't* block the UI thread, would it? – Daniel Hilgarth Oct 12 '12 at 12:25
  • @svick "Blocking inside `GetResults()` would actually block the UI thread." - This is false. Blocking in `GetResults` would *not* block the UI thread because until `GetResults` returns, it's running asynchronously. The background thread/task will block, but not the UI thread. The UI thread was left the moment `GetResults` was called. Unless the awaitable from `GetResult` actually doesn't go out to another thread (and it's almost always the case that it does, or it's waiting on some IO completion), it won't block. – casperOne Oct 12 '12 at 13:25
  • 2
    @Gabe `await` in itself does not guaranted that another thread is created, but it causes everything else *after* the statement to run as a continuation on the `Task` or awaitable that you call `await` on. More often than not, it is *some* sort of asynchronous operation, which could be IO completion, or something that *is* on another thread. – casperOne Oct 12 '12 at 13:26
  • @casperOne But there is no background task in the code in the question. `GetResults()` is called directly from event handler, which means it runs on the UI context. It would not block the UI thread if there was something like `Task.Run()` or `ConfigureAwait(false)` somewhere, but there is no such thing in there. – svick Oct 12 '12 at 13:45
  • 1
    @svick `GetResults` returns a `Task` which is awaited on. This means that the `Task` runs, and the method that is awaiting actually *exits*. Then, a continuation is created that continues the remaining code when the thing you `await` on completes (using a `SynchronizationContext`, if there is one, and not told to not use it). The `Task` runs asynchronously, and then the remaining code in the event handler is marshaled back to the UI thread on the synchronization context when the `Task` is complete. – casperOne Oct 12 '12 at 14:00
  • 1
    @casperOne Sure, but “runs asynchronously” doesn't mean “runs on another thread”. `GetResults()` is still on the UI context. – svick Oct 12 '12 at 14:03
  • @svick No, `GetResults` *continues* on the UI context. That's a major difference. Most methods on `Task` that create `Task` instances do *not* capture synchronization context. It's *awaiting* on them that does. The *actual* task that does the background work doesn't capture this context at all. – casperOne Oct 12 '12 at 14:05
  • 2
    @casperOne My point is that there is no code in this question that explicitly creates a `Task`, so all of the code will run on the UI thread. – svick Oct 12 '12 at 14:06
  • @svick No, but if you have `async` then you have to have `await`, unless you're doing something *way* outside the norm, that `await` is going to be on a `Task` that is based on another thread, or waiting on an IO completion which will continue on another thread. Either way, the wait is taken off the UI thread, and then resumed on the UI thread when that particular operation (no matter how many layers deep its buried) is executed. You're not blocking the *entire* time. – casperOne Oct 12 '12 at 14:11
  • 1
    For others who want to see more, see here: http://chat.stackoverflow.com/rooms/17937 - @svick and I basically misunderstood each other, but were saying the same thing. – casperOne Oct 12 '12 at 14:26
  • 18
    +1. I had to look this up, so just in case others are interested: `SemaphoreSlim.WaitAsync` does not just push the `Wait` onto a thread pool thread. `SemaphoreSlim` has a proper queue of `Task`s that are used to implement `WaitAsync`. – Stephen Cleary Oct 12 '12 at 20:29
  • 20
    TaskCompletionSource + await .Task + .SetResult() turns out to be the perfect solution for my scenario - thanks! :-) – Max Oct 16 '12 at 20:58
  • Using the `TaskCompletionSource`, `await tcs.Task`, `tcs.SetResult()` turned out to be the easiest way of awaiting an event. Far easier, in fact, than the Event-based Asynchronous Pattern [demonstrated on MSDN](https://msdn.microsoft.com/en-us/library/bz33kx67.aspx "Walkthrough: Implementing a Component That Supports the Event-based Asynchronous Pattern"). – Alex Essilfie Aug 21 '15 at 01:58
  • 1
    A lot of confusion between parallelism and asnchronism. You can have a perfectly asynchronous environment that runs on one and only thread. Look at javascript on browsers. It runs only on one thread, but the code is mostly asynchronous (with all the callbacks there, and now with Promises). An async environment may be based on an event loop. Everything is processed in one thread but in different moments in time. An async operation is usually sequential in itself, not parallel. It's "do this, when you finish do that, ...etc". Parallelism comes in when you also define how and where to do the job! – Thanasis Ioannidis Oct 15 '19 at 21:02
  • Thank you, TaskCompletionSource is exactly what I need. I need to postpone execution of HostedServices until application ready. – Vyacheslav Borisov Oct 25 '22 at 02:49
94

When you have an unusual thing you need to await on, the easiest answer is often TaskCompletionSource (or some async-enabled primitive based on TaskCompletionSource).

In this case, your need is quite simple, so you can just use TaskCompletionSource directly:

private TaskCompletionSource<object> continueClicked;

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
  // Note: You probably want to disable this button while "in progress" so the
  //  user can't click it twice.
  await GetResults();
  // And re-enable the button here, possibly in a finally block.
}

private async Task GetResults()
{ 
  // Do lot of complex stuff that takes a long time
  // (e.g. contact some web services)

  // Wait for the user to click Continue.
  continueClicked = new TaskCompletionSource<object>();
  buttonContinue.Visibility = Visibility.Visible;
  await continueClicked.Task;
  buttonContinue.Visibility = Visibility.Collapsed;

  // More work...
}

private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
  if (continueClicked != null)
    continueClicked.TrySetResult(null);
}

Logically, TaskCompletionSource is like an async ManualResetEvent, except that you can only "set" the event once and the event can have a "result" (in this case, we're not using it, so we just set the result to null).

Stécy
  • 11,951
  • 16
  • 64
  • 89
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 5
    Since I parse "await an event" as basically the same situation as 'wrap EAP in a task', I'd definitely prefer this approach. IMHO, it's definitely simpler / easier-to-reason-about code. – James Manning Oct 12 '12 at 15:38
  • 1
    And just in case you are wondering about disposing, [here](https://stackoverflow.com/a/54769131/2441442) is the answer, from the same author. – Christian Gollhardt Sep 19 '22 at 17:24
7

Here is a utility class that I use:

public class AsyncEventListener
{
    private readonly Func<bool> _predicate;

    public AsyncEventListener() : this(() => true)
    {

    }

    public AsyncEventListener(Func<bool> predicate)
    {
        _predicate = predicate;
        Successfully = new Task(() => { });
    }

    public void Listen(object sender, EventArgs eventArgs)
    {
        if (!Successfully.IsCompleted && _predicate.Invoke())
        {
            Successfully.RunSynchronously();
        }
    }

    public Task Successfully { get; }
}

And here is how I use it:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
Anders Skovborg
  • 234
  • 3
  • 5
  • 1
    I don't know how this works. How is the Listen method asynchronously executing my custom handler? Wouldn't `new Task(() => { });` be instantly completed? – nawfal Dec 06 '19 at 09:44
5

Ideally, you don't. While you certainly can block the async thread, that's a waste of resources, and not ideal.

Consider the canonical example where the user goes to lunch while the button is waiting to be clicked.

If you have halted your asynchronous code while waiting for the input from the user, then it's just wasting resources while that thread is paused.

That said, it's better if in your asynchronous operation, you set the state that you need to maintain to the point where the button is enabled and you're "waiting" on a click. At that point, your GetResults method stops.

Then, when the button is clicked, based on the state that you have stored, you start another asynchronous task to continue the work.

Because the SynchronizationContext will be captured in the event handler that calls GetResults (the compiler will do this as a result of using the await keyword being used, and the fact that SynchronizationContext.Current should be non-null, given you are in a UI application), you can use async/await like so:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();

     // Show dialog/UI element.  This code has been marshaled
     // back to the UI thread because the SynchronizationContext
     // was captured behind the scenes when
     // await was called on the previous line.
     ...

     // Check continue, if true, then continue with another async task.
     if (_continue) await ContinueToGetResultsAsync();
}

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

ContinueToGetResultsAsync is the method that continues to get the results in the event that your button is pushed. If your button is not pushed, then your event handler does nothing.

casperOne
  • 73,706
  • 19
  • 184
  • 253
  • What async thread? There is no code that will *not* run on the UI thread, both in the original question and in your answer. – svick Oct 12 '12 at 13:53
  • 1
    @svick Not true. `GetResults` returns a `Task`. `await` simply says "run the task, and when the task is done, continue the code after this". Given that there is a synchronization context, the call is marshaled back to the UI thread, as it's captured on the `await`. `await` is *not* the same as `Task.Wait()`, not in the least. – casperOne Oct 12 '12 at 13:59
  • I didn't say anything about `Wait()`. But the code in `GetResults()` will run on the UI thread here, there is no other thread. In other words, yes, `await` basically does run the task, like you say, but here, that task also runs on the UI thread. – svick Oct 12 '12 at 14:01
  • @svick There's no reason to make the assumption that the task runs on the UI thread, why do you make that assumption? It's *possible*, but unlikely. And the call is two separate UI calls, technically, one up to the `await` and then the code after `await`, there is no blocking. The rest of the code is marshaled back in a continuation and scheduled through the `SynchronizationContext`. – casperOne Oct 12 '12 at 14:03
  • I'm not making any assumptions, I'm just looking at the code as it is. And as it is, the code inside `GetResults()` will run on the UI thread. I think it's you that makes assumptions that `GetResults()` will contain something like `Task.Run()`. But that's not what I'm talking about, I'm talking about code directly in `GetResults()`. – svick Oct 12 '12 at 14:04
  • @svick Based on *what* exactly? *Parts* of it will run, but at some point, there has to be *something* that it awaits on that's going to be `Task` based, and then *that* code will run asynchronous, and everything else will continue on the UI thread, but in a continuation call. Those points up to and after the asynchronous task will be on the UI thread, but there will be a background operation (unless you do something like say run synchronously or some other edge case) that is not, and that's where the blocking is broken up. – casperOne Oct 12 '12 at 14:09
  • @svick You can't have `async` without `await`, so what exactly are you awaiting on that *isn't* run on some other thread, or waiting on IO completion? – casperOne Oct 12 '12 at 14:09
  • 1
    For others who want to see more, see here: http://chat.stackoverflow.com/rooms/17937 - @svick and I basically misunderstood each other, but were saying the same thing. – casperOne Oct 12 '12 at 14:27
5

Simple Helper Class:

public class EventAwaiter<TEventArgs>
{
    private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();

    private readonly Action<EventHandler<TEventArgs>> _unsubscribe;

    public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
    {
        subscribe(Subscription);
        _unsubscribe = unsubscribe;
    }

    public Task<TEventArgs> Task => _eventArrived.Task;

    private EventHandler<TEventArgs> Subscription => (s, e) =>
        {
            _eventArrived.TrySetResult(e);
            _unsubscribe(Subscription);
        };
}

Usage:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Felix Keil
  • 2,344
  • 1
  • 25
  • 27
  • 1
    How would you cleanup the subscription to `example.YourEvent`? – Denis P Mar 09 '18 at 19:39
  • @DenisP perhaps pass the event into constructor for EventAwaiter? – CJBrew Mar 19 '18 at 14:06
  • @DenisP I improved the version and run a short test. – Felix Keil Mar 19 '18 at 14:51
  • I could see adding IDisposable as well, depending on circumstances. Also, to avoid having to type in the event twice, we could also use Reflection to pass the event name, so then the usage is even simpler. Otherwise, I like the pattern, thank you. – Denis P Mar 21 '18 at 15:35
3

Stephen Toub published this AsyncManualResetEvent class on his blog.

public class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return m_tcs.Task; } 

    public void Set() 
    { 
        var tcs = m_tcs; 
        Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), 
            tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); 
        tcs.Task.Wait(); 
    }
    
    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = m_tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
2

I'm using my own AsyncEvent class for awaitable events.

public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;

public class AsyncEvent : AsyncEvent<EventArgs>
{
    public AsyncEvent() : base()
    {
    }
}

public class AsyncEvent<T> where T : EventArgs
{
    private readonly HashSet<AsyncEventHandler<T>> _handlers;

    public AsyncEvent()
    {
        _handlers = new HashSet<AsyncEventHandler<T>>();
    }

    public void Add(AsyncEventHandler<T> handler)
    {
        _handlers.Add(handler);
    }

    public void Remove(AsyncEventHandler<T> handler)
    {
        _handlers.Remove(handler);
    }

    public async Task InvokeAsync(object sender, T args)
    {
        foreach (var handler in _handlers)
        {
            await handler(sender, args);
        }
    }

    public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        var result = left ?? new AsyncEvent<T>();
        result.Add(right);
        return result;
    }

    public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        left.Remove(right);
        return left;
    }
}

To declare an event in the class that raises events:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

To raise the events:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

To subscribe to the events:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}
cat_in_hat
  • 635
  • 9
  • 15
  • 2
    You have completely invented a new event handler mechanism. Maybe this is what delegates in .NET are translated to eventually, but can't expect people to adopt this. Having a return type for the delegate (of the event) itself can put people off to begin with. But good effort, really like how well it is done. – nawfal Dec 05 '19 at 14:21
  • @nawfal Thanks! I've modified it since to avoid returning a delegate. The source is available [here](https://github.com/integrativesoft/lara/blob/master/src/LaraUI/Tools/AsyncEvent.cs) as part of Lara Web Engine, an alternative to Blazor. – cat_in_hat Dec 05 '19 at 20:57
1

With Reactive Extensions (Rx.Net)

var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => example.YourEvent += h,
                h => example.YourEvent -= h);

var res = await eventObservable.FirstAsync();

You can add Rx with Nuget Package System.Reactive

Tested Sample:

    private static event EventHandler<EventArgs> _testEvent;

    private static async Task Main()
    {
        var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => _testEvent += h,
                h => _testEvent -= h);

        Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));

        var res = await eventObservable.FirstAsync();

        Console.WriteLine("Event got fired");
    }
Felix Keil
  • 2,344
  • 1
  • 25
  • 27
0

Here is a small toolbox of six methods, that can be used for converting events to tasks:

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(
    Action<EventHandler> addHandler,
    Action<EventHandler> removeHandler)
{
    var tcs = new TaskCompletionSource<object>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, EventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(null);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    Action<EventHandler<TEventArgs>> addHandler,
    Action<EventHandler<TEventArgs>> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(object sender, TEventArgs e)
    {
        removeHandler(Handler);
        tcs.SetResult(e);
    }
}

/// <summary>Converts a .NET event, conforming to the standard .NET event pattern
/// based on a supplied event delegate type, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TDelegate, TEventArgs>(
    Action<TDelegate> addHandler, Action<TDelegate> removeHandler)
{
    var tcs = new TaskCompletionSource<TEventArgs>();
    TDelegate handler = default;
    Action<object, TEventArgs> genericHandler = (sender, e) =>
    {
        removeHandler(handler);
        tcs.SetResult(e);
    };
    handler = (TDelegate)(object)genericHandler.GetType().GetMethod("Invoke")
        .CreateDelegate(typeof(TDelegate), genericHandler);
    addHandler(handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler"/>, to a Task.</summary>
public static Task EventToAsync(object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<object>();
    EventHandler handler = default;
    handler = new EventHandler((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(null);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a named .NET event, conforming to the standard .NET event
/// pattern based on <see cref="EventHandler{TEventArgs}"/>, to a Task.</summary>
public static Task<TEventArgs> EventToAsync<TEventArgs>(
    object target, string eventName)
{
    var type = target.GetType();
    var eventInfo = type.GetEvent(eventName);
    if (eventInfo == null) throw new InvalidOperationException("Event not found.");
    var tcs = new TaskCompletionSource<TEventArgs>();
    EventHandler<TEventArgs> handler = default;
    handler = new EventHandler<TEventArgs>((sender, e) =>
    {
        eventInfo.RemoveEventHandler(target, handler);
        tcs.SetResult(e);
    });
    eventInfo.AddEventHandler(target, handler);
    return tcs.Task;
}

/// <summary>Converts a generic Action-based .NET event to a Task.</summary>
public static Task<TArgument> EventActionToAsync<TArgument>(
    Action<Action<TArgument>> addHandler,
    Action<Action<TArgument>> removeHandler)
{
    var tcs = new TaskCompletionSource<TArgument>();
    addHandler(Handler);
    return tcs.Task;

    void Handler(TArgument arg)
    {
        removeHandler(Handler);
        tcs.SetResult(arg);
    }
}

All these methods are creating a Task that will complete with the next invocation of the associated event. This task can never become faulted or canceled, it may only complete successfully.

Usage example with a standard event (Progress<T>.ProgressChanged):

var p = new Progress<int>();

//...

int result = await EventToAsync<int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<EventHandler<int>, int>(
    h => p.ProgressChanged += h, h => p.ProgressChanged -= h);

// ...or...

int result = await EventToAsync<int>(p, "ProgressChanged");

Usage example with a non-standard event:

public static event Action<int> MyEvent;

//...

int result = await EventActionToAsync<int>(h => MyEvent += h, h => MyEvent -= h);

The event is unsubscribed when the task is completed. No mechanism is provided for unsubscribing earlier than that.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

Here is a class I used for testing, which support CancellationToken.

This Test method shows us awaiting an instance of ClassWithEvent's MyEvent to be raised. :

    public async Task TestEventAwaiter()
    {
        var cls = new ClassWithEvent();

        Task<bool> isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(3));

        cls.Raise();
        Assert.IsTrue(await isRaisedTask);
        isRaisedTask = EventAwaiter<ClassWithEvent>.RunAsync(
            cls, 
            nameof(ClassWithEvent.MyMethodEvent), 
            TimeSpan.FromSeconds(1));

        System.Threading.Thread.Sleep(2000);

        Assert.IsFalse(await isRaisedTask);
    }

Here's the event awaiter class.

public class EventAwaiter<TOwner>
{
    private readonly TOwner_owner;
    private readonly string _eventName;
    private readonly TaskCompletionSource<bool> _taskCompletionSource;
    private readonly CancellationTokenSource _elapsedCancellationTokenSource;
    private readonly CancellationTokenSource _linkedCancellationTokenSource;
    private readonly CancellationToken _activeCancellationToken;
    private Delegate _localHookDelegate;
    private EventInfo _eventInfo;

    public static Task<bool> RunAsync(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        return (new EventAwaiter<TOwner>(owner, eventName, timeout, cancellationToken)).RunAsync(timeout);
    }
    private EventAwaiter(
        TOwner owner,
        string eventName,
        TimeSpan timeout,
        CancellationToken? cancellationToken = null)
    {
        if (owner == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(owner)));
        if (eventName == null) throw new TypeInitializationException(this.GetType().FullName, new ArgumentNullException(nameof(eventName)));

        _owner = owner;
        _eventName = eventName;
        _taskCompletionSource = new TaskCompletionSource<bool>();
        _elapsedCancellationTokenSource = new CancellationTokenSource();
        _linkedCancellationTokenSource =
            cancellationToken == null
                ? null
                : CancellationTokenSource.CreateLinkedTokenSource(_elapsedCancellationTokenSource.Token, cancellationToken.Value);
        _activeCancellationToken = (_linkedCancellationTokenSource ?? _elapsedCancellationTokenSource).Token;

        _eventInfo = typeof(TOwner).GetEvent(_eventName);
        Type eventHandlerType = _eventInfo.EventHandlerType;
        MethodInfo invokeMethodInfo = eventHandlerType.GetMethod("Invoke");
        var parameterTypes = Enumerable.Repeat(this.GetType(),1).Concat(invokeMethodInfo.GetParameters().Select(p => p.ParameterType)).ToArray();
        DynamicMethod eventRedirectorMethod = new DynamicMethod("EventRedirect", typeof(void), parameterTypes);
        ILGenerator generator = eventRedirectorMethod.GetILGenerator();
        generator.Emit(OpCodes.Nop);
        generator.Emit(OpCodes.Ldarg_0);
        generator.EmitCall(OpCodes.Call, this.GetType().GetMethod(nameof(OnEventRaised),BindingFlags.Public | BindingFlags.Instance), null);
        generator.Emit(OpCodes.Ret);
        _localHookDelegate = eventRedirectorMethod.CreateDelegate(eventHandlerType,this);
    }
    private void AddHandler()
    {
        _eventInfo.AddEventHandler(_owner, _localHookDelegate);
    }
    private void RemoveHandler()
    {
        _eventInfo.RemoveEventHandler(_owner, _localHookDelegate);
    }
    private Task<bool> RunAsync(TimeSpan timeout)
    {
        AddHandler();
        Task.Delay(timeout, _activeCancellationToken).
            ContinueWith(TimeOutTaskCompleted);

        return _taskCompletionSource.Task;
    }

    private void TimeOutTaskCompleted(Task tsk)
    {
        RemoveHandler();
        if (_elapsedCancellationTokenSource.IsCancellationRequested) return;

        if (_linkedCancellationTokenSource?.IsCancellationRequested == true)
            SetResult(TaskResult.Cancelled);
        else if (!_taskCompletionSource.Task.IsCompleted)
            SetResult(TaskResult.Failed);

    }

    public void OnEventRaised()
    {
        RemoveHandler();
        if (_taskCompletionSource.Task.IsCompleted)
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
        }
        else
        {
            if (!_elapsedCancellationTokenSource.IsCancellationRequested)
                _elapsedCancellationTokenSource?.Cancel(false);
            SetResult(TaskResult.Success);
        }
    }
    enum TaskResult { Failed, Success, Cancelled }
    private void SetResult(TaskResult result)
    {
        if (result == TaskResult.Success)
            _taskCompletionSource.SetResult(true);
        else if (result == TaskResult.Failed)
            _taskCompletionSource.SetResult(false);
        else if (result == TaskResult.Cancelled)
            _taskCompletionSource.SetCanceled();
        Dispose();

    }
    public void Dispose()
    {
        RemoveHandler();
        _elapsedCancellationTokenSource?.Dispose();
        _linkedCancellationTokenSource?.Dispose();
    }
}

It basically relies on CancellationTokenSource to report back the result. It uses some IL injection to create a delegate to match the event's signature. That delegate is then added as a handler for that event using some reflection. The body of the generate method simply calls another function on the EventAwaiter class, which then reports success using the CancellationTokenSource.

Caution, do not use this, as is, in product. This is meant as a working example.

For instance, IL generation is an expensive process. You should avoid regenerate the same method over and over again, and instead cache these.

Cogent
  • 404
  • 7
  • 16
0

AsyncEx has AsyncManualResetEvent for this. You can:

var signal = new AsyncManualResetEvent();
await signal.WaitAsync();

And trigger it with:

signal.Set();
ChrisTorng
  • 742
  • 7
  • 18