web-dev-qa-db-de.com

Wie Sie warten, wenn Sie ein EventHandler-Ereignis auslösen

Manchmal wird das Ereignismuster verwendet, um Ereignisse in MVVM-Anwendungen durch ein untergeordnetes Ansichtsmodell auszulösen, um eine Nachricht auf lose verknüpfte Weise an das übergeordnete Ansichtsmodell zu senden. 

Parent ViewModel

searchWidgetViewModel.SearchRequest += (s,e) => 
{
    SearchOrders(searchWidgitViewModel.SearchCriteria);
};

SearchWidget ViewModel

public event EventHandler SearchRequest;

SearchCommand = new RelayCommand(() => {

    IsSearching = true;
    if (SearchRequest != null) 
    {
        SearchRequest(this, EventArgs.Empty);
    }
    IsSearching = false;
});

Beim Refactoring meiner Anwendung für .NET4.5 mache ich so viel Code möglich, wie async und await verwendet werden können. Folgendes funktioniert jedoch nicht (na ja, eigentlich hatte ich nicht damit gerechnet)

 await SearchRequest(this, EventArgs.Empty);

Das Framework tut dies definitiv, um Event-Handler wie dieses aufzurufen, aber ich bin nicht sicher, wie es geht?

private async void button1_Click(object sender, RoutedEventArgs e)
{
   textBlock1.Text = "Click Started";
   await DoWork();
   textBlock2.Text = "Click Finished";
}

Alles, was ich zum Thema gefunden habe, um Ereignisse asynchros zu heben, isturalt , aber ich kann nichts im Rahmen finden, um dies zu unterstützen.

Wie kann ich das Aufrufen eines Ereignisses await aber im UI-Thread bleiben?.

37
Simon_Weaver

Edit: Dies funktioniert nicht für mehrere Abonnenten. Wenn Sie also nur einen Abonnenten haben, würde ich dies nicht empfehlen.


Fühlt sich etwas hackig an - aber ich habe nie etwas Besseres gefunden:

Einen Delegierten deklarieren Dies ist identisch mit EventHandler , gibt jedoch eine Aufgabe anstelle von void zurück

public delegate Task AsyncEventHandler(object sender, EventArgs e);

Sie können dann Folgendes ausführen, und solange der im übergeordneten Element deklarierte Handler async und await ordnungsgemäß verwendet, wird dies asynchron ausgeführt:

if (SearchRequest != null) 
{
    Debug.WriteLine("Starting...");
    await SearchRequest(this, EventArgs.Empty);
    Debug.WriteLine("Completed");
}

Beispiel-Handler:

 // declare handler for search request
 myViewModel.SearchRequest += async (s, e) =>
 {                    
     await SearchOrders();
 };

Hinweis: Ich habe dies noch nie mit mehreren Abonnenten getestet und bin mir nicht sicher, wie dies funktionieren wird. Wenn Sie also mehrere Abonnenten benötigen, sollten Sie es sorgfältig testen.

27
Simon_Weaver

Basierend auf der Antwort von Simon_Weaver habe ich eine Hilfsklasse erstellt, die mit mehreren Abonnenten umgehen kann und eine ähnliche Syntax wie c # -Ereignisse hat.

public class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
{
    private readonly List<Func<object, TEventArgs, Task>> invocationList;
    private readonly object locker;

    private AsyncEvent()
    {
        invocationList = new List<Func<object, TEventArgs, Task>>();
        locker = new object();
    }

    public static AsyncEvent<TEventArgs> operator +(
        AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");

        //Note: Thread safety issue- if two threads register to the same event (on the first time, i.e when it is null)
        //they could get a different instance, so whoever was first will be overridden.
        //A solution for that would be to switch to a public constructor and use it, but then we'll 'lose' the similar syntax to c# events             
        if (e == null) e = new AsyncEvent<TEventArgs>();

        lock (e.locker)
        {
            e.invocationList.Add(callback);
        }
        return e;
    }

    public static AsyncEvent<TEventArgs> operator -(
        AsyncEvent<TEventArgs> e, Func<object, TEventArgs, Task> callback)
    {
        if (callback == null) throw new NullReferenceException("callback is null");
        if (e == null) return null;

        lock (e.locker)
        {
            e.invocationList.Remove(callback);
        }
        return e;
    }

    public async Task InvokeAsync(object sender, TEventArgs eventArgs)
    {
        List<Func<object, TEventArgs, Task>> tmpInvocationList;
        lock (locker)
        {
            tmpInvocationList = new List<Func<object, TEventArgs, Task>>(invocationList);
        }

        foreach (var callback in tmpInvocationList)
        {
            //Assuming we want a serial invocation, for a parallel invocation we can use Task.WhenAll instead
            await callback(sender, eventArgs);
        }
    }
}

Um es zu verwenden, deklarieren Sie es in Ihrer Klasse, zum Beispiel:

