可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Sometimes the event pattern is used to raise events in MVVM applications by or a child viewmodel to send a message to its parent viewmodel in a loosely coupled way like this.
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;
});
In refactoring my application for .NET4.5 I am making as much as code possible to use async
and await
. However the following doesn't work (well I really wasn't expecting it to)
await SearchRequest(this, EventArgs.Empty);
The framework definitely does this to call event handlers such as this, but I'm not sure how it does it?
private async void button1_Click(object sender, RoutedEventArgs e)
{
textBlock1.Text = "Click Started";
await DoWork();
textBlock2.Text = "Click Finished";
}
Anything I've found on the subject of raising events asynchrously is ancient but I can't find something in the framework to support this.
How can I await
the calling of an event but remain on the UI thread.
回答1:
Events don't mesh perfectly with async
and await
, as you've discovered.
The way UIs handle async
events is different than what you're trying to do. The UI provides a SynchronizationContext
to its async
events, enabling them to resume on the UI thread. It does not ever "await" them.
Best Solution (IMO)
I think the best option is to build your own async
-friendly pub/sub system, using AsyncCountdownEvent
to know when all handlers have completed.
Lesser Solution #1
async void
methods do notify their SynchronizationContext
when they start and finish (by incrementing/decrementing the count of asynchronous operations). All UI SynchronizationContext
s ignore these notifications, but you could build a wrapper that tracks it and returns when the count is zero.
Here's an example, using AsyncContext
from my AsyncEx library:
SearchCommand = new RelayCommand(() => {
IsSearching = true;
if (SearchRequest != null)
{
AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
}
IsSearching = false;
});
However, in this example the UI thread is not pumping messages while it's in Run
.
Lesser Solution #2
You could also make your own SynchronizationContext
based on a nested Dispatcher
frame that pops itself when the count of asynchronous operations reaches zero. However, you then introduce re-entrancy problems; DoEvents
was left out of WPF on purpose.
回答2:
Edit: This doesn't work well for multiple subscribers, so unless you only have one I wouldn't recommend using this.
Feels slightly hacky - but I have never found anything better:
Declare a delegate. This is identical to EventHandler
but returns a task instead of void
public delegate Task AsyncEventHandler(object sender, EventArgs e);
You can then run the following and as long as the handler declared in the parent uses async
and await
properly then this will run asynchronously:
if (SearchRequest != null)
{
Debug.WriteLine("Starting...");
await SearchRequest(this, EventArgs.Empty);
Debug.WriteLine("Completed");
}
Sample handler :
// declare handler for search request
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
Note: I've never tested this with multiple subscribers and not sure how this will work - so if you need multiple subscribers then make sure to test it carefully.
回答3:
Based on Simon_Weaver's answer, I created a helper class that can handle multiple subscribers, and has a similar syntax to c# events.
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);
}
}
}
To use it, you declare it in your class, for example:
public AsyncEvent<EventArgs> SearchRequest;
To subscribe an event handler, you'll use the familiar syntax (the same as in Simon_Weaver's answer):
myViewModel.SearchRequest += async (s, e) =>
{
await SearchOrders();
};
To invoke the event, use the same pattern we use for c# events (only with InvokeAsync):
var eventTmp = SearchRequest;
if (eventTmp != null)
{
await eventTmp.InvokeAsync(sender, eventArgs);
}
If using c# 6, one should be able to use the null conditional operator and write this instead:
await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
回答4:
To answer the direct question: I do not think EventHandler
allows implementations to communicate sufficiently back to the invoker to allow proper awaiting. You might be able to perform tricks with a custom synchronization context, but if you care about waiting for the handlers, it is better that the handlers are able to return their Task
s back to the invoker. By making this part of the delegate’s signature, it is clearer that the delegate will be await
ed.
I suggest using the Delgate.GetInvocationList()
approach described in Ariel’s answer mixed with ideas from tzachs’s answer. Define your own AsyncEventHandler<TEventArgs>
delegate which returns a Task
. Then use an extension method to hide the complexity of invoking it correctly. I think this pattern makes sense if you want to execute a bunch of asynchronous event handlers and wait for their results.
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)));
}
This allows you to create a normal .net-style event
. Just subscribe to it as you normally would.
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();
};
}
Then simply remember to use the extension methods to invoke the event rather than invoking them directly. If you want more control in your invocation, you may use the GetHandlers()
extension. For the more common case of waiting for all the handlers to complete, just use the convenience wrapper InvokeAllAsync()
. In many patterns, events either don’t produce anything the caller is interested in or they communicate back to the caller by modifying the passed in EventArgs
. (Note, if you can assume a synchronization context with dispatcher-style serialization, your event handlers may mutate the EventArgs
safely within their synchronous blocks because the continuations will be marshaled onto the dispatcher thread. This will magically happen for you if, for example, you invoke and await
the event from a UI thread in winforms or WPF. Otherwise, you may have to use locking when mutating EventArgs
in case if any of your mutations happen in a continuation which gets run on the threadpool).
public async Task Run(string[] args)
{
if (SomethingHappened != null)
await SomethingHappened.InvokeAllAsync(this, EventArgs.Empty);
}
This gets you closer to something that looks like a normal event invocation, except that you have to use .InvokeAllAsync()
. And, of course, you still have the normal issues that come with events such as needing to guard invocations for events with no subscribers to avoid a NullArgumentException
.
Note that I am not using await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty)
because await
explodes on null
. You could use the following call pattern if you want, but it can be argued that the parens are ugly and the if
style is generally better for various reasons:
await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
回答5:
I'm not clear on what you mean by "How can I await
the calling of an event but remain on the UI thread". Do you want the event handler to be executed on the UI thread? If that's the case then you can do something like this:
var h = SomeEvent;
if (h != null)
{
await Task.Factory.StartNew(() => h(this, EventArgs.Empty),
Task.Factory.CancellationToken,
Task.Factory.CreationOptions,
TaskScheduler.FromCurrentSynchronizationContext());
}
Which wraps the invocation of the handler in a Task
object so that you can use await
, since you can't use await
with a void
method--which is where your compile error stems from.
But, I'm not sure what benefit you expect to get out of that.
I think there's a fundamental design issue there. It's fine to kick of some background work on a click event and you can implement something that supports await
. But, what's the effect on how the UI can be used? e.g. if you have a Click
handler that kicks off an operation that takes 2 seconds, do you want the user to be able to click that button while the operation is pending? Cancellation and timeout are additional complexities. I think much more understanding of the usability aspects needs to be done here.
回答6:
Since delegates (and events are delegates) implement the Asynchronous Programming Model (APM), you could use the TaskFactory.FromAsync method. (See also Tasks and the Asynchronous Programming Model (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;
}
The above code, however, will invoke the event on a thread pool thread, i.e. it will not capture the current synchronization context. If this is a problem, you could modify it as follows:
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;
}
回答7:
To continue on Simon Weaver's answer, I tried the following
if (SearchRequest != null)
{
foreach (AsyncEventHandler onSearchRequest in SearchRequest.GetInvocationList())
{
await onSearchRequest(null, EventArgs.Empty);
}
}
This seams to do the trick.
回答8:
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());
}
回答9:
If you are using custom event handlers, you might want to take a look at the DeferredEvents, as it will allow you to raise and await for the handlers of an event, like this:
await MyEvent.InvokeAsync(sender, DeferredEventArgs.Empty);
The event handler will do something like this:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
var deferral = e.GetDeferral();
await DoSomethingAsync();
deferral.Complete();
}
Alternatively, you can use the using
pattern like this:
public async void OnMyEvent(object sender, DeferredEventArgs e)
{
using (e.GetDeferral())
{
await DoSomethingAsync();
}
}
You can read about the DeferredEvents here.