Ist es möglich, ein Ereignis anstelle einer anderen asynchronen Methode abzuwarten?


156

In meiner C # / XAML-U-Bahn-App gibt es eine Schaltfläche, die einen lang laufenden Prozess startet. Wie empfohlen verwende ich async / await, um sicherzustellen, dass der UI-Thread nicht blockiert wird:

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

Gelegentlich erfordern die Vorgänge in GetResults zusätzliche Benutzereingaben, bevor sie fortgesetzt werden können. Nehmen wir zur Vereinfachung an, der Benutzer muss nur auf die Schaltfläche "Weiter" klicken.

Meine Frage ist: Wie kann ich die Ausführung von GetResults so aussetzen, dass sie auf ein Ereignis wie das Klicken einer anderen Schaltfläche wartet ?

Hier ist ein hässlicher Weg, um das zu erreichen, wonach ich suche: Der Event-Handler für die Schaltfläche "Weiter" setzt ein Flag ...

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

... und GetResults fragt es regelmäßig ab:

 buttonContinue.Visibility = Visibility.Visible;
 while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
 buttonContinue.Visibility = Visibility.Collapsed;

Die Umfrage ist eindeutig schrecklich (beschäftigtes Warten / Verschwendung von Zyklen) und ich suche etwas Ereignisbasiertes.

Irgendwelche Ideen?

Übrigens wäre in diesem vereinfachten Beispiel eine Lösung natürlich, GetResults () in zwei Teile aufzuteilen, den ersten Teil über die Schaltfläche Start und den zweiten Teil über die Schaltfläche Weiter aufzurufen. In der Realität ist das Geschehen in GetResults komplexer und an verschiedenen Punkten der Ausführung können unterschiedliche Arten von Benutzereingaben erforderlich sein. Eine Aufteilung der Logik in mehrere Methoden wäre also nicht trivial.

Antworten:


226

Sie können eine Instanz der SemaphoreSlim-Klasse als Signal verwenden:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Alternativ können Sie eine Instanz der TaskCompletionSource <T> -Klasse verwenden , um eine Task <T> zu erstellen , die das Ergebnis des Schaltflächenklicks darstellt:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;

7
@ DanielHilgarth ManualResetEvent(Slim)scheint nicht zu unterstützen WaitAsync().
Svick

3
@ DanielHilgarth Nein, das konntest du nicht. asyncbedeutet nicht "läuft auf einem anderen Thread" oder so ähnlich. Es bedeutet nur "Sie können awaitin dieser Methode verwenden". In diesem Fall GetResults()würde das Blockieren im Inneren den UI-Thread tatsächlich blockieren.
Svick

2
@Gabe awaitan sich garantiert nicht, dass ein anderer Thread erstellt wird, aber alles andere nach der Anweisung wird als Fortsetzung des von TaskIhnen aufgerufenen oder erwarteten Ereignisses ausgeführt await. Meistens handelt es sich um eine Art asynchronen Vorgang, bei dem es sich um eine E / A-Vervollständigung handeln kann oder um etwas, das sich in einem anderen Thread befindet.
CasperOne

16
+1. Ich musste das nachschlagen, also nur für den Fall, dass andere interessiert sind: SemaphoreSlim.WaitAsyncSchiebt das nicht einfach Waitauf einen Thread-Pool-Thread. SemaphoreSlimhat eine richtige Warteschlange von Tasks, die zur Implementierung verwendet werden WaitAsync.
Stephen Cleary

14
TaskCompletionSource <T> + warte auf .Task + .SetResult () ist die perfekte Lösung für mein Szenario - danke! :-)
Max

75

Wenn Sie eine ungewöhnliche Sache haben, die Sie tun müssen, awaitist die einfachste Antwort oft TaskCompletionSource(oder ein asyncaktiviertes Grundelement basierend auf TaskCompletionSource).

In diesem Fall ist Ihr Bedarf recht einfach, sodass Sie ihn direkt verwenden TaskCompletionSourcekönnen:

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

Logischerweise TaskCompletionSourceist es wie ein async ManualResetEvent, außer dass Sie das Ereignis nur einmal "setzen" können und das Ereignis ein "Ergebnis" haben kann (in diesem Fall verwenden wir es nicht, also setzen wir das Ergebnis einfach auf null).


5
Da ich "Warte auf ein Ereignis" im Grunde genommen in der gleichen Situation wie "EAP in eine Aufgabe einwickeln" analysiere, würde ich diesen Ansatz definitiv vorziehen. IMHO, es ist definitiv einfacher / leichter zu begründender Code.
James Manning

8

Hier ist eine Utility-Klasse, die ich verwende:

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

Und so benutze ich es:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;

1
Ich weiß nicht, wie das funktioniert. Wie führt die Listen-Methode meinen benutzerdefinierten Handler asynchron aus? Wäre nicht new Task(() => { });sofort fertig?
Nawfal

5

Einfache Hilfsklasse:

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

Verwendung:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;

1
Wie würden Sie das Abonnement bereinigen example.YourEvent?
Denis P

@DenisP übergibt das Ereignis möglicherweise an den Konstruktor für EventAwaiter?
CJBrew

@DenisP Ich habe die Version verbessert und einen kurzen Test durchgeführt.
Felix Keil

