2010-02-17 15 views
5

Sto costruendo un'applicazione utilizzando il modello di progettazione MVVM e voglio utilizzare i RoutedUICommands definiti nella classe ApplicationCommands. Poiché la proprietà CommandBindings di una vista (leggi UserControl) non è DependencyProperty, non è possibile associare direttamente CommandBindings in ViewModel alla vista. Ho risolto questo problema definendo una classe View astratta che si lega a livello di codice, basata su un'interfaccia ViewModel che garantisce che ogni ViewModel abbia una ObservableCollection di CommandBindings. Tutto questo funziona bene, tuttavia, in alcuni scenari voglio eseguire la logica che è definita in diverse classi (il View e ViewModel) stesso comando. Ad esempio, quando si salva un documento.RoutedUICommand Anteprima Bug eseguito?

Nel ViewModel il codice salva il documento su disco:

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

A prima vista il codice precedente è tutto quello che volevo fare, ma il controllo TextBox nella vista per cui il documento è destinato, solo gli aggiornamenti la sua fonte quando perde la concentrazione. Tuttavia, posso salvare un documento senza perdere il focus premendo Ctrl + S. Ciò significa che il documento viene salvato prima delle modifiche in cui è stato aggiornato nella sorgente, ignorando effettivamente le modifiche. Ma dal momento che la modifica di UpdateSourceTrigger a PropertyChanged non è un'opzione valida per motivi di prestazioni, qualcos'altro deve forzare un aggiornamento prima di salvare. Così ho pensato, permette di utilizzare l'evento PreviewExecuted per forzare l'aggiornamento in caso PreviewExecuted, in questo modo:

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

Tuttavia, l'assegnazione di un gestore per l'evento PreviewExecuted sembra annullare l'evento del tutto, anche se ho impostato in modo esplicito il Gestito la proprietà su false. Quindi il gestore di eventi executeSave che ho definito nell'esempio di codice precedente non viene più eseguito. Si noti che quando si modifica cb.PreviewExecuted su cb.Executed, entrambe le parti del codice do vengono eseguite, ma non nell'ordine corretto.

Penso che questo sia un bug in .Net, perché dovresti essere in grado di aggiungere un gestore a PreviewExecuted ed Execed e farli eseguire in ordine, purché tu non contrassegni l'evento come gestito.

Qualcuno può confermare questo comportamento? O mi sbaglio? C'è una soluzione alternativa per questo bug?

risposta

3

EDIT 2: Guardando il codice sorgente sembra che internamente funziona così:

  1. I UIElement chiamate CommandManager.TranslateInput() in reazione ad un input dell'utente (mouse o tastiera).
  2. Il CommandManager passa quindi a CommandBindings su diversi livelli alla ricerca di un comando associato all'input.
  3. Quando viene trovato il comando viene chiamato il metodo CanExecute() e se restituisce true viene chiamato il numero Executed().
  4. In caso di RoutedCommand ognuno dei metodi fa essencially la stessa cosa - solleva una coppia di eventi allegate CommandManager.PreviewCanExecuteEvent e CommandManager.CanExecuteEvent (o PreviewExecutedEvent e ExecutedEvent) sul UIElement che ha avviato il processo. Questo conclude la prima fase.
  5. Ora il UIElement dispone di gestori di classe registrati per questi quattro eventi e questi gestori chiamano semplicemente CommandManager.OnCanExecute() e CommandManager.CanExecute() (sia per l'anteprima che per gli eventi effettivi).
  6. È qui solo nei metodi CommandManager.OnCanExecute() e CommandManager.OnExecute() in cui vengono richiamati i gestori registrati con CommandBinding. Se non sono stati trovati, lo CommandManager trasferisce l'evento fino al genitore di UIElement e il nuovo ciclo inizia finché il comando non viene gestito o viene raggiunta la radice dell'albero visivo.

Se si guarda il codice sorgente della classe CommandBinding c'è OnExecuted() metodo che è responsabile della chiamata i gestori si registra per PreviewExecuted ed eseguito eventi attraverso CommandBinding. Non v'è che po 'là:

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

questo imposta l'evento come gestito subito dopo le vostre dichiarazioni dei gestori PreviewExecuted e così il Eseguito non viene chiamato.

EDIT 1: Guardando CanExecute & eventi PreviewCanExecute v'è una differenza fondamentale:

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

impostazione Handled su true è subordinato qui e così è il programmatore che decide se procedere o meno con CanExecute . Semplicemente non impostare CanExecute di CanExecuteRoutedEventArgs su true nel gestore PreviewCanExecute e verrà chiamato il gestore CanExecute.

Per quanto riguarda la proprietà ContinueRouting dell'evento Anteprima, se impostato su false impedisce l'evento di anteprima da un ulteriore instradamento, ma non influisce in alcun modo sul seguente evento principale.

Si noti che funziona solo in questo modo quando i gestori vengono registrati tramite CommandBinding.

se si vuole ancora avere sia PreviewExecuted ed eseguito per l'esecuzione si hanno due opzioni:

  1. È possibile possibile chiamare Execute() il metodo del comando indirizzato dall'interno gestore PreviewExecuted. Solo a pensarci: potresti incontrare problemi di sincronizzazione mentre chiami il gestore Executed prima che l'anteprima sia stata completata. Per me questo non sembra un buon modo per andare.
  2. È possibile registrare il gestore di PreviewExecuted separatamente tramite il metodo statico CommandManager.AddPreviewExecutedHandler(). Questo sarà chiamato direttamente dalla classe UIElement e non coinvolgerà CommandBinding. EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

Dagli sguardi - è stato fatto in questo modo apposta. Perché? Si può solo immaginare ...

+0

La trama si infittisce ... Così ho guardato il codice sorgente che lei ha citato e lo fanno lo stesso cosa in OnCanExecute con PreviewCanExecute. Tuttavia, esiste una differenza importante tra CanExecuteRoutedEventArgs da OnCanExecute e ExecutedRoutedEventArgs da OnExecuted. Come ci si aspetterebbe, CanExecuteRoutedEventArgs contiene una proprietà ContinueRouting che esegue esattamente questo, ma per qualche motivo a cui ExecutedRoutedEventArgs deve fare a meno. Davvero non riesco davvero a capire questa scelta di Microsoft. – elmar

+0

Penso che ContinueRouting non sia coinvolto in quel processo - vedi il mio EDIT 2 nel post. Per quanto riguarda il motivo per cui l'hanno fatto in questo modo ...Guarda le due parti del metodo CommandBinding.OnExecuted(), sono quasi esattamente le stesse - potrebbe essere il classico caso di copia/incolla :) e quindi è un bug. Seriamente però, non penso sia così. Mi piace davvero sapere qual è stata la loro ragione dietro a questo. –

1

costruisco la seguente soluzione alternativa, per ottenere il comportamento ContinueRouting mancante:

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