public AsyncEvent<EventArgs> SearchRequest;

Um einen Event-Handler zu abonnieren, verwenden Sie die bekannte Syntax (wie in der Antwort von Simon_Weaver):

myViewModel.SearchRequest += async (s, e) =>
{                    
   await SearchOrders();
};

Um das Ereignis aufzurufen, verwenden Sie dasselbe Muster, das wir für c # -Ereignisse verwenden (nur bei InvokeAsync):

var eventTmp = SearchRequest;
if (eventTmp != null)
{
   await eventTmp.InvokeAsync(sender, eventArgs);
}

Wenn Sie c # 6 verwenden, sollte es möglich sein, den bedingten Null-Operator zu verwenden und stattdessen Folgendes zu schreiben:

await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
19
tzachs

Um die direkte Frage zu beantworten: Ich glaube nicht, dass EventHandler es Implementierungen ermöglicht, ausreichend mit dem Aufrufer zu kommunizieren, um das richtige Warten zu ermöglichen. Sie können möglicherweise Tricks mit einem benutzerdefinierten Synchronisationskontext ausführen. Wenn Sie jedoch auf die Handler warten möchten, ist es besser, dass die Handler ihre Tasks an den Aufrufer zurückgeben können. Indem Sie diesen Teil der Unterschrift des Delegierten vornehmen, ist es klarer, dass der Delegierte awaited ist.

Ich schlage vor, den in Ariels Antwort beschriebenen Delgate.GetInvocationList()-Ansatz zu verwenden gemischt mit Ideen aus tzachs 's Antwort . Definieren Sie Ihren eigenen AsyncEventHandler<TEventArgs>-Delegaten, der eine Task zurückgibt. Verwenden Sie dann eine Erweiterungsmethode, um die Komplexität des korrekten Aufrufs zu verbergen. Ich denke, dieses Muster ist sinnvoll, wenn Sie eine Reihe von asynchronen Ereignishandlern ausführen und auf ihre Ergebnisse warten möchten.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public delegate Task AsyncEventHandler<TEventArgs>(
    object sender,
    TEventArgs e)
    where TEventArgs : EventArgs;

public static class AsyncEventHandlerExtensions
{
    public static IEnumerable<AsyncEventHandler<TEventArgs>> GetHandlers<TEventArgs>(
        this AsyncEventHandler<TEventArgs> handler)
        where TEventArgs : EventArgs
        => handler.GetInvocationList().Cast<AsyncEventHandler<TEventArgs>>();

    public static Task InvokeAllAsync<TEventArgs>(
        this AsyncEventHandler<TEventArgs> handler,
        object sender,
        TEventArgs e)
        where TEventArgs : EventArgs
        => Task.WhenAll(
            handler.GetHandlers()
            .Select(handleAsync => handleAsync(sender, e)));
}

Auf diese Weise können Sie eine normale .net-Variable event erstellen. Abonniere es einfach wie gewohnt.

public event AsyncEventHandler<EventArgs> SomethingHappened;

public void SubscribeToMyOwnEventsForNoReason()
{
    SomethingHappened += async (sender, e) =>
    {
        SomethingSynchronous();
        // Safe to touch e here.
        await SomethingAsynchronousAsync();
        // No longer safe to touch e here (please understand
        // SynchronizationContext well before trying fancy things).
        SomeContinuation();
    };
}

Dann denken Sie einfach daran, die Erweiterungsmethoden zu verwenden, um das Ereignis aufzurufen, anstatt sie direkt aufzurufen. Wenn Sie mehr Kontrolle über Ihren Aufruf wünschen, können Sie die Erweiterung GetHandlers() verwenden. Für den allgemeineren Fall des Wartens, bis alle Handler abgeschlossen sind, verwenden Sie einfach den Convenience-Wrapper InvokeAllAsync(). In vielen Mustern erzeugen Ereignisse entweder nichts, woran der Anrufer interessiert ist, oder sie kommunizieren mit dem Anrufer, indem sie die in EventArgs übergebene Änderung ändern. (Wenn Sie einen Synchronisationskontext mit einer Dispatcher-ähnlichen Serialisierung voraussetzen können, können Ihre Event-Handler die EventArgs innerhalb ihrer synchronen Blöcke sicher mutieren, da die Fortsetzungen in den Dispatcher-Thread gemarshallt werden. Dies wird z. B. Sie rufen das Ereignis von einem UI-Thread in winforms oder WPF auf und await. Andernfalls müssen Sie möglicherweise das Sperren verwenden, wenn Sie EventArgs mutieren, falls eine Ihrer Mutationen in einer Fortsetzung auftritt, die im Threadpool ausgeführt wird.

public async Task Run(string[] args)
{
    if (SomethingHappened != null)
        await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}

Dies bringt Sie näher an etwas, das wie ein normaler Ereignisaufruf aussieht, außer dass Sie .InvokeAllAsync() verwenden müssen. Und natürlich haben Sie immer noch die normalen Probleme, die mit Ereignissen einhergehen, z. B., dass Aufrufe von Ereignissen ohne Abonnenten überwacht werden müssen, um eine NullArgumentException zu vermeiden.

Beachten Sie, dass ich nicht mit await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) arbeite, da await bei null explodiert. Sie können das folgende Aufrufmuster verwenden, wenn Sie möchten, es kann jedoch argumentiert werden, dass die Parens hässlich sind und der if-Stil aus verschiedenen Gründen im Allgemeinen besser ist:

