Roslyn: Proposal: Awaitable Events

Created on 16 Sep 2016  路  2Comments  路  Source: dotnet/roslyn

Add ablilty for awaiting for events, without creating separate function as handler.
Example:

Public Async Function F1()
    Dim b As New Button
    Controls.Add(b)
    Await b.Click (Dim Sender, Dim Args)
    ... ' using Sender and Args
End Function

Parameters for Click are known types, so Sender and Args types can be inferred.
Such Await do not return anything, but could be nice to add it as condition for loops

    While Await b.Click (Dim Sender, Dim Args)
    ...
    End While
Area-Language Design

Most helpful comment

I think I'd like to see something like this enabled through #298, if possible. That might require some kind of delegate signature equivalence and/or the ability to resolve extension methods on events. I think an intermediate event wrapper would suffice for enabling these scenarios:

public struct EventWrapper<TEventArgs> where TEventArgs : EventArgs {
    private readonly Action<EventHandler<TEventArgs>> subscribe;
    private readonly Action<EventHandler<TEventArgs>> unsubscribe;

    private EventWrapper(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe) {
        this.subscribe = subscribe;
        this.unsubscribe = unsubscribe;
    }

    public IDisposable Subscribe(EventHandler<TEventArgs> handler) {
        this.subscribe(handler);
        return Disposable.Create(() => this.unsubscribe(handler)); // borrowing from Rx
    }

    public static EventWrapper<TEventArgs> Wrap(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe) {
        return new EventWrapper<TEventArgs>(subscribe, unsubscribe);
    }
}

then:

var (sender, args) = await button.Click;

// converted into something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
); // intermediary struct which wraps the event accessor methods
EventAwaiter<EventArgs> awaiter = wrapper.GetAwaiter(); // resolved via extension method
// awaiting here
// GetAwaiter implementation would unsubscribe immediately after event fires
awaiter.Result.Deconstruct(out var sender, out var args);
foreach (await var (sender, args) in button.Click) { ... }

// converted into something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
);
AsyncEventEnumerator<EventArgs> enumerator = null;
try {
    enumerator = wrapper.GetAsyncEnumerator(); // resolved via extension method
    while (await enumerator.MoveNextAsync()) {
        enumerator.Current.Deconstruct(out var sender, out var args);
        ...
    }
}
finally {
    if (enumerator != null) await enumerator.DisposeAsync();
}

The reason I'd like to see something more general purpose is that I think that there are possibilities to expose events as other types that aren't currently supported directly by the compiler. For example, Rx:

IObservable<Event<EventArgs>> observable = b.Click.ToObservable();

// something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
);
IObservable<Event<EventArgs>> observable = wrapper.ToObservable();

All 2 comments

I think I'd like to see something like this enabled through #298, if possible. That might require some kind of delegate signature equivalence and/or the ability to resolve extension methods on events. I think an intermediate event wrapper would suffice for enabling these scenarios:

public struct EventWrapper<TEventArgs> where TEventArgs : EventArgs {
    private readonly Action<EventHandler<TEventArgs>> subscribe;
    private readonly Action<EventHandler<TEventArgs>> unsubscribe;

    private EventWrapper(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe) {
        this.subscribe = subscribe;
        this.unsubscribe = unsubscribe;
    }

    public IDisposable Subscribe(EventHandler<TEventArgs> handler) {
        this.subscribe(handler);
        return Disposable.Create(() => this.unsubscribe(handler)); // borrowing from Rx
    }

    public static EventWrapper<TEventArgs> Wrap(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe) {
        return new EventWrapper<TEventArgs>(subscribe, unsubscribe);
    }
}

then:

var (sender, args) = await button.Click;

// converted into something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
); // intermediary struct which wraps the event accessor methods
EventAwaiter<EventArgs> awaiter = wrapper.GetAwaiter(); // resolved via extension method
// awaiting here
// GetAwaiter implementation would unsubscribe immediately after event fires
awaiter.Result.Deconstruct(out var sender, out var args);
foreach (await var (sender, args) in button.Click) { ... }

// converted into something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
);
AsyncEventEnumerator<EventArgs> enumerator = null;
try {
    enumerator = wrapper.GetAsyncEnumerator(); // resolved via extension method
    while (await enumerator.MoveNextAsync()) {
        enumerator.Current.Deconstruct(out var sender, out var args);
        ...
    }
}
finally {
    if (enumerator != null) await enumerator.DisposeAsync();
}

The reason I'd like to see something more general purpose is that I think that there are possibilities to expose events as other types that aren't currently supported directly by the compiler. For example, Rx:

IObservable<Event<EventArgs>> observable = b.Click.ToObservable();

// something akin to

EventWrapper<EventArgs> wrapper = EventWrapper<EventArgs>.Wrap(
    handler => button.Click += handler,
    handler => button.Click -= handler
);
IObservable<Event<EventArgs>> observable = wrapper.ToObservable();

Language design discussion now takes place at https://github.com/dotnet/csharplang . Please reopen there if you want to continue this. I am closing this as the discussion appears to be stale.

Was this page helpful?
0 / 5 - 0 ratings