Ich konnte sehen, dass je nach den Umständen auch IDisposable hinzugefügt wurde. Um zu vermeiden, dass Sie das Ereignis zweimal eingeben müssen, können Sie auch Reflection verwenden, um den Ereignisnamen zu übergeben, sodass die Verwendung noch einfacher ist. Ansonsten gefällt mir das Muster, danke.
Denis P

4

Im Idealfall nicht . Sie können den asynchronen Thread zwar blockieren, dies ist jedoch eine Verschwendung von Ressourcen und nicht ideal.

Betrachten Sie das kanonische Beispiel, in dem der Benutzer zum Mittagessen geht, während die Schaltfläche darauf wartet, angeklickt zu werden.

Wenn Sie Ihren asynchronen Code angehalten haben, während Sie auf die Eingabe des Benutzers gewartet haben, werden nur Ressourcen verschwendet, während dieser Thread angehalten wird.

Das heißt, es ist besser, wenn Sie in Ihrem asynchronen Vorgang den Status, den Sie beibehalten müssen, so einstellen, dass die Schaltfläche aktiviert ist und Sie auf einen Klick "warten". An diesem Punkt stoppt Ihre GetResultsMethode .

Dann, wenn die Schaltfläche wird geklickt, basierend auf dem Zustand , dass Sie gespeichert haben, starten Sie eine weitere asynchrone Aufgabe , die Arbeit fortzusetzen.

Da das SynchronizationContextim Ereignishandler erfasst wird, der aufruft GetResults(der Compiler tut dies aufgrund der Verwendung des verwendeten awaitSchlüsselworts und der Tatsache, dass SynchronizationContext.Current ungleich Null sein sollte, vorausgesetzt, Sie befinden sich in einer UI-Anwendung) kann async/await mag so:

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

ContinueToGetResultsAsyncist die Methode, die weiterhin die Ergebnisse erhält, falls Ihre Taste gedrückt wird. Wenn Ihre Taste nicht gedrückt wird, unternimmt Ihr Event-Handler nichts.


Welcher asynchrone Thread? Es gibt keinen Code, der nicht im UI-Thread ausgeführt wird, sowohl in der ursprünglichen Frage als auch in Ihrer Antwort.
Svick

@svick Nicht wahr. GetResultsgibt a zurück Task. awaitsagt einfach "Führen Sie die Aufgabe aus, und wenn die Aufgabe erledigt ist, setzen Sie den Code danach fort". Da es einen Synchronisationskontext gibt, wird der Aufruf zurück zum UI-Thread gemarshallt, da er auf dem Thread erfasst wird await. awaitist nicht dasselbe wie Task.Wait(), nicht im geringsten.
CasperOne

Ich habe nichts darüber gesagt Wait(). Aber der Code in GetResults()wird hier auf dem UI-Thread ausgeführt, es gibt keinen anderen Thread. Mit anderen Worten, ja, awaitführt die Aufgabe im Grunde aus, wie Sie sagen, aber hier wird diese Aufgabe auch auf dem UI-Thread ausgeführt.
Svick

@svick Es gibt keinen Grund anzunehmen, dass die Aufgabe auf dem UI-Thread ausgeführt wird. Warum nehmen Sie diese Annahme an? Es ist möglich , aber unwahrscheinlich. Und der Aufruf besteht aus zwei separaten UI-Aufrufen, technisch gesehen einem bis zum awaitund dem Code danach await, es gibt keine Blockierung. Der Rest des Codes wird in einer Fortsetzung zurückgeführt und über das Programm geplant SynchronizationContext.
CasperOne

1
Für andere, die mehr sehen wollen, siehe hier: chat.stackoverflow.com/rooms/17937 - @svick und ich haben uns im Grunde genommen falsch verstanden, aber dasselbe gesagt.
CasperOne

3

Stephen Toub hat diese AsyncManualResetEventKlasse in seinem Blog veröffentlicht .

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

0

Mit reaktiven Erweiterungen (Rx.Net)

var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => example.YourEvent += h,
                h => example.YourEvent -= h);

var res = await eventObservable.FirstAsync();

Sie können Rx mit Nuget Package System.Reactive hinzufügen

Getestete Probe:

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

0

Ich verwende meine eigene AsyncEvent-Klasse für erwartete Ereignisse.

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

So deklarieren Sie ein Ereignis in der Klasse, das Ereignisse auslöst:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Um die Ereignisse zu erhöhen:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

So abonnieren Sie die Veranstaltungen:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}

1
Sie haben einen neuen Event-Handler-Mechanismus vollständig erfunden. Vielleicht ist es das, worauf Delegierte in .NET irgendwann übersetzt werden, aber sie können nicht erwarten, dass die Leute dies übernehmen. Ein Rückgabetyp für den Delegierten (des Ereignisses) selbst kann die Leute zunächst abschrecken. Aber gute Mühe, mag wirklich, wie gut es gemacht wird.
Nawfal

@nawfal Danke! Ich habe es seitdem geändert, um die Rückgabe eines Delegaten zu vermeiden. Die Quelle ist hier als Teil von Lara Web Engine verfügbar , einer Alternative zu Blazor.
cat_in_hat
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.