Dans mon application de métro C #/XAML, il y a un bouton qui lance un processus de longue durée. Donc, comme recommandé, j'utilise async/wait pour m'assurer que le thread d'interface utilisateur n'est pas bloqué:
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)
...
}
De temps en temps, les choses qui se passent dans GetResults nécessitent une entrée utilisateur supplémentaire avant de pouvoir continuer. Pour plus de simplicité, supposons que l'utilisateur ait simplement à cliquer sur un bouton "Continuer".
Ma question est: comment puis-je suspendre l'exécution de GetResults de telle sorte qu'il attend un événement tel que le clic d'un autre bouton?
Voici un moyen désagréable de réaliser ce que je recherche: le gestionnaire d'événements pour le bouton "continue" permet de définir un indicateur ...
private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
_continue = true;
}
... et GetResults l'interroge périodiquement:
buttonContinue.Visibility = Visibility.Visible;
while (!_continue) await Task.Delay(100); // poll _continue every 100ms
buttonContinue.Visibility = Visibility.Collapsed;
Le scrutin est clairement horrible (attente occupée/perte de cycles) et je cherche quelque chose d’événementiel.
Des idées?
Btw dans cet exemple simplifié, une solution serait bien sûr de scinder GetResults () en deux parties, invoquer la première partie à partir du bouton de démarrage et la seconde partie à partir du bouton Continuer. En réalité, le contenu de GetResults est plus complexe et différents types de saisie utilisateur peuvent être requis à différents moments de l'exécution. Donc, diviser la logique en plusieurs méthodes ne serait pas trivial.
Vous pouvez utiliser une instance de SemaphoreSlim Class en tant que signal:
private SemaphoreSlim signal = new SemaphoreSlim(0, 1);
// set signal in event
signal.Release();
// wait for signal somewhere else
await signal.WaitAsync();
Sinon, vous pouvez utiliser une instance de TaskCompletionSource <T> Class pour créer une Task <T> qui représente le résultat du clic du bouton:
private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
// complete task in event
tcs.SetResult(true);
// wait for task somewhere else
await tcs.Task;
Lorsque vous avez une chose inhabituelle que vous devez await
, la réponse la plus simple est souvent TaskCompletionSource
(ou une primitive async
-enabled basée sur TaskCompletionSource
).
Dans ce cas, votre besoin est assez simple, vous pouvez donc utiliser directement 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);
}
Logiquement, TaskCompletionSource
est comme un async
ManualResetEvent
, sauf que vous ne pouvez "définir" l'événement qu'une seule fois et que l'événement peut avoir un "résultat" (dans ce cas, nous ne l'utilisons pas, nous ne faisons que définir le résultat sur null
.
Voici une classe utilitaire que j'utilise:
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; }
}
Et voici comment je l'utilise:
var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;
// ... make it change ...
await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
Idéalement, vous ne faites pas. Bien que vous puissiez certainement bloquer le thread async, c'est un gaspillage de ressources, et pas idéal.
Prenons l'exemple canonique où l'utilisateur va déjeuner alors que le bouton attend d'être cliqué.
Si vous avez arrêté votre code asynchrone en attendant l'entrée de l'utilisateur, le gaspillage de ressources se produit alors que le thread est en pause.
Cela dit, il est préférable que, dans votre opération asynchrone, vous définissiez l'état que vous devez maintenir au point d'activer le bouton et que vous "attendez" un clic. À ce stade, votre méthode GetResults
arrête.
Ensuite, lorsque le bouton est cliqué sur, en fonction de l'état que vous avez enregistré, vous démarrez une autre tâche asynchrone pour continuer le travail.
Parce que le SynchronizationContext
sera capturé dans le gestionnaire d'événements qui appelle GetResults
(le compilateur procédera de la sorte en raison de l'utilisation du mot clé await
utilisé et du fait que SynchronizationContext.Current être non-nul, étant donné que vous êtes dans une application d'interface utilisateur), vous pouvez utiliser async
/await
comme suit:
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
est la méthode qui continue à obtenir les résultats dans le cas où votre bouton est enfoncé. Si votre bouton est appuyé sur pas, votre gestionnaire d'événements ne fait rien.
Classe d'assistant simple:
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);
};
}
Utilisation:
var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Stephen Toub a publié cette AsyncManualResetEvent
classe sur son blog .
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;
}
}
}
Avec Extensions réactives (Rx.Net)
var eventObservable = Observable
.FromEventPattern<EventArgs>(
h => example.YourEvent += h,
h => example.YourEvent -= h);
var res = await eventObservable.FirstAsync();
Vous pouvez ajouter Rx avec Nuget Package System.Reactive
Échantillon testé:
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");
}
J'utilise ma propre classe AsyncEvent pour les événements à venir.
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;
}
}
Pour déclarer un événement dans la classe qui déclenche des événements:
public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;
Pour soulever les événements:
if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());
Pour s'inscrire aux événements:
MyControl.Click += async (sender, args) => {
// await...
}
MyControl.Click += (sender, args) => {
// synchronous code
return Task.CompletedTask;
}