66

有时,事件模式用于在 MVVM 应用程序中引发事件,或者子视图模型以像这样的松散耦合方式向其父视图模型发送消息。

父视图模型

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;
});

在为 .NET4.5 重构我的应用程序时,我正在制作尽可能多的代码来使用asyncawait. 但是以下不起作用(好吧,我真的没想到会这样)

 await SearchRequest(this, EventArgs.Empty);

框架确实这样做是为了调用诸如 this 之类的事件处理程序,但我不确定它是如何做到的?

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

我在异步引发事件的主题上发现的任何东西都是 古老的,但我在框架中找不到支持这一点的东西。

如何await调用事件但仍保留在 UI 线程上。

4

12 回答 12

39

编辑:这不适用于多个订阅者,因此除非您只有一个订阅者,否则我不建议您使用它。


感觉有点hacky - 但我从来没有找到更好的东西:

声明一个代表。这等同于EventHandler但返回一个任务而不是 void

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

然后,您可以运行以下命令,只要在父级中声明的处理程序使用正确asyncawait那么这将异步运行:

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

样品处理程序:

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

注意:我从未对多个订阅者进行过测试,并且不确定这将如何工作 - 所以如果您需要多个订阅者,请务必仔细测试。

于 2012-09-16T23:38:20.053 回答
30

根据 Simon_Weaver 的回答,我创建了一个可以处理多个订阅者的辅助类,并且具有与 c# 事件类似的语法。

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);
        }
    }
}

要使用它,请在类中声明它,例如:

public AsyncEvent<EventArgs> SearchRequest;

要订阅事件处理程序,您将使用熟悉的语法(与 Simon_Weaver 的答案相同):

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

要调用事件,请使用我们用于 c# 事件的相同模式(仅使用 InvokeAsync):

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

如果使用 c# 6,则应该能够使用 null 条件运算符并改为:

await (SearchRequest?.InvokeAsync(sender, eventArgs) ?? Task.CompletedTask);
于 2015-06-09T17:42:30.683 回答
28

正如您所发现的,事件与asyncand并不完美结合。await

UI 处理async事件的方式与您尝试执行的操作不同。UI为其事件提供了一个SynchronizationContextasync,使它们能够在 UI 线程上恢复。它永远不会“等待”他们。

最佳解决方案 (IMO)

我认为最好的选择是构建您自己的async友好发布/订阅系统,AsyncCountdownEvent用于了解所有处理程序何时完成。

较小的解决方案#1

async void方法会在开始和结束时通知它们SynchronizationContext(通过增加/减少异步操作的计数)。所有 UISynchronizationContext都会忽略这些通知,但您可以构建一个包装器来跟踪它并在计数为零时返回。

这是一个示例,使用AsyncContext来自我的AsyncEx 库

SearchCommand = new RelayCommand(() => {
  IsSearching = true;
  if (SearchRequest != null) 
  {
    AsyncContext.Run(() => SearchRequest(this, EventArgs.Empty));
  }
  IsSearching = false;
});

但是,在此示例中,UI线程Run.

较小的解决方案#2

SynchronizationContext您还可以根据嵌套框架制作自己的Dispatcher框架,当异步操作的计数达到零时,该框架会自行弹出。但是,您随后会引入重入问题;DoEvents故意被排除在WPF之外。

于 2012-09-17T04:38:37.117 回答
11

回答直接问题:我认为EventHandler不允许实现充分地与调用者进行通信以允许适当的等待。您可能能够使用自定义同步上下文执行技巧,但如果您关心等待处理程序,最好处理程序能够将它们Task的 s 返回给调用者。通过制作委托签名的这一部分,可以更清楚地了解委托将被await编辑。

我建议将Delgate.GetInvocationList()Ariel 的回答中描述的方法与tzachs 的回答中的想法混合使用。定义您自己的AsyncEventHandler<TEventArgs>委托,它返回一个Task. 然后使用扩展方法来隐藏正确调用它的复杂性。如果你想执行一堆异步事件处理程序并等待它们的结果,我认为这种模式是有意义的。

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)));
}

这允许您创建一个普通的 .net 样式event。只需像往常一样订阅它。

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();
    };
}

