webentwicklung-frage-antwort-db.com.de

Kann man ein Ereignis statt einer anderen asynchronen Methode abwarten?

In meiner C #/XAML-Metro-App gibt es eine Schaltfläche, die einen langwierigen 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 erforderten die Dinge in GetResults zusätzliche Benutzereingaben, bevor sie fortfahren 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 es auf ein event wartet, wie z. B. das Klicken einer anderen Schaltfläche?

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

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 (fleißiges Warten/Verschwendung von Zyklen) und ich suche etwas Eventbezogenes.

Irgendwelche Ideen?

In diesem vereinfachten Beispiel wäre eine Lösung natürlich, GetResults () in zwei Teile aufzuteilen, den ersten Teil von der Startschaltfläche und den zweiten Teil von der Weiter-Schaltfläche aufzurufen. In der Tat ist das, was in GetResults passiert, komplexer und an verschiedenen Stellen der Ausführung können verschiedene Arten von Benutzereingaben erforderlich sein. Das Aufteilen der Logik in mehrere Methoden wäre also nicht trivial.

135
Max

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 Klickens der Schaltfläche darstellt:

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

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

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

Wenn Sie eine ungewöhnliche Sache haben, für die Sie await benötigen, ist die einfachste Antwort häufig TaskCompletionSource (oder ein async- fähiges Grundelement basierend auf TaskCompletionSource).

In diesem Fall ist Ihr Bedarf recht einfach. Sie können TaskCompletionSource direkt verwenden:

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

Logisch ist TaskCompletionSource wie ein asyncManualResetEvent, nur 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 auf null). .

62
Stephen Cleary

Hier ist eine Utility-Klasse, die ich benutze:

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;
5
Anders Skovborg

Im Idealfall Sie nicht. Während Sie den asynchronen Thread sicherlich blockieren können, ist dies eine Verschwendung von Ressourcen und nicht ideal.

Betrachten Sie das kanonische Beispiel, in dem der Benutzer zum Mittagessen geht, während die Schaltfläche auf das Klicken wartet.

Wenn Sie Ihren asynchronen Code angehalten haben, während Sie auf die Eingabe des Benutzers warten, verschwenden Sie lediglich Ressourcen, während der Thread angehalten wird.

Das heißt, es ist besser, wenn Sie in Ihrer asynchronen Operation den Status festlegen, den Sie beibehalten müssen, bis die Schaltfläche aktiviert ist und Sie auf einen Klick warten. An diesem Punkt stoppt Ihre GetResults-Methode .

Wenn Sie anschließend auf die Schaltfläche ist klicken, starten Sie eine andere asynchrone Task, um die Arbeit fortzusetzen.

Die SynchronizationContext wird in der Ereignisbehandlungsroutine erfasst, die GetResults aufruft (der Compiler führt dies aufgrund der Verwendung des await-Schlüsselworts und der Tatsache aus, dass SynchronizationContext.Current nicht null sein sollte Wenn Sie sich in einer UI-Anwendung befinden, können Sie async/await wie folgt verwenden:

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 ist die Methode, die die Ergebnisse auch dann liefert, wenn Ihre Taste gedrückt wird. Wenn Ihre Schaltfläche nicht gedrückt ist, führt der Ereignishandler nichts aus.

4
casperOne

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

Verwendungszweck:

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

Stephen Toub hat diese AsyncManualResetEvent-Klasse 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; 
        } 
    } 
}
3
Drew Noakes

With Reaktive 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

Testprobe:

    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
Felix Keil

Ich verwende meine eigene AsyncEvent-Klasse für abwartende 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 auszulösen:

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;
}
0
integrative.io