2013-08-24 6 views
15

Ho un comando RoutedUICommand che può essere licenziato in due modi diversi:incoerenza nel comportamento di comando di routing WPF seconda il focus UI Stato

  • direttamente tramite ICommand.Execute su un evento click del pulsante;
  • utilizzando la sintassi dichiarativa: <button Command="local:MainWindow.MyCommand" .../>.

Il comando viene gestito solo dalla finestra superiore:

<Window.CommandBindings> 
    <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/> 
</Window.CommandBindings> 

Il primo approccio funziona solo se v'è un elemento focalizzato nella finestra. Il secondo lo fa sempre, indipendentemente dalla messa a fuoco.

Ho esaminato ICommand.Execute implementazione di BCL e ha scoperto che il comando non viene licenziato se Keyboard.FocusedElement è null, quindi questo è di progettazione. Vorrei ancora chiedermelo, perché potrebbe esserci un gestore al livello più alto (come nel mio caso) che vuole ancora ricevere comandi, anche se l'app non ha un focus sull'interfaccia utente (ad esempio, potrei voler chiamare ICommand.Execute da un'attività asincrona quando ha ricevuto un messaggio socket). Lascia che sia così, non mi è ancora chiaro perché il secondo approccio (dichiarativo) funzioni sempre indipendentemente dallo stato di messa a fuoco.

Cosa mi manca nella mia comprensione del routing del comando WPF? Sono sicuro che questo non è "un bug ma una funzionalità".

Di seguito è riportato il codice. Se ti piace giocare con esso, ecco lo full project. Fare clic sul primo pulsante: il comando verrà eseguito, poiché lo stato attivo si trova all'interno dello TextBox. Fai clic sul secondo pulsante: tutto va bene. Fai clic sul pulsante Clear Focus. Ora il primo pulsante (ICommand.Execute) non esegue il comando, mentre il secondo lo fa ancora. Dovresti fare clic sullo TextBox per far funzionare nuovamente il primo pulsante, quindi c'è un elemento focalizzato.

Questo è un esempio artificiale, ma ha imitazioni reali. Ho intenzione di inviare una domanda relativa a ospitare WinForms controlli con WindowsFormsHost ([Modificato]asked here), nel qual caso è sempre Keyboard.FocusedElementnull quando il focus è all'interno WindowsFormsHost (uccidere in modo efficace esecuzione del comando tramite ICommand.Execute).

codice XAML:

<Window x:Class="WpfCommandTest.MainWindow" 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     xmlns:local="clr-namespace:WpfCommandTest" 
     Title="MainWindow" Height="480" Width="640" Background="Gray"> 

    <Window.CommandBindings> 
     <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/> 
    </Window.CommandBindings> 

    <StackPanel Margin="20,20,20,20"> 
     <TextBox Name="textBoxOutput" Focusable="True" IsTabStop="True" Height="300"/> 

     <Button FocusManager.IsFocusScope="True" Name="btnTest" Focusable="False" IsTabStop="False" Content="Test (ICommand.Execute)" Click="btnTest_Click" Width="200"/> 
     <Button FocusManager.IsFocusScope="True" Focusable="False" IsTabStop="False" Content="Test (Command property)" Command="local:MainWindow.MyCommand" Width="200"/> 
     <Button FocusManager.IsFocusScope="True" Name="btnClearFocus" Focusable="False" IsTabStop="False" Content="Clear Focus" Click="btnClearFocus_Click" Width="200" Margin="138,0,139,0"/> 
    </StackPanel> 

</Window> 

codice C#, la maggior parte di esso è legato alla registrazione di stato di attenzione:

using System; 
using System.Windows; 
using System.Windows.Input; 

namespace WpfCommandTest 
{ 
    public partial class MainWindow : Window 
    { 
     public static readonly RoutedUICommand MyCommand = new RoutedUICommand("MyCommand", "MyCommand", typeof(MainWindow)); 
     const string Null = "null"; 

     public MainWindow() 
     { 
      InitializeComponent(); 
      this.Loaded += (s, e) => textBoxOutput.Focus(); // set focus on the TextBox 
     } 

     void CanExecuteCommmand(object sender, CanExecuteRoutedEventArgs e) 
     { 
      e.CanExecute = true; 
     } 

     void CommandExecuted(object sender, ExecutedRoutedEventArgs e) 
     { 
      var routedCommand = e.Command as RoutedCommand; 
      var commandName = routedCommand != null ? routedCommand.Name : Null; 
      Log("*** Executed: {0} ***, {1}", commandName, FormatFocus()); 
     } 

     void btnTest_Click(object sender, RoutedEventArgs e) 
     { 
      Log("btnTest_Click, {0}", FormatFocus()); 
      ICommand command = MyCommand; 
      if (command.CanExecute(null)) 
       command.Execute(null); 
     } 

     void btnClearFocus_Click(object sender, RoutedEventArgs e) 
     { 
      FocusManager.SetFocusedElement(this, this); 
      Keyboard.ClearFocus(); 
      Log("btnClearFocus_Click, {0}", FormatFocus()); 
     } 

     void Log(string format, params object[] args) 
     { 
      textBoxOutput.AppendText(String.Format(format, args) + Environment.NewLine); 
      textBoxOutput.CaretIndex = textBoxOutput.Text.Length; 
      textBoxOutput.ScrollToEnd(); 
     } 

     string FormatType(object obj) 
     { 
      return obj != null ? obj.GetType().Name : Null; 
     } 

     string FormatFocus() 
     { 
      return String.Format("focus: {0}, keyboard focus: {1}", 
       FormatType(FocusManager.GetFocusedElement(this)), 
       FormatType(Keyboard.FocusedElement)); 
     } 
    } 
} 

[UPDATE] Cambiamo leggermente il codice:

void btnClearFocus_Click(object sender, RoutedEventArgs e) 
{ 
    //FocusManager.SetFocusedElement(this, this); 
    FocusManager.SetFocusedElement(this, null); 
    Keyboard.ClearFocus(); 
    CommandManager.InvalidateRequerySuggested(); 
    Log("btnClearFocus_Click, {0}", FormatFocus()); 
} 

Ora abbiamo un altro caso interessante: non attivo logico, senza messa a fuoco della tastiera, ma il comando stil viene licenziato dal secondo pulsante, raggiunge gestore della finestra superiore e Viene eseguito (che io considero il comportamento corretto):

enter image description here

+0

IsFocusScope deve trovarsi sul pannello dello stack di contenimento anziché sui controlli stessi. –

+0

Grazie per l'idea, ma lo spostamento di 'IsFocusScope =" True "' nel contenente 'StackPanel' non cambia il comportamento descritto per quando non è attivo. Cambia comunque la semantica del routing in questo modo: se 'TextBox' ** è focalizzato ** e ha ** il proprio binding ** per' MainWindow.MyCommand' (uguale alla finestra in alto), il comando generato dal secondo pulsante non raggiungerebbe mai il 'TextBox'. La finestra in alto lo inghiottirebbe per primo. Questo non è auspicabile: voglio che l'elemento dell'interfaccia utente focalizzato sia dato in primo luogo la possibilità di gestire un 'RoutedUICommand', sparato senza un bersaglio specifico. – Noseratio

risposta

2

JoeGaggler, un mio collega, evidentemente ha trovato la ragione di questo comportamento:

Credo trovato usando riflettore: se il bersaglio comando è nullo (cioè attivo della tastiera è nullo), allora lo ICommandSource utilizza se stesso (non la finestra) come destinazione del comando, che alla fine colpisce CommandBinding per la finestra (questo è il motivo per cui il binding dichiarativo funziona).

Sto facendo questa risposta a una wiki della comunità, quindi non ottengo crediti per la sua ricerca.

13

Ok, cercherò di descrivere il problema, come ho capito. Cominciamo con una citazione dalla sezione MSDN con FAQ (Why are WPF commands not used?):

Inoltre, il gestore di comandi che l'evento indirizzato viene consegnato al è determinato dal focus corrente nell'interfaccia utente. Funziona bene se il gestore comandi è a livello di finestra, perché la finestra si trova sempre nell'albero di messa a fuoco dell'elemento attualmente focalizzato, quindi viene richiamato per i messaggi di comando. Tuttavia, non funziona per le viste secondarie che hanno i propri gestori di comandi a meno che non abbiano il focus in quel momento. Infine, solo un gestore di comandi viene mai consultato con i comandi indirizzati.

Si prega di prestare attenzione alla linea:

che hanno i propri gestori di comandi meno che non abbiano la messa a fuoco al momento.

È chiaro che quando lo stato attivo non lo è, il comando non verrà eseguito. Ora la domanda è: qual è la messa a fuoco della documentazione? Questo si riferisce al tipo di messa a fuoco? Ricordo che ci sono due tipi di focus: logico e tastiera focus.

Ora diamo una citazione da here:

L'elemento nell'ambito di applicazione messa a fuoco di Windows che ha il focus logica verrà utilizzato come destinazione del comando. Note che è l'ambito di messa a fuoco di Windows non l'ambito di messa a fuoco attivo. Ed è la messa a fuoco logica e non la messa a fuoco della tastiera. Quando si tratta del comando di routing, FocusScope rimuove qualsiasi elemento in cui vengono posizionati ed è elementi figlio dal percorso di instradamento dei comandi. Quindi, se crei un ambito di messa a fuoco nella tua app e desideri che un comando si installi su di esso, dovrai impostare manualmente il target del comando. Oppure, non è possibile utilizzare FocusScope se non per barre degli strumenti, menu, ecc. E gestire manualmente il problema di messa a fuoco del contenitore.

Secondo queste fonti, è possibile ipotizzare che l'attenzione deve essere attivo, cioè un elemento che può essere utilizzato mediante la tastiera, per esempio: TextBox.

Per indagare ulteriormente, io sono un po 'cambiato il vostro esempio (sezione XAML):

<StackPanel Margin="20,20,20,20"> 
    <StackPanel.CommandBindings> 
     <CommandBinding Command="local:MainWindow.MyCommand" CanExecute="CanExecuteCommmand" Executed="CommandExecuted"/> 
    </StackPanel.CommandBindings> 

    <TextBox Name="textBoxOutput" Focusable="True" IsTabStop="True" Height="150" Text="WPF TextBox&#x0a;"/> 

    <Menu> 
     <MenuItem Header="Sample1" Command="local:MainWindow.MyCommand" /> 
     <MenuItem Header="Sample2" /> 
     <MenuItem Header="Sample3" /> 
    </Menu> 

    <Button FocusManager.IsFocusScope="True" 
      Name="btnTest" Focusable="False" 
      IsTabStop="False" 
      Content="Test (ICommand.Execute)" 
      Click="btnTest_Click" Width="200"/> 

    <Button FocusManager.IsFocusScope="True" 
      Content="Test (Command property)" 
      Command="local:MainWindow.MyCommand" Width="200"/> 

    <Button FocusManager.IsFocusScope="True" 
      Name="btnClearFocus" Focusable="False" 
      IsTabStop="False" Content="Clear Focus" 
      Click="btnClearFocus_Click" Width="200" 
      Margin="138,0,139,0"/> 
</StackPanel> 

ho aggiunto il comando nel StackPanel e ha aggiunto Menu controllo. Ora, se si fa clic per cancellare, pulsanti di controllo associati con il comando, non saranno disponibili:

enter image description here

Ora, se si clicca sul pulsante Test (ICommand.Execute) vediamo la seguente:

enter image description here

La messa a fuoco della tastiera è impostata su Window, ma il comando non viene ancora eseguito. Ancora una volta, ricorda la nota, sopra:

Si noti che è l'ambito di messa a fuoco di Windows non l'ambito di messa a fuoco attivo.

Non ha una messa a fuoco attiva, quindi il comando non funziona. Essa funziona solo se la messa a fuoco è attivo, impostato su TextBox:

enter image description here

Torniamo al tuo esempio originale.

Chiaramente, il primo Button non causa il comando, senza il focus attivo. L'unica differenza è che in questo caso, il secondo pulsante non è disabilitato perché non è attivo il focus, quindi facendo clic su di esso, chiamiamo direttamente il comando. Forse, questo si spiega con una serie di MSDN citazioni:

Questo funziona bene se il gestore di comandi è al livello della finestra, perché la finestra è sempre nell'albero fuoco dell'elemento attualmente concentrata, in modo che viene chiamato per i messaggi di comando.

Penso, ho trovato un'altra fonte che dovrebbe spiegare questo strano comportamento. Preventivo da here:

Le voci di menu oi pulsanti della barra degli strumenti sono posizionati di default in un FocusScope separato (rispettivamente per il menu o la barra degli strumenti). Se alcuni di questi elementi attivano i comandi indirizzati, e non hanno già un obiettivo di comando già impostato, WPF cerca sempre un obiettivo di comando cercando l'elemento che ha il focus sulla tastiera all'interno della finestra di contenimento (ad esempio il prossimo ambito di messa a fuoco più alto).

Quindi WPF NON cerca semplicemente i collegamenti dei comandi della finestra che lo contiene, come intuitivamente si aspetta, ma cerca sempre un elemento focalizzato sulla tastiera da impostare come destinazione del comando corrente! Apparentemente il team WPF ha preso il percorso più veloce per fare in modo che comandi incorporati come Copy/Cut/Paste funzionino con finestre che contengono più caselle di testo o simili; sfortunatamente hanno rotto ogni altro comando lungo la strada.

Ed ecco perché: se l'elemento focalizzato all'interno della finestra di contenimento non può ricevere il focus della tastiera (ad esempio, è un'immagine non interattiva), quindi TUTTE le voci di menu e i pulsanti della barra degli strumenti sono disabilitati - anche se non richiedono alcun comando obiettivo da eseguire! Il gestore CanExecute di tali comandi viene semplicemente ignorato.

Apparentemente l'unica soluzione alternativa per il problema n. 2 è impostare in modo esplicito CommandTarget di tali voci di menu o pulsanti della barra degli strumenti sulla finestra di contenimento (o qualche altro controllo).

+0

Grazie per la tua ricerca, sono felice di non essere solo :) Credo di avere una buona conoscenza del concetto di messa a fuoco logica e della sua portata (anche se in realtà confusa; IMO, la spiegazione migliore è [qui] (http://stackoverflow.com/a/10834342/1768303)). Ecco perché ho messo "FocusManager.IsFocusScope =" True "' sui pulsanti (per farli comportarsi come una voce di menu) e non ho bisogno di un altro campo di messa a fuoco oltre a quello della finestra principale. Controlla il mio ultimo aggiornamento per vedere cosa intendo. Ciò che conta qui, penso, è che nel secondo caso il comando * ha un invocatore * (il pulsante), mentre nel primo caso non lo fa. – Noseratio

+0

Per quanto riguarda il collegamento a [articolo MSDN su PRISM] (http://msdn.microsoft.com/en-us/library/ff921126.aspx), IMO è in realtà abbastanza indicativo in quanto scoraggiano i comandi WPF nativi per un MVVM serio Quadro WPF. – Noseratio

+1

Ho votato la tua risposta per l'utile informazione e come riconoscimento per la tua ricerca. Tuttavia, sto ancora cercando una spiegazione sul perché il caso "ICommand.CanExecute/Execute' non funzioni, specialmente alla luce di questa citazione: * Inoltre, il gestore di comandi a cui viene consegnato l'evento indirizzato è determinato dal focus attuale nell'interfaccia utente. Funziona bene se il gestore comandi è a livello di finestra, perché la finestra è sempre nella struttura del focus dell'elemento correntemente attivo, quindi viene richiamata per i messaggi di comando. * – Noseratio