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?.
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.
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);
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 Task
s an den Aufrufer zurückgeben können. Indem Sie diesen Teil der Unterschrift des Delegierten vornehmen, ist es klarer, dass der Delegierte await
ed 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);
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;
}
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.
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.
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());
}
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.