2010-02-17 42 views
5

我正在構建一個使用MVVM設計模式的應用程序,並且想要使用ApplicationCommands類中定義的RoutedUICommands。由於View(讀UserControl)的CommandBindings屬性不是DependencyProperty,因此我們無法直接將ViewModel中定義的CommandBindings綁定到View。我通過定義一個抽象的View類來解決這個問題,它通過一個ViewModel接口確保每個ViewModel具有一個CommandBindings的ObservableCollection,從而以編程方式綁定這個類。這一切都很好,但是,在某些情況下,我想要執行在不同類(View和ViewModel)相同的命令中定義的邏輯。例如,保存文檔時。RoutedUICommand Preview執行的Bug?

在視圖模型的代碼將文檔保存到磁盤:

private void InitializeCommands() 
{ 
    CommandBindings = new CommandBindingCollection(); 
    ExecutedRoutedEventHandler executeSave = (sender, e) => 
    { 
     document.Save(path); 
     IsModified = false; 
    }; 
    CanExecuteRoutedEventHandler canSave = (sender, e) => 
    { 
     e.CanExecute = IsModified; 
    }; 
    CommandBinding save = new CommandBinding(ApplicationCommands.Save, executeSave, canSave); 
    CommandBindings.Add(save); 
} 

乍一看前面的代碼是所有我想做的事,但在查看文本框到了該文檔的約束,只有更新它的來源,當它失去了重點。但是,通過按Ctrl + S,我可以保存文檔而不會丟失焦點。這意味着文檔在源代碼中更新之前保存,有效地忽略了更改。但由於出於性能原因將UpdateSourceTrigger更改爲PropertyChanged不是可行的選項,因此其他內容必須在保存前強制更新。所以我想,讓使用PreviewExecuted事件迫使更新的PreviewExecuted事件,像這樣:

//Find the Save command and extend behavior if it is present 
foreach (CommandBinding cb in CommandBindings) 
{ 
    if (cb.Command.Equals(ApplicationCommands.Save)) 
    { 
     cb.PreviewExecuted += (sender, e) => 
     { 
      if (IsModified) 
      { 
       BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty); 
       be.UpdateSource(); 
      } 
      e.Handled = false; 
     }; 
    } 
} 

然而,分配一個處理程序的PreviewExecuted事件似乎完全取消的情況下,甚至當我明確地設置將屬性處理爲false。所以我在之前的代碼示例中定義的executeSave事件處理程序不再執行。請注意,當我將cb.PreviewExecuted更改爲cb.Executed代碼時,執行但未按正確的順序執行。

我認爲這是.Net中的一個Bug,因爲您應該能夠向PreviewExecuted和Executed添加一個處理程序,並按順序執行它們,前提是您不要將該事件標記爲已處理。

任何人都可以證實此行爲?或者我錯了?有沒有解決這個Bug的方法?

回答

3

編輯2:通過觀察源代碼似乎內部它的工作原理一樣,:

  1. UIElement呼叫CommandManager.TranslateInput()在反應於用戶輸入(鼠標或鍵盤)。
  2. CommandManager然後在不同層上經歷CommandBindings尋找與輸入相關聯的命令。
  3. 找到該命令後,將調用CanExecute()方法,如果返回true,則調用Executed()
  4. 在每個RoutedCommand的方法情況下essencially做同樣的事情 - 它提出了一對上發起的過程中UIElement附着事件CommandManager.PreviewCanExecuteEventCommandManager.CanExecuteEvent(或PreviewExecutedEventExecutedEvent)的。第一階段結束。
  5. 現在UIElement已經爲這四個事件註冊了類處理程序,這些處理程序只需調用CommandManager.OnCanExecute()CommandManager.CanExecute()(對於預覽和實際事件)。
  6. 只有在CommandManager.OnCanExecute()CommandManager.OnExecute()方法中調用了註冊爲CommandBinding的處理程序。如果沒有找到,則CommandManager將事件傳輸到UIElement的父節點,然後新的週期開始,直到處理該命令或到達可視樹的根。

如果你看一下類的CommandBinding源代碼有OnExecuted()方法,它負責調用您註冊通過的CommandBinding PreviewExecuted和執行的事件處理程序。有那麼一點:

PreviewExecuted(sender, e); 
e.Handled = true; 

這將事件設置爲在您的PreviewExecuted處理程序返回後處理,因此未調用Executed。

編輯1:看看CanExecute & PreviewCanExecute事件有一個關鍵的區別:

PreviewCanExecute(sender, e); 
    if (e.CanExecute) 
    { 
    e.Handled = true; 
    } 

設置進行處理,以真正的是在這裏的條件,所以它是誰決定是否與CanExecute進行編程。只需在您的PreviewCanExecute處理程序中將CanExecuteRoutedEventArgs的CanExecute設置爲true,並且將調用CanExecute處理程序。

至於​​Preview事件的屬性 - 設置爲false時,它會阻止Preview事件進一步路由,但它不會以任何方式影響以下主事件。

請注意,只有在通過CommandBinding註冊處理程序時,它才能以此方式工作。

如果你仍想兼得PreviewExecuted和執行運行,你有兩種選擇:

  1. 您可以從PreviewExecuted處理程序中調用路由命令Execute()方法。試想一下 - 在PreviewExecuted完成之前,您可能會遇到同步問題,因爲您正在調用Executed處理程序。對我來說,這看起來並不是一條好路。
  2. 您可以通過CommandManager.AddPreviewExecutedHandler()靜態方法分別註冊PreviewExecuted處理程序。這將從UIElement類直接調用,不會涉及CommandBinding。 EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

從外觀上看 - 這是故意這樣做的。爲什麼?人們只能猜測...

+0

情況越來越複雜......所以,我看着你提到的源代碼和他們做同樣的OnCanExecute與PreviewCanExecute中的東西。但是,OnCanExecute的CanExecuteRoutedEventArgs和OnExecuted的ExecutedRoutedEventArgs之間存在重要的區別。正如你所期望的那樣,CanExecuteRoutedEventArgs包含一個ContinueRouting屬性,它正是這樣做的,但由於某種原因,ExecutedRoutedEventArgs必須不做。我真的無法從微軟那裏得到我的選擇。 – elmar 2010-02-23 10:06:36

+0

我認爲ContinueRouting不參與該過程 - 請參閱我的編輯2的帖子。 至於他們爲什麼這樣做...看看CommandBinding.OnExecuted()方法的兩部分,它們幾乎完全一樣 - 它可能是複製/粘貼的經典案例:)然後它是一個錯誤。說真的,我不認爲是這樣。 我真的很想知道他們背後的原因是什麼。 – 2010-02-23 19:04:23

1

我建立了以下解決方法,取得缺少ContinueRouting行爲:

foreach (CommandBinding cb in CommandBindings) 
{ 
    if (cb.Command.Equals(ApplicationCommands.Save)) 
    { 
     ExecutedRoutedEventHandler f = null; 
     f = (sender, e) => 
     { 
      if (IsModified) 
      { 
       BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty); 
       be.UpdateSource(); 

       // There is a "Feature/Bug" in .Net which cancels the route when adding PreviewExecuted 
       // So we remove the handler and call execute again 
       cb.PreviewExecuted -= f; 
       cb.Command.Execute(null); 
      } 
     }; 
     cb.PreviewExecuted += f; 
    } 
}