如何'等待'引发EventHandler事件

时间:2012-09-16 23:35:55

标签: c# events mvvm .net-4.5 async-await

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

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

在重构我的.NET4.5应用程序时,我尽可能使用asyncawait。但是以下方法不起作用(我真的没想到)

 await SearchRequest(this, EventArgs.Empty);

框架肯定会调用事件处理程序such as this,但我不确定它是如何做到的?

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

我发现任何关于异议提出事件的问题is ancient但是我在框架中找不到支持这一点的东西。

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

11 个答案:

答案 0 :(得分:28)

修改:这对多个订阅者不起作用,所以除非你只有一个我不建议使用它。


感觉有点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();
 };

注意:我从未使用多个订阅者对此进行测试,也不确定这是如何工作的 - 所以如果您需要多个订阅者,请务必仔细测试。

答案 1 :(得分:20)

根据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,则应该能够使用空条件运算符并改为编写:

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

答案 2 :(得分:17)

正如您所发现的,事件与asyncawait不完全吻合。

UI处理async事件的方式与您尝试的方式不同。 UI provides a SynchronizationContext to its async events,使他们能够在UI线程上恢复。它永远“等待”他们。

最佳解决方案(IMO)

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

较小的解决方案#1

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

以下是一个示例,使用AsyncEx library中的AsyncContext

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

但是,在这个示例中,UI线程Run中传送消息。

较小的解决方案#2

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

答案 3 :(得分:5)

回答直接问题:我认为EventHandler不允许实现与调用者充分沟通以允许正确等待。您可以使用自定义同步上下文执行技巧,但如果您关心等待处理程序,则处理程序最好能够将Task返回给调用者。通过使代表签名的这一部分,代表将await更清楚。

我建议使用Delgate.GetInvocationList() approach described in Ariel’s answer混合来自tzachs’s answer的想法。定义您自己的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,因为延续将被编组到调度程序线程上。如果你突然发生这种情况。例如,你从winforms或WPF中的UI线程调用和await事件。否则,你可能必须在变异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上爆炸。如果你愿意,你可以使用下面的调用模式,但可以认为parens是丑陋的,并且if样式通常由于各种原因更好:

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

答案 4 :(得分:3)

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

查看代码:

var tcs = new TaskCompletionSource<object>();
service.loginCreateCompleted += (object sender, EventArgs e) =>
{
    tcs.TrySetResult(e.Result);
};
await tcs.Task;

答案 5 :(得分: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,因为您不能将awaitvoid方法一起使用 - 这是编译错误源于的地方。

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

我认为那里存在一个基本的设计问题。在点击事件上做一些背景工作是很好的,你可以实现支持await的东西。但是,对UI的使用方式有何影响?例如如果你有一个Click处理程序启动需要2秒的操作,你是否希望用户能够在操作挂起时单击该按钮?取消和超时是额外的复杂性。我认为需要在这里更多地了解可用性方面。

答案 6 :(得分:2)

由于委托(和事件是委托)实现异步编程模型(APM),您可以使用TaskFactory.FromAsync方法。 (另见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;
}

但是,上述代码将在线程池线程上调用该事件,即它不会捕获当前的同步上下文。如果这是一个问题,您可以按如下方式修改它:

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

答案 8 :(得分:1)

如果您使用的是自定义事件处理程序,则可能要看一下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 here

答案 9 :(得分:0)

要继续Simon Weaver的答案,我尝试了以下

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

这样做就可以了。

答案 10 :(得分: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
    }
}
相关问题