然后只需记住使用扩展方法来调用事件,而不是直接调用它们。如果您想在调用中获得更多控制权,您可以使用GetHandlers()扩展。对于等待所有处理程序完成的更常见情况,只需使用便捷包装器InvokeAllAsync()。在许多模式中,事件要么不产生调用者感兴趣的任何东西,要么通过修改传入的EventArgs. (注意,如果您可以假设具有调度程序样式序列化的同步上下文,则您的事件处理程序可能会EventArgs在其同步块内安全地改变它们,因为延续将被编组到调度程序线程上。这将神奇地发生在您身上,例如,您调用和await来自 winforms 或 WPF 中的 UI 线程的事件。否则,您可能必须在突变时使用锁定EventArgs,以防您的任何突变发生在线程池上运行的延续中)。

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

这使您更接近于看起来像普通事件调用的东西,除了您必须使用.InvokeAllAsync(). 而且,当然,您仍然会遇到事件带来的正常问题,例如需要保护对没有订阅者的事件的调用以避免NullArgumentException.

请注意,我没有使用await SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty),因为awaitnull. 如果需要,您可以使用以下调用模式,但可以说括号很难看,并且if由于各种原因,样式通常更好:

await (SomethingHappened?.InvokeAllAsync(this, EventArgs.Empty) ?? Task.CompletedTask);
于 2016-02-08T22:30:21.497 回答
4

如果您正在使用自定义事件处理程序,您可能需要查看DeferredEvents,因为它允许您引发和等待事件的处理程序,如下所示:

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

事件处理程序将执行以下操作:

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

    await DoSomethingAsync();

    deferral.Complete();
}

或者,您可以使用这样的using模式:

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

您可以在此处阅读有关 DeferredEvents 的信息。

于 2018-08-07T09:57:02.570 回答
4

我知道这是一个老问题,但我最好的解决方案是使用TaskCompletionSource

见代码:

var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>
{
    tcs.TrySetResult(e.Result);
};
await tcs.Task;
于 2021-02-10T12:57:15.313 回答
3

您可以使用Microsoft 提供的Microsoft.VisualStudio.ThreadingAsyncEventHandler包中的委托,据我了解,它在 Visual Studio 中使用。

private AsyncEventHandler _asyncEventHandler;
_asyncEventHandler += DoStuffAsync;

Debug.WriteLine("Async invoke incoming!");
await _asyncEventHandler.InvokeAsync(this, EventArgs.Empty);
Debug.WriteLine("Done.");
private async Task DoStuffAsync(object sender, EventArgs args)
{
    await Task.Delay(1000);
    Debug.WriteLine("hello from async event handler");
    await Task.Delay(1000);
}

输出:
异步调用传入!
来自异步事件处理程序的问候
完成。

于 2021-09-18T22:00:47.463 回答
2

我不清楚您所说的“我如何await调用事件但仍保留在 UI 线程上”是什么意思。您希望在 UI 线程上执行事件处理程序吗?如果是这种情况,那么您可以执行以下操作:

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

它将处理程序的调用包装在一个Task对象中,以便您可以使用await,因为您不能使用await方法void- 这是您的编译错误的根源。

但是,我不确定您期望从中获得什么好处。

我认为那里有一个基本的设计问题。可以在 click 事件上执行一些后台工作,并且您可以实现一些支持await. 但是,对如何使用 UI 有什么影响?例如,如果您有一个Click启动一个需要 2 秒的操作的处理程序,您是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为这里需要对可用性方面做更多的理解。

于 2012-09-16T23:57:49.387 回答
2

由于委托(并且事件是委托)实现了异步编程模型 (APM),因此您可以使用TaskFactory.FromAsync方法。(另请参阅任务和异步编程模型 (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;
}

然而,上面的代码将调用线程池线程上的事件,即它不会捕获当前的同步上下文。如果这是一个问题,您可以将其修改如下:

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;
}
于 2016-04-14T15:51:35.503 回答
1
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());
}
于 2017-12-10T22:10:57.557 回答
0

为了继续Simon Weaver的回答,我尝试了以下方法

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

这似乎可以解决问题。

于 2015-12-28T12:57:40.917 回答
0

这有点来自@Simon_Weaver 的回答,但我觉得它很有用。假设您有一些RaisesEvents具有事件的类,RaisesEvents.MyEvent并且您已将其注入到MyClass要订阅的类中,在方法中进行订阅MyEvent可能更好Initialize(),但为简单起见:

public class MyClass
{
    public MyClass(RaisesEvent otherClass)
    {
        otherClass.MyEvent += MyAction;
    }

    private Action MyAction => async () => await ThingThatReturnsATask();

    public void Dispose() //it doesn't have to be IDisposable, but you should unsub at some point
    {
        otherClass.MyEvent -= MyAction;
    }

    private async Task ThingThatReturnsATask()
    {
        //async-await stuff in here
    }
}
于 2019-06-07T23:05:41.927 回答