2012-12-21 68 views
1

我有一個WPF應用程序,它遵循MVVM。BusyIndi​​cator允許觸發兩次RelayCommand

爲了使UI響應,我使用TPL執行長時間運行的命令,並且BusyIndicator向用戶顯示,該應用程序現在處於忙碌狀態。

在視圖模型之一我有這樣的命令:

public ICommand RefreshOrdersCommand { get; private set; } 

public OrdersEditorVM() 
{ 
    this.Orders = new ObservableCollection<OrderVM>(); 
    this.RefreshOrdersCommand = new RelayCommand(HandleRefreshOrders); 

    HandleRefreshOrders(); 
} 

private void HandleRefreshOrders() 
{ 
    var task = Task.Factory.StartNew(() => RefreshOrders()); 
    task.ContinueWith(t => RefreshOrdersCompleted(t.Result), 
     CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); 
    task.ContinueWith(t => this.LogAggregateException(t.Exception, Resources.OrdersEditorVM_OrdersLoading, Resources.OrdersEditorVM_OrdersLoadingFaulted), 
     CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext()); 
} 

private Order[] RefreshOrders() 
{ 
    IsBusy = true; 
    System.Diagnostics.Debug.WriteLine("Start refresh."); 
    try 
    { 
     var orders = // building a query with Entity Framework DB Context API 

     return orders 
      .ToArray(); 
    } 
    finally 
    { 
     IsBusy = false; 
     System.Diagnostics.Debug.WriteLine("Stop refresh."); 
    } 
} 

private void RefreshOrdersCompleted(Order[] orders) 
{ 
    Orders.RelpaceContent(orders.Select(o => new OrderVM(this, o))); 

    if (Orders.Count > 0) 
    { 
     Orders[0].IsSelected = true; 
    } 
} 

IsBusy屬性綁定與BusyIndicator.IsBusy,和RefreshOrdersCommand屬性綁定與工具欄上的按鈕,其被置於內部BusyIndicator中的視圖這個視圖模型。

問題

如果用戶點擊按鈕不經常,一切工作正常:BusyIndicator用按鈕隱藏工具欄,數據變成加載,BusyIndicator消失。在輸出窗口中,我可以看到線對:

開始刷新。 停止刷新。

但是,如果用戶點擊按鈕非常頻繁,看起來像BusyIndicator不會隱藏在時間欄,和兩個後臺線程試圖執行RefreshOrders方法,這會導致異常(它是OK的,因爲EF DbContext不是線程安全的)。在輸出窗口中我看到這張圖片:

開始刷新。 開始刷新。

我在做什麼錯?

我查看了BusyIndicator的代碼。我不知道,什麼都可以錯在那裏:設置IsBusy只是讓兩個調用VisualStateManager.GoToState,這反過來,只是讓可見的矩形,其中隱藏BusyIndicator的內容:

    <VisualState x:Name="Visible"> 
         <Storyboard> 
          <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="busycontent" Storyboard.TargetProperty="(UIElement.Visibility)"> 
          <DiscreteObjectKeyFrame KeyTime="00:00:00"> 
           <DiscreteObjectKeyFrame.Value> 
            <Visibility>Visible</Visibility> 
           </DiscreteObjectKeyFrame.Value> 
          </DiscreteObjectKeyFrame> 
          </ObjectAnimationUsingKeyFrames> 
          <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="overlay" Storyboard.TargetProperty="(UIElement.Visibility)"> 
          <DiscreteObjectKeyFrame KeyTime="00:00:00"> 
           <DiscreteObjectKeyFrame.Value> 
            <Visibility>Visible</Visibility> 
           </DiscreteObjectKeyFrame.Value> 
          </DiscreteObjectKeyFrame> 
          </ObjectAnimationUsingKeyFrames> 
         </Storyboard> 
        </VisualState> 

