2010-02-17 18 views
5

Estoy construyendo una aplicación usando el patrón de diseño de MVVM y quiero hacer uso de los RoutedUICommands definidos en la clase ApplicationCommands. Como la propiedad CommandBindings de una Vista (léase UserControl) no es DependencyProperty, no podemos vincular CommandBindings definidos en un ViewModel a la Vista directamente. Lo resolví definiendo una clase abstracta View que vincula esto programáticamente, basada en una interfaz ViewModel que garantiza que cada ViewModel tenga una ObservableCollection de CommandBindings. Todo esto funciona bien, sin embargo, en algunos escenarios quiero ejecutar la lógica que se define en diferentes clases (el View y ViewModel) mismo comando. Por ejemplo, al guardar un documento.RoutedUICommand PreviewExecuted Bug?

En el modelo de vista del código guarda el documento en el 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 primera vista, el código anterior es todo lo que quería hacer, pero el cuadro de texto en la vista a la que el documento está obligado, sólo las actualizaciones su Fuente cuando pierde su foco. Sin embargo, puedo guardar un documento sin perder el foco presionando Ctrl + S. Esto significa que el documento se guarda antes de los cambios donde se actualizó en la fuente, haciendo caso omiso de los cambios. Pero como cambiar UpdateSourceTrigger a PropertyChanged no es una opción viable por motivos de rendimiento, algo más debe forzar una actualización antes de guardar. Así que pensé, vamos a utilizar el evento PreviewExecuted para forzar la actualización en caso PreviewExecuted, así:

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

Sin embargo, la asignación de un controlador para el evento PreviewExecuted parece cancelar el evento por completo, incluso cuando establece explícitamente la Propiedad manejada a falso. Entonces, el manejador de eventos executeSave que definí en la muestra del código anterior ya no se ejecuta. Tenga en cuenta que cuando cambio cb.PreviewExecuted a cb.Executed ambas piezas de código hacen ejecutan, pero no en el orden correcto.

Creo que esto es un error en .Net, porque debería poder agregar un controlador a PreviewExecuted y Executed y hacer que se ejecuten en orden, siempre que no marque el evento como manejado.

¿Alguien puede confirmar este comportamiento? ¿O estoy equivocado? ¿Hay alguna solución para este error?

Respuesta

3

EDIT 2: De mirar el código fuente parece que internamente funciona así:

  1. Las llamadas UIElementCommandManager.TranslateInput() en reacción a la entrada del usuario (ratón o teclado).
  2. El CommandManager luego pasa por CommandBindings en diferentes niveles buscando un comando asociado a la entrada.
  3. Cuando se encuentra el comando se llama a su método CanExecute() y si devuelve true se llama al Executed().
  4. En caso de RoutedCommand cada uno de los métodos hace essencially la misma cosa - plantea un par de eventos adjuntos CommandManager.PreviewCanExecuteEvent y CommandManager.CanExecuteEvent (o PreviewExecutedEvent y ExecutedEvent) en el UIElement que inició el proceso. Eso concluye la primera fase.
  5. Ahora el UIElement tiene manipuladores de clase registrados para esos cuatro eventos y estos controladores simplemente llaman al CommandManager.OnCanExecute() y CommandManager.CanExecute() (tanto para la vista previa como para los eventos reales).
  6. Está solo aquí en CommandManager.OnCanExecute() y CommandManager.OnExecute() métodos donde se invocan los controladores registrados con CommandBinding. Si no se encuentra ninguno, el CommandManager transfiere el evento al padre de UIElement, y el nuevo ciclo comienza hasta que se maneja el comando o se alcanza la raíz del árbol visual.
método que es responsable de llamar a los manejadores que se registren para PreviewExecuted y ejecutado a través de eventos CommandBinding

Si nos fijamos en el código fuente de la clase CommandBinding hay OnExecuted(). No es que hay poco:

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

esto establece el evento como dirigido a la derecha después devuelve el manejador PreviewExecuted por lo que el ejecutado no se llama.

EDIT 1: En cuanto a CanExecute & eventos PreviewCanExecute hay una diferencia clave:

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

entorno Handled en verdad está condicionada aquí y lo que es el programador que decide si debe o no proceder con CanExecute . Simplemente no establezca canExecute CanExecuteRoutedEventArgs en true en su controlador PreviewCanExecute y se llamará al controlador CanExecute.

En cuanto a ContinueRouting propiedad del evento Vista previa: cuando se establece en falso, impide que el evento Vista previa siga el enrutamiento, pero no afecta el siguiente evento principal de ninguna manera.

Tenga en cuenta que solo funciona de esta manera cuando los controladores se registran a través de CommandBinding.

Si todavía quiere tener tanto PreviewExecuted y ejecutado para funcionar tiene dos opciones:

  1. Puede puede llamar Execute() método del comando enrutado desde dentro manejador PreviewExecuted. Solo de pensarlo, es posible que se encuentre con problemas de sincronización cuando llame al controlador ejecutado antes de que PreviewExecuted finalice. Para mí, esto no parece una buena forma de hacerlo.
  2. Puede registrar el controlador PreviewExecuted por separado a través del método estático CommandManager.AddPreviewExecutedHandler(). Se llamará directamente desde la clase UIElement y no implicará CommandBinding. EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

Por lo que parece, fue hecho de esta manera a propósito. ¿Por qué? Uno sólo puede adivinar ...

+0

La trama se complica ... Así que mirado el código fuente que ha mencionado y que hagan lo mismo cosa en OnCanExecute con PreviewCanExecute. Sin embargo, existe una diferencia importante entre CanExecuteRoutedEventArgs de OnCanExecute y ExecutedRoutedEventArgs de OnExecuted. Como cabría esperar, CanExecuteRoutedEventArgs contiene una propiedad ContinueRouting que hace exactamente eso, pero por alguna razón el ExecutedRoutedEventArgs tiene que prescindir. Realmente no puedo entender esta elección de Microsoft. – elmar

+0

Creo que ContinueRouting no está involucrado en ese proceso; vea mi EDIT 2 en la publicación. En cuanto a por qué lo hicieron de esta manera ...Mire las dos partes del método CommandBinding.OnExecuted(), son casi exactamente iguales, podría ser el caso clásico de copiar/pegar :) y luego es un error. En serio, no creo que sea el caso. Realmente me gusta saber cuál fue su razón detrás de esto. –

1

construyo la siguiente solución, para obtener el comportamiento ContinueRouting falta:

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