2012-10-12 30 views
108

在我的C#/ XAML metro應用程序中,有一個按鈕可以啓動長時間運行的進程。因此,推薦,我使用異步/等待以確保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,使其等待事件如點擊另一個按鈕?

下面是一個醜陋的方式來實現我正在尋找:在繼續」按鈕設置一個標誌事件處理程序...

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; 

輪詢顯然是可怕的(週期忙等待/廢物)和我正在尋找一些基於事件的。

任何想法?

順便說一下,在這個簡化的例子中,一種解決方案當然是將GetResults()分成兩部分,從開始按鈕調用第一部分,從繼續按鈕調用第二部分。實際上,GetResults中發生的事情更復雜,並且在執行過程中的不同點可能需要不同類型的用戶輸入。因此,將邏輯分解爲多種方法將是不平凡的。

回答

150

可以使用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; 
+0

我會使用'ManualResetEvent'。使用「SemaphoreSlim」有沒有優勢?或者你可以使用其中一種嗎? –

+5

@DanielHilgarth'ManualResetEvent(Slim)'似乎不支持'WaitAsync()'。 – svick

+0

@svick:好點。但是,由於'GetResult'已經是'async',所以你可以在這個方法中不會出現任何問題,不是嗎? –

4

理想情況下,你不要。雖然你當然可以阻止異步線程,但這是浪費資源,並不理想。

考慮用戶在按鈕等待點擊時去午餐的規範示例。

如果您在等待來自用戶的輸入時暫停了異步代碼,那麼只是在該線程暫停時浪費資源。

也就是說,如果在異步操作中設置了需要保持啓用按鈕的位置並且您正在「等待」點擊的狀態,則會更好。此時,您的GetResults方法停止

然後,當按鈕點擊,根據您所儲存的狀態,你開始另一個異步任務繼續工作。

因爲SynchronizationContext將在調用GetResults(編譯器會做,因爲使用await關鍵字的結果所使用的事件處理程序,而事實上,SynchronizationContext.Current應該是非空被捕獲,因爲你是在一個UI應用程序),你可以使用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是繼續得到您的按鈕被按下的事件結果的方法。如果你的按鈕是而不是推送,那麼你的事件處理程序什麼都不做。

+0

什麼是異步線程?在原始問題和答案中都沒有*代碼不會在UI線程上運行。 – svick

+0

@svick不正確。 'GetResults'返回一個'Task'。 「等待」只是說「運行任務,當任務完成時,在此之後繼續執行代碼」。考慮到有一個同步上下文,調用會被封送回UI線程,因爲它在'await'上被捕獲。 'await'與* Task.Wait()'不同*,完全沒有。 – casperOne

+0

我沒有說'Wait()'什麼。但'GetResults()'中的代碼將在這裏的UI線程上運行,沒有其他線程。換句話說,是的,'await'基本上可以執行任務,就像你說的那樣,但是在這裏,這個任務也在UI線程上運行。 – svick

54

當你有你需要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就像一個asyncManualResetEvent,但你只能「集」的事件一次,該事件可以有一個「結果」(在這種情況下,我們沒有使用它,所以我們只是將結果設置爲null)。

+5

因爲我解析「等待事件」與「在任務中包裝EAP」基本相同的情況,所以我肯定會更喜歡這種方法。恕我直言,這絕對是更簡單/更容易推理的代碼。 –

2

Stephen Toub發佈了這個AsyncManualResetEventon his 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; 
     } 
    } 
} 
3

這裏是我使用的工具類:

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

簡單的輔助類:

public class EventAwaiter<TEventArgs> 
{ 
    #region Fields 

    private TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>(); 

    #endregion Fields 

    #region Properties 

    public Task<TEventArgs> Task { get; set; } 

    public EventHandler<TEventArgs> Subscription => (s, e) => _eventArrived.TrySetResult(e); 

    #endregion Properties 
} 

用法:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(); 
example.YourEvent += valueChangedEventAwaiter.Subscription; 
await valueChangedEventAwaiter.Task; 
+0

你將如何清理對'example.YourEvent'的訂閱? –