Is it possible to await an event instead of another async method?
Asked Answered
D

11

210

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.

Description answered 12/10, 2012 at 11:53 Comment(0)
D
307

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;
Diverticulum answered 12/10, 2012 at 12:1 Comment(19)
I would have used a ManualResetEvent. Is there an advantage to using SemaphoreSlim or could you use either one?Tyche
@DanielHilgarth ManualResetEvent(Slim) doesn't seem to support WaitAsync().Jonathonjonati
@svick: Good point. However, as GetResult already is async you could block inside this method without any problems, couldn't you?Tyche
@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.Jonathonjonati
@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?Tyche
@Jonathonjonati "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.Clardy
@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.Clardy
@Clardy 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.Jonathonjonati
@Jonathonjonati 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.Clardy
@Clardy Sure, but “runs asynchronously” doesn't mean “runs on another thread”. GetResults() is still on the UI context.Jonathonjonati
@Jonathonjonati 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.Clardy
@Clardy 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.Jonathonjonati
@Jonathonjonati 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.Clardy
For others who want to see more, see here: chat.stackoverflow.com/rooms/17937 - @Jonathonjonati and I basically misunderstood each other, but were saying the same thing.Clardy
+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 Tasks that are used to implement WaitAsync.Surpassing
TaskCompletionSource<T> + await .Task + .SetResult() turns out to be the perfect solution for my scenario - thanks! :-)Description
Using the TaskCompletionSource<T>, 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.Precocious
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!Insulation
Thank you, TaskCompletionSource is exactly what I need. I need to postpone execution of HostedServices until application ready.Alight
S
97

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).

Surpassing answered 12/10, 2012 at 14:59 Comment(2)
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.Udella
And just in case you are wondering about disposing, here is the answer, from the same author.Fates
M
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;
Melosa answered 2/8, 2016 at 9:31 Comment(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?Ephialtes
C
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.

Clardy answered 12/10, 2012 at 12:49 Comment(8)
What async thread? There is no code that will not run on the UI thread, both in the original question and in your answer.Jonathonjonati
@Jonathonjonati 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.Clardy
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.Jonathonjonati
@Jonathonjonati 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.Clardy
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().Jonathonjonati
@Jonathonjonati 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.Clardy
@Jonathonjonati 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?Clardy
For others who want to see more, see here: chat.stackoverflow.com/rooms/17937 - @Jonathonjonati and I basically misunderstood each other, but were saying the same thing.Clardy
R
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;
Repulse answered 8/2, 2017 at 15:26 Comment(4)
How would you cleanup the subscription to example.YourEvent?Byers
@DenisP perhaps pass the event into constructor for EventAwaiter?Ericson
@DenisP I improved the version and run a short test.Repulse
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.Byers
Q
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; 
        } 
    } 
}
Quoits answered 4/2, 2016 at 21:26 Comment(2)
The link to the blog gives 403 Forbidden for me, check that please.Cordle
@Cordle fixed, thanks.Quoits
A
3

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;
}
Augustus answered 26/7, 2019 at 3:49 Comment(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.Ephialtes
@Ephialtes Thanks! I've modified it since to avoid returning a delegate. The source is available here as part of Lara Web Engine, an alternative to Blazor.Augustus
R
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");
    }
Repulse answered 19/3, 2018 at 15:11 Comment(0)
M
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.

Maloy answered 16/12, 2020 at 14:18 Comment(0)
D
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.

Doughty answered 4/8, 2021 at 22:34 Comment(0)
P
0

AsyncEx has AsyncManualResetEvent for this. You can:

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

And trigger it with:

signal.Set();
Pulmotor answered 22/8, 2022 at 3:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.