...和禁用內容:

    <VisualState x:Name="Busy"> 
         <Storyboard> 
          <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.001" Storyboard.TargetName="content" Storyboard.TargetProperty="(Control.IsEnabled)"> 
          <DiscreteObjectKeyFrame KeyTime="00:00:00"> 
           <DiscreteObjectKeyFrame.Value> 
            <sys:Boolean>False</sys:Boolean> 
           </DiscreteObjectKeyFrame.Value> 
          </DiscreteObjectKeyFrame> 
          </ObjectAnimationUsingKeyFrames> 
         </Storyboard> 
        </VisualState> 

任何想法?

更新。 問題不在於如何防止命令再次進入。我想知道機制,它允許按兩次按鈕。

下面是它如何工作(從我的角度):

  • ViewModel.IsBusyBusyIndicator.IsBusy約束。綁定是同步的。
  • 設置者BusyIndicator.IsBusy調用VisualStateManager.GoToState兩次。其中一個電話會顯示一個矩形,它與BusyIndicator的內容重疊(按鈕,在我的情況中)。
  • 所以,據我所知,我設置ViewModel.IsBusy後,我無法物理實現按鈕,因爲所有這些事情都發生在同一個(UI)線程上。

但是如何按下按鈕兩次?

回答

3

我不會試圖依靠忙指標來控制有效的程序流。命令執行設置爲IsBusy,如果IsBusy已經是True,它本身不應該運行。

private void HandleRefreshOrders() 
{ 
    if (IsBusy) 
     return; 
... 

您也可以將按鈕的Enabled狀態結合到IsBusy爲好,但我覺得最核心的解決方案是從無意重新門禁你的命令。開除工人也會增加一些複雜性。我會將狀態設置移至HandleRefreshOrders,然後處理ContinueWith實現中狀態的重置。 (可能需要一些額外的意見/讀取,以確保它是線程安全的。)

*編輯:澄清移動IsBusy,以避免重複執行:

private void HandleRefreshOrders() 
{ 
    if (IsBusy) 
     return; 

    IsBusy = true; 

    var task = Task.Factory.StartNew(() => RefreshOrders()); 
    task.ContinueWith(t => 
      { 
       RefreshOrdersCompleted(t.Result); 
       IsBusy = false; 
      }, 
     CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext()); 
    task.ContinueWith(t => 
      { 
       this.LogAggregateException(t.Exception, Resources.OrdersEditorVM_OrdersLoading, Resources.OrdersEditorVM_OrdersLoadingFaulted); 
       IsBusy = false; 
      }, 
    CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext()); 
} 

然後從RefreshOrders刪除IsBusy引用方法。一旦你點擊按鈕一次,IsBusy將被設置。如果用戶快速點擊,則第二次嘗試觸發命令將跳過。任務將在後臺線程上執行,一旦完成,IsBusy標誌將被重置,命令將再次響應。目前假定對RefreshOrdersCompletedLogAggregateException的調用不會冒泡異常,否則如果它們拋出,則不會重置該標誌。可以在這些調用之前移動標誌,或者在annon中使用try/finally。方法聲明。

+0

>「我不會試圖依靠繁忙的指標來控制有效的程序流」。爲什麼?你能爭辯這個嗎?我無法看到'if(IsBusy)return'的任何關鍵區別。此外,問題不在於「如何防止雙重命令執行」。我不明白,我怎樣才能實現雙擊按鈕,這必須在第一次點擊後隱藏。 – Dennis

+0

也許更清楚地解釋你的問題,因爲你是自相矛盾的。 「看起來像BusyIndi​​cator沒有及時隱藏工具欄,兩個後臺線程試圖執行RefreshOrders方法,...開始刷新,開始刷新 我在做什麼錯了?這看起來像是在問你爲什麼要執行雙重命令。或者,您希望啓用雙擊按鈕的問題?在這種情況下,一般選項是在命令上啓動一個Timer,如果再次點擊時,執行雙擊行爲,如果超時則單擊行爲。 –

+0

你說得對,我已經對這個問題做了一些澄清。 – Dennis