66

イベントパターンは、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用にアプリケーションをリファクタリングする際に、可能な限り多くのコードを使用できるようにしていasyncますawait。ただし、以下は機能しません(まあ、私は本当にそれを期待していませんでした)

 await SearchRequest(this, EventArgs.Empty);

フレームワークは間違いなくこのようなイベントハンドラーを呼び出すためにこれを行いますが、それがどのように行われるのかわかりませんか?

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

編集:これは複数のサブスクライバーにはうまく機能しないため、サブスクライバーが1つしかない場合を除いて、これを使用することはお勧めしません。


少しハッキーな感じがしますが、これ以上良いものは見つかりませんでした。

デリゲートを宣言します。これは同じですEventHandlerが、voidの代わりにタスクを返します

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

async次に、次を実行できます。親で宣言されたハンドラーが使用している限り、awaitこれは非同期で実行されます。

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

あなたが発見したように、イベントはとasyncと完全には一致しません。await

UIがasyncイベントを処理する方法は、実行しようとしている方法とは異なります。UIはイベントにを提供SynchronizationContextasync、UIスレッドで再開できるようにします。それは決して彼らを「待つ」ことはありません。

ベストソリューション(IMO)

最善のオプションは、すべてのハンドラーがいつ完了したかを知るためにasync使用して、独自のフレンドリーなpub/subシステムを構築することだと思います。AsyncCountdownEvent

少ないソリューション#1

async voidSynchronizationContextメソッドは、開始時と終了時に通知します(非同期操作のカウントをインクリメント/デクリメントします)。すべての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、継続がディスパッチャースレッドにマーシャリングされるため、イベントハンドラーは同期ブロック内で安全に変異する可能性があります。これは、たとえば、呼び出してawaitwinformsまたは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)awaitnullif

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パッケージのデリゲートを使用でき、私が理解していることからVisualStudioで使用されています。

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は使用できないためです。これは、コンパイルエラーの原因です。awaitvoid

しかし、私はあなたがそれからどのような利益を期待するのかわかりません。

そこには根本的なデザインの問題があると思います。クリックイベントのバックグラウンド作業を開始するのは問題ありません。をサポートするものを実装できますawait。しかし、UIの使用方法にはどのような影響がありますか?たとえば、Click2秒かかる操作を開始するハンドラーがある場合、操作が保留されている間、ユーザーがそのボタンをクリックできるようにしますか?キャンセルとタイムアウトはさらに複雑です。ここでは、ユーザビリティの側面についてさらに理解を深める必要があると思います。

于 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

サイモンウィーバーの答えを続けるために、私は次のことを試みました

        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 に答える