await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
5
binki

Da Delegaten (und Ereignisse sind Delegaten) das Asynchronous Programming Model (APM) implementieren, können Sie die Methode TaskFactory.FromAsync verwenden. (Siehe auch Tasks und das asynchrone Programmiermodell (APM) .)

public event EventHandler SearchRequest;

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        await Task.Factory.FromAsync(SearchRequest.BeginInvoke, SearchRequest.EndInvoke, this, EventArgs.Empty, null);
    }
    IsSearching = false;
}

Der obige Code ruft jedoch das Ereignis in einem Thread-Pool-Thread auf, d. H. Der aktuelle Synchronisationskontext wird nicht erfasst. Wenn dies ein Problem ist, können Sie es wie folgt ändern:

public event EventHandler SearchRequest;

private delegate void OnSearchRequestDelegate(SynchronizationContext context);

private void OnSearchRequest(SynchronizationContext context)
{
    context.Send(state => SearchRequest(this, EventArgs.Empty), null);
}

public async Task SearchCommandAsync()
{
    IsSearching = true;
    if (SearchRequest != null)
    {
        var search = new OnSearchRequestDelegate(OnSearchRequest);
        await Task.Factory.FromAsync(search.BeginInvoke, search.EndInvoke, SynchronizationContext.Current, null);
    }
    IsSearching = false;
}
2
Scott

Mir ist nicht klar, was Sie unter "Wie kann ich den Aufruf eines Ereignisses await bedeuten, aber im UI-Thread bleiben" bedeuten. Soll der Event-Handler im UI-Thread ausgeführt werden? Wenn dies der Fall ist, können Sie Folgendes tun:

var h = SomeEvent;
if (h != null)
{
    await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
        Task.Factory.CancellationToken,
        Task.Factory.CreationOptions,
        TaskScheduler.FromCurrentSynchronizationContext());
}

Dies umschließt den Aufruf des Handlers in einem Task-Objekt, sodass Sie await verwenden können, da Sie await nicht mit einer void-Methode verwenden können, von der Ihr Kompilierungsfehler stammt.

Aber ich bin mir nicht sicher, welchen Nutzen Sie davon erwarten.

Ich denke, da gibt es ein grundlegendes Designproblem. Es ist in Ordnung, einige Hintergrundarbeiten zu einem Klickereignis zu starten und Sie können etwas implementieren, das await unterstützt. Aber wie wirkt sich das auf die Benutzeroberfläche aus? z.B. Wenn Sie einen Click-Handler haben, der eine Operation startet, die 2 Sekunden dauert, soll der Benutzer in der Lage sein, auf diese Schaltfläche zu klicken, während die Operation aussteht. Stornierung und Timeout sind zusätzliche Komplexitäten. Ich denke, hier muss viel mehr Verständnis für die Aspekte der Benutzerfreundlichkeit geschaffen werden.

2
Peter Ritchie

Um mit der Antwort von Simon Weaver fortzufahren, habe ich folgendes versucht

        if (SearchRequest != null)
        {
            foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
            {
                await onSearchRequest(null, EventArgs.Empty);
            }
        }

Dies scheint den Trick zu tun.

0
Ariel Steiner
public static class FileProcessEventHandlerExtensions
{
    public static Task InvokeAsync(this FileProcessEventHandler handler, object sender, FileProcessStatusEventArgs args)
     => Task.WhenAll(handler.GetInvocationList()
                            .Cast<FileProcessEventHandler>()
                            .Select(h => h(sender, args))
                            .ToArray());
}
0
Andrii

Wenn Sie angepasste Ereignishandler verwenden, möchten Sie vielleicht die DeferredEvents betrachten, da Sie damit die Handler eines Ereignisses erhöhen und erwarten können, beispielsweise

await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);

Der Event-Handler wird so etwas tun:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    var deferral = e.GetDeferral();

    await DoSomethingAsync();

    deferral.Complete();
}

Alternativ können Sie das using-Muster wie folgt verwenden:

public async void OnMyEvent(object sender, DeferredEventArgs e)
{
    using (e.GetDeferral())
    {
        await DoSomethingAsync();
    }
}

Über die DeferredEvents können Sie hier nachlesen.

0
Pedro Lamas