私のC#/ XAMLメトロアプリには、実行時間の長いプロセスを開始するボタンがあります。したがって、推奨されるように、async/awaitを使用してUIスレッドがブロックされないようにします。
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
await GetResults();
}
private async Task GetResults()
{
// Do lot of complex stuff that takes a long time
// (e.g. contact some web services)
...
}
場合によっては、GetResults内で発生する処理を続行するには、追加のユーザー入力が必要になります。簡単にするために、ユーザーが「続行」ボタンをクリックするだけでよいとしましょう。
私の質問は:GetResultsの実行を一時停止して、別のボタンのクリックなどのeventを待つ方法です。 ?
私が探しているものを達成するためのい方法は次のとおりです。続行ボタンのイベントハンドラはフラグを設定します...
private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
_continue = true;
}
...およびGetResultsは定期的にポーリングします:
buttonContinue.Visibility = Visibility.Visible;
while (!_continue) await Task.Delay(100); // poll _continue every 100ms
buttonContinue.Visibility = Visibility.Collapsed;
ポーリングは明らかにひどい(忙しい待機/サイクルの無駄)であり、イベントベースの何かを探しています。
何か案は?
この単純化された例では、1つのソリューションはもちろん、GetResults()を2つの部分に分割し、最初の部分を開始ボタンから、2番目の部分を継続ボタンから呼び出すことです。実際には、GetResultsで発生する処理はより複雑であり、実行中のさまざまなポイントでさまざまな種類のユーザー入力が必要になる場合があります。したがって、ロジックを複数のメソッドに分割するのは簡単ではありません。
SemaphoreSlim Class のインスタンスを信号として使用できます。
private SemaphoreSlim signal = new SemaphoreSlim(0, 1);
// set signal in event
signal.Release();
// wait for signal somewhere else
await signal.WaitAsync();
または、 TaskCompletionSource <T> Class のインスタンスを使用して、ボタンクリックの結果を表す Task <T> を作成できます。
private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
// complete task in event
tcs.SetResult(true);
// wait for task somewhere else
await tcs.Task;
await
をオンにする必要がある異常なものがある場合、最も簡単な答えはTaskCompletionSource
(またはasync
に基づいたいくつかのTaskCompletionSource
対応プリミティブ)です。
この場合、ニーズは非常に単純なので、TaskCompletionSource
を直接使用できます。
private TaskCompletionSource<object> continueClicked;
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
// Note: You probably want to disable this button while "in progress" so the
// user can't click it twice.
await GetResults();
// And re-enable the button here, possibly in a finally block.
}
private async Task GetResults()
{
// Do lot of complex stuff that takes a long time
// (e.g. contact some web services)
// Wait for the user to click Continue.
continueClicked = new TaskCompletionSource<object>();
buttonContinue.Visibility = Visibility.Visible;
await continueClicked.Task;
buttonContinue.Visibility = Visibility.Collapsed;
// More work...
}
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
if (continueClicked != null)
continueClicked.TrySetResult(null);
}
論理的には、TaskCompletionSource
はasync
ManualResetEvent
に似ていますが、イベントを1回だけ「設定」でき、イベントに「結果」を含めることができる点が異なります(この場合、それを使用して、結果をnull
に設定するだけです。
私が使用するユーティリティクラスは次のとおりです。
public class AsyncEventListener
{
private readonly Func<bool> _predicate;
public AsyncEventListener() : this(() => true)
{
}
public AsyncEventListener(Func<bool> predicate)
{
_predicate = predicate;
Successfully = new Task(() => { });
}
public void Listen(object sender, EventArgs eventArgs)
{
if (!Successfully.IsCompleted && _predicate.Invoke())
{
Successfully.RunSynchronously();
}
}
public Task Successfully { get; }
}
そして、ここに私がそれを使用する方法があります:
var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;
// ... make it change ...
await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
理想的には、あなたはしてはいけない。非同期スレッドを確実にブロックできますが、これはリソースの浪費であり、理想的ではありません。
ボタンがクリックされるのを待っている間にユーザーが昼食に行く標準的な例を考えてみましょう。
ユーザーからの入力を待っている間に非同期コードを停止した場合、そのスレッドが一時停止している間、リソースを浪費しているだけです。
つまり、非同期操作では、ボタンを有効にしてクリックを「待機」するポイントに維持する必要がある状態を設定する方が良いと言えます。その時点で、GetResults
メソッドstops。
次に、保存した状態に基づいてボタンがクリックされると、別の非同期タスクを開始して作業を継続します 。
SynchronizationContext
は、GetResults
を呼び出すイベントハンドラーでキャプチャされるため(コンパイラは、使用されているawait
キーワードを使用した結果、および SynchronizationContext.Current = UIアプリケーションを使用している場合、null以外である必要があります)、 async
/await
を使用できます:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
await GetResults();
// Show dialog/UI element. This code has been marshaled
// back to the UI thread because the SynchronizationContext
// was captured behind the scenes when
// await was called on the previous line.
...
// Check continue, if true, then continue with another async task.
if (_continue) await ContinueToGetResultsAsync();
}
private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
_continue = true;
}
private async Task GetResults()
{
// Do lot of complex stuff that takes a long time
// (e.g. contact some web services)
...
}
ContinueToGetResultsAsync
は、ボタンが押された場合に結果を取得し続けるメソッドです。ボタンがnotプッシュされていない場合、イベントハンドラーは何もしません。
シンプルヘルパークラス:
public class EventAwaiter<TEventArgs>
{
private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();
private readonly Action<EventHandler<TEventArgs>> _unsubscribe;
public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
{
subscribe(Subscription);
_unsubscribe = unsubscribe;
}
public Task<TEventArgs> Task => _eventArrived.Task;
private EventHandler<TEventArgs> Subscription => (s, e) =>
{
_eventArrived.TrySetResult(e);
_unsubscribe(Subscription);
};
}
使用法:
var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Stephen ToubはこのAsyncManualResetEvent
クラスを公開しました 彼のブログで 。
public class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
public Task WaitAsync() { return m_tcs.Task; }
public void Set()
{
var tcs = m_tcs;
Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
tcs.Task.Wait();
}
public void Reset()
{
while (true)
{
var tcs = m_tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
return;
}
}
}
With Reactive Extensions(Rx.Net) (
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
var res = await eventObservable.FirstAsync();
Nuget Package System.ReactiveでRxを追加できます
テスト済みサンプル:
private static event EventHandler<EventArgs> _testEvent;
private static async Task Main()
{
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => _testEvent += h,
h => _testEvent -= h);
Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));
var res = await eventObservable.FirstAsync();
Console.WriteLine("Event got fired");
}
待機可能なイベントに独自のAsyncEventクラスを使用しています。
public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;
public class AsyncEvent : AsyncEvent<EventArgs>
{
public AsyncEvent() : base()
{
}
}
public class AsyncEvent<T> where T : EventArgs
{
private readonly HashSet<AsyncEventHandler<T>> _handlers;
public AsyncEvent()
{
_handlers = new HashSet<AsyncEventHandler<T>>();
}
public void Add(AsyncEventHandler<T> handler)
{
_handlers.Add(handler);
}
public void Remove(AsyncEventHandler<T> handler)
{
_handlers.Remove(handler);
}
public async Task InvokeAsync(object sender, T args)
{
foreach (var handler in _handlers)
{
await handler(sender, args);
}
}
public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
{
var result = left ?? new AsyncEvent<T>();
result.Add(right);
return result;
}
public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
{
left.Remove(right);
return left;
}
}
イベントを発生させるクラスでイベントを宣言するには:
public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;
イベントを発生させるには:
if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());
イベントを購読するには:
MyControl.Click += async (sender, args) => {
// await...
}
MyControl.Click += (sender, args) => {
// synchronous code
return Task.CompletedTask;
}