Gestione degli input e dei comandi in WPF

Di Cristian Civera - Microsoft MVP

In questa pagina

Architettura WPF/Win32 Architettura WPF/Win32
Classi di gestione device Classi di gestione device
Gestione e tipologie degli eventi Gestione e tipologie degli eventi
Focus degli elementi Focus degli elementi
Comandi Comandi
Conclusioni Conclusioni

Windows Presentation Foundation è una nuova tecnologia nata con lo scopo di permettere agli sviluppatori di creare applicazioni di maggiore impatto visivo. Questa tecnologia è in grado di colmare molte lacune di Win32 e delle librerie MFC, ATL, WinForm .NET, ecc. L’evoluzione dei computer, di ciò che devono fare, e una richiesta sempre maggiore di essere più produttivi nello sviluppo ha fatto sentire sempre di più l’esigenza di adempiere a queste mancanze.Anche gli input, che un’applicazione può ricevere, sono cambiati e non si limitano più a mouse e tastiera, ma anche a penne stylus oppure a comandi vocali. Windows è cambiato per supportare questi nuovi device, ma l’ha fatto mascherandoli sempre come input simulati provenienti da tastiera e mouse, al fine di rendere le applicazioni già in produzione comunque funzionanti e compatibili con questi nuovi input. WPF è nuova tecnologia che, prendendo in considerazione questo aspetto, dispone di un’architettura indipendente e aperta a future forme di input.

Architettura WPF/Win32

Il motore di gestione degli input in WPF è prevalentemente contenuto nel namespace System.Windows.Input ed è organizzato in modo indipendente dal sistema operativo.
Poiché il guscio di WPF è una finestra Win32 rappresentata dal tipo HwndSource, anche se ospitata in Internet Explorer, questa si relaziona con l’esterno e fa da tramite ricevendo qualsiasi messaggio e facendolo filtrare ad apposite classi specifiche per ogni device. Si tratta di una classe che contiene l’interop per le chiamate alle API Win32, così come fanno già le WinForm .NET, e sebbene possa essere usata direttamente, viene creata internamente dalla classe System.Windows.Window.

La classe InputManager riceve tutti gli input dalle rispettive classi di interop, li accumula in uno stack denominato “staging area” ed espone degli eventi [Pre/Post]ProcessInput e [Pre/Post]Notify scatenati ad ogni elaborazione della coda alla quale più InputDevice (KeyboardDevice, MouseDevice, StylusDevice e TableDevice) si abbonano e generano una serie di eventi basilari, specifici al device, disponibili a livello di applicazione e indispensabili per il suo funzionamento. Questi eventi sono per esempio KeyUp o KeyDown, scatenati quando viene premuto un pulsante, oppure MouseUp e MouseDown, per gestire la pressione del Mouse, oppure ancora StylusUp o StylusEnter, ecc.
Tra i processori degli input, ne è presente uno particolare che ha il compito di gestire l’immissione di testo, l’autocompletamento e si relaziona con il motore di speech per consentire tra le varie funzioni, anche di dettare del testo.

Questa architettura permette quindi da un lato di trattare Win32 come uno dei possibili mondi con il quale dialogare e dall’altro di scatenare molteplici eventi anche a fronte di unico input/messaggio proveniente da Windows.

 

Classi di gestione device

L’utilizzo e la gestione dei device sono semplificati mediante delle classi statiche specifiche che sono:

  • Keyboard: permette di reperire informazioni sullo stato della tastiera. Oltre agli eventi restituisce il focus corrente con FocusedElement, lo stato dei pulsanti accessori tipo Ctrl, Alt e Shift mediante Modifiers e permette di conoscere la tipologia di un Key, un oggetto rappresentante un pulsante di una tastiera;

  • Mouse: oltre agli eventi del device permette di reperire lo stato di tutti pulsanti, di gestire il cursore, di conoscere la posizione con il metodo GetPosition e di recuperare con la proprietà Captured l’elemento che il mouse ha catturato;

  • Stylus: in modo analogo a Mouse, gestisce l’elemento catturato e molteplici eventi legati alla penna in dotazione ai Tablet PC.

Alcuni semplici utilizzi di queste classi possono essere i seguenti:

C#

// Cambio il focus
Keyboard.Focus(element1);

// Verifico la pressione del pulsante Ctrl
if (Keyboard.Modifiers == ModifierKeys.Control)
{
    // Fai qualcosa
}

// Recupero la posizione del Mouse
Point p = Mouse.GetPosition(element1);

VB

‘ Cambio il focus
Keyboard.Focus(element1)

‘ Verifico la pressione del pulsante Ctrl
If Keyboard.Modifiers = ModifierKeys.Control Then
    ‘ Fai qualcosa
End If

‘ Recupero la posizione del Mouse
Dim p As Point = Mouse.GetPosition(element1)

Va comunque sottolineato il fatto che raramente si dovrà utilizzare queste classi e che spesso gli eventi esposti dagli elementi di WPF soddisfano la maggior parte delle nostre esigenze.

 

Gestione e tipologie degli eventi

Gli eventi forniti dai device, in unione a quelli logici di funzionamento non legati agli input, sono riesposti dalla classe UIElement e sono accessibili con la normale sintassi offerta da .NET. In C# quindi la gestione della pressione del pulsante su un particolare elemento può essere fatta in questo modo:

C#

// Costruttore della classe
this.element1.KeyUp += new KeyEventHandler(element1_KeyUp);

void element1_KeyUp(object sender, KeyEventArgs e)
{
    MessageBox.Show("Pulsante premuto");
}

VB

‘ Costruttore della classe
AddHandler Me.element1.KeyUp, New KeyEventHandler(AddressOf element1_KeyUp)

Sub element1_KeyUp(sender As Object, e As KeyEventArgs)

    MessageBox.Show("Pulsante premuto")
End Sub

In XAML (eXtensible Application Markup Language) questo può essere realizzato con la medesima sintassi delle proprietà e cioè indicando come attributo il nome dell’evento e il metodo da chiamare definito nella classe codebehind e corrispondente alla firma dell’evento.

<StackPanel>
  <Button x:Name="element1"
          KeyUp="element1_KeyUp"
          Content="Apri" />
</StackPanel>

Questo comportamento non è però sufficiente in WPF. La complessa struttura ad albero degli elementi che un’applicazione può avere, gli stili e i modelli implicano una migliore gestione degli eventi che permetta ad un elemento padre di poter gestire gli eventi dei figli, di controllare il loro comportamento in funzione di un evento, o di inibire un evento qualora non lo si voglia notificare al destinatario.
Da ciò nasce un meccanismo di routing che fa transitare un evento, rappresentato dalla classe RoutedEvent, all’interno dell’albero degli elementi secondo tre modalità (RoutingStrategy):

  • Direct: l’evento viene scatenato direttamente sull’elemento destinatario. Il comportamento è quindi simile a quello già conosciuto nelle WinForm;

  • Tunnel: l’evento parte dall’elemento radice e percorre tutti gli elementi che portano al raggiungimento dell’elemento destinatario;

  • Bubble: l’evento parte dall’elemento destinatario e scende passando per il padre fino a raggiungere la radice.

Le ultime due modalità sono le più massivamente frequenti in WPF e vengono sfruttate in coppia per fornire un Preview[Evento] e un Evento. Per esempio l’evento KeyUp è accompagnato dall’evento PreviewKeyUp: il primo sfrutta il bubbling (processo di trasmissione di tipo bubble) degli eventi, il secondo il tunneling (processo di trasmissine di tipo tunnel), poiché partendo dalla radice permette di conoscere prima rispetto al destinatario il verificarsi di un evento.

La figura successiva rappresenta questo concetto con un albero di elementi.

*

Figura 1

Nel Framework .NET un normale evento è di tipo EventHandler, mentre in WPF il delegate è di tipo RoutedEventHandler ed ha la seguente firma:

C#

public delegate RoutedEventHandler(object sender, RoutedEventArgs e);

VB

Public Delegate Sub RoutedEventHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)

Il parametro sender contiene sempre l’oggetto alla quale è stato agganciato l’evento perciò, prendendo in considerazione la figura, se il destinatario dell’evento è “elemento figlio2” e l’evento è stato intercettato su “elemento1”, sender conterrà un riferimento a quest’ultimo. La classe RoutedEventArgs è quella base per ogni argomento di evento e dispone delle seguenti proprietà:

  • OriginalSource: l’oggetto originale che ha generato lo scatenarsi dell’evento;

  • Source: l’oggetto che ha scatenato l’evento. Questa informazione varia e spesso coincide con un contenitore di elementi e differisce quindi da OriginalSource;

  • RoutedEvent: l’oggetto rappresentante l’evento e i metadata ad esso riferiti;

  • Handled: normalmente impostato su false, permette di inibire il proseguimento del routing dell’evento.

La proprietà Handled è molto importante perché permette di indicare che quell’evento è stato gestito e ciò comporta che sugli elementi padri, nel caso di bubbling, o figli, nel caso di tunneling, non venga più scatenato il rispettivo evento, intercettato con il metodo canonico o XAML visti in precedenza.
Ogni IInputElement (FrameworkContentElement, FrameworkElement) però espone i metodi AddHandler e RemoveHandler che ricevono il RoutedEvent e il delegate alla funzione da intercettare o rimuovere. Il loro utilizzo differisce negli effetti, poiché, passando true come parametro a handledEventsToo, l’evento viene sempre scatenato, qualunque sia la manipolazione sulla proprietà Handled.
L’esempio seguente mostra come intercettare la pressione della tastiera a livello di intera finestra.

C#

// Intercetto tutti gli eventi di KeyUp a livello di Window
this.AddHandler(UIElement.KeyUpEvent, new KeyEventHandler(element1_KeyUp), true);
VB
‘ Intercetto tutti gli eventi di KeyUp a livello di Window
Me.AddHandler(UIElement.KeyUpEvent, New KeyEventHandler(AddressOf element1_KeyUp), True)

In XAML è possibile fare la stessa cosa sfruttando gli Attached Events che in modo analogo alle Attached Properties permettono di intercettare qualsiasi RoutedEvent con la sintassi nometipo.evento.

<StackPanel Keyboard.KeyUp="element1_KeyUp">
  <Button x:Name="element1"            
          Content="Apri" />
</StackPanel>

Molti dei controlli presenti in WPF basano le loro funzionalità proprio sull’instradamento degli eventi scatenati dai loro figli. Per esempio la classe Button espone un evento Click, ma un pulsante di per se non è niente; è lo styling che lo definisce e lo disegna con primitive come rettangoli e linee o addirittura è il suo contenuto (proprietà Content) che può perfino sconfinare le sue dimensioni ed essere un video o un’immagine. In questo caso quindi, il Button intercetta il bubbling degli eventi di KeyUp o MouseUp dei suoi più svariati figli, modifica il suo aspetto di pressione e rilancia l’evento Click a significare che il pulsante è stato premuto da qualunque tipo di device.
E’ chiaro quindi che nella maggior parte dei casi bisogna intercettare gli eventi esposti dai controlli e non gestire quegli specifici dei device.

 

Focus degli elementi

Alcuni degli elementi presenti in WPF sono controlli che interagiscono con l’utente mediante input. Quando un utente usa un’applicazione, esegue operazioni su uno o un altro controllo agendo con la tastiera, con il mouse o qualsiasi altra forma di input. Cliccando su un oggetto o spostandosi con la tastiera l’utente rende “attivo” l’elemento e cioè gli applica il Focus. Questa funzionalità è importante, perché permette all’utente di essere più produttivo con shortcut o navigazione e, non di poca importanza, di rendere più accessibile l’applicazione. Esistono due tipi di focus: keyboard e logico.

Il primo si riferisce all’elemento che riceve gli input da tastiera e può essere uno solo in un dato momento. Solitamente si distingue dal fatto che una TextBox ha il cursore lampeggiante o un Button ha un bordo tratteggiato.

Il focus logico invece è relativo ed univoco rispetto allo scope in cui si trova. Ogni applicazione può avere più zone e se l’elemento con il keyboard focus si trova nel medesimo scope, il focus logico coinciderà con il primo. Nel momento in cui il keyboard focus abbandona lo scope, il focus logico rimane invariato mentre quello keyboard cambia. Questa sottile differenza consente ad alcuni elementi, come per esempio la Toolbar, MenuItem e ContextMenu, di definire delle zone di navigazione.

La classe Keyboard, già accennata in precedenza, permette di gestire il focus da tastiera ed espone la proprietà statica FocusedElement per conoscere l’elemento che detiene il focus. In alternativa è possibile impostare l’elemento attivo, qualora supporti questo stato e la proprietà Focusable ritorni true, con il metodo Focus.

C#

if (element1.Focusable)
{
    Keyboard.Focus(element1);
    // oppure
    element1.Focus();
}
if (element1.IsKeyboardFocused)
    Console.WriteLine("Focus attivo sull'elemento");
if (element1.IsKeyboardFocusWithin)
    Console.WriteLine("Focus attivo sull'elemento e sui figli");

VB

If element1.Focusable Begin
    Keyboard.Focus(element1)
    ‘ oppure
    element1.Focus()
End If
If element1.IsKeyboardFocused Then
    Console.WriteLine("Focus attivo sull'elemento")
End If
If element1.IsKeyboardFocusWithin Then
    Console.WriteLine("Focus attivo sull'elemento e sui figli")
End If

Il tipo FocusManager invece permette di gestire il focus logico e di definire attraverso le Attached Properties l’elemento che detiene il focus, tramite FocusedElement, e qual è lo scope di un elemento, tramite IsFocusScope.
L’esempio seguente crea uno scope logico relativo ad uno StackPanel e imposta automaticamente il focus sulla prima textbox.

<Grid FocusManager.FocusedElement="{Binding ElementName=txt}">
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition />
  </Grid.ColumnDefinitions>

  <StackPanel Grid.Column="0">
    <Button x:Name="element1"            
            Content="Apri" />
    <TextBox x:Name="txt"></TextBox>
  </StackPanel>

  <!-- scope limitato allo stackpanel -->
  <StackPanel Grid.Column="1" FocusManager.IsFocusScope="true">
    <Button x:Name="element2"            
            Content="Apri2" />
    <TextBox x:Name="txt2"></TextBox>
  </StackPanel>
</Grid>

La gestione del focus lavora a stretto contatto con la navigazione che è possibile effettuare tramite tastiera. I pulsanti Tab, Control Tab e le frecce permettono di spostarsi da un elemento all’altro. La navigazione è controllata dalla classe KeyboardNavigation e può assumere diversi comportamenti che si possono impostare con ulteriori “Attached Properties” (proprietà non definite direttamente sul tipo) di nome TabNavigation, ControlNavigation e DirectionalNavigation.
All’esempio precedente è possibile aggiungere quindi come muoversi, quando si preme Tab, solamente all’interno del pannello in modo ciclico.

…
<StackPanel Grid.Column="1" KeyboardNavigation.TabNavigation="Cycle" FocusManager.IsFocusScope="true">
…

Ovviamente a tutto questo si aggiungono una serie di eventi, quali PreviewGotKeyboardFocus, GotKeyboardFocus, PreviewLostKeyboardFocus e LostKeyboardFocus, volti a notificare quando un elemento acquisisce o perde un focus keyboard o logico. L’esempio seguente svuota il contenuto di una TextBox nell’acquisire l’input dall’utente.

<TextBox x:Name="txt2" GotKeyboardFocus="txt2_GotKeyboardFocus">Testo da cercare</TextBox>

C#

void txt2_GotKeyboardFocus(object s, KeyboardFocusChangedEventArgs e)
{
    txt2.Text = String.Empty;
}

VB

void txt2_GotKeyboardFocus(object s, KeyboardFocusChangedEventArgs e)
{
    txt2.Text = String.Empty;
}

 

Comandi

Le molteplici applicazioni che vengono sviluppate, spesso sono simili tra loro nella logica e nella organizzazione. Hanno una barra di menu, una toolbar, dei menu contestuali, degli shortcut ed è possibile eseguire la medesima operazione in più modi e da più elementi dell’applicazione. WPF implementa, adotta e favorisce un pattern volto ad identificare in modo logico queste operazioni o comandi. L’idea è quella di avere un oggetto che implementi ICommand e che custodisca alcune informazioni e rappresenti un comando. Si pensi a Cut, Copy o Paste che sono comunemente raggiungibili tramite gli shortcut Ctrl-X, Ctrl-C, Ctrl-V; l’operazione logica da eseguire è la medesima, mentre l’implementazione varia a seconda dell’elemento destinatario che può identificarsi in una TextBox o una GridView o qualsiasi oggetto che supporti l’operazione.

In WPF sono già definiti molteplici command, ad essi si possono aggiungere tutti i comandi personalizzati che si ritiene di dover creare: alcuni di questi sono già implementati in controlli, altri invece sono solo suggeriti da Microsoft come ipotetiche funzionalità della nostra applicazione. Si suddividono in cinque famiglie rappresentate dalle seguenti classi:

  • MediaCommands: comandi relativi alla riproduzione di contenuti multimediali (play, stop ecc);

  • ApplicationCommands: comandi comuni in tutte le applicazioni come open, close, save, print ecc;

  • NavigationCommands: comandi relativi alla navigazione all’interno di NavigationWindow o Frame come back, forward, stop, home ecc;

  • ComponentCommands: comandi relativi ai componenti e che permettono di spostarsi, scorrere e selezionare;

  • EditingCommands: comandi dedicati alla modifica di testo e alla sua formattazione (delete, align, increase font ecc).

L’uso di un comando è relativamente semplice. Tutti i controlli che implementano ICommandSource (Button, MenuItem, HyperLink, CheckBox, RadioButton) dispongono infatti di una proprietà Command per scatenare, alla loro pressione il comando. L’esempio seguente crea un tipico menu con una voce Cut e frutta il comando ApplicationCommands.Cut per tagliare.

<Menu DockPanel.Dock="Top">
  <MenuItem Header="_Edit">
    <MenuItem Header="Cut" Command="ApplicationCommands.Cut" />
  </MenuItem>
</Menu>

Va considerato che il comando Cut non contiene nessuna logica esecutiva. Dispone di un evento CanExecuteChanged per notificare a chi resta in ascolto il fatto che quel comando può essere eseguito o meno. Questo permette ai controlli di abilitarsi o meno in modo automatico al variare del focus corrente. Il metodo CanExecute invece viene chiamato da quei controlli che possono scatenare un comando e vogliono sapere se è eseguibile o meno. Infine il metodo Execute scatena effettivamente l’esecuzione dell’operazione.

L’unica implementazione presente in WPF dell’interfaccia ICommand è la classe RoutedCommand che al momento dell’esecuzione riceve il destinatario del comando (solitamente chi detiene il keyboard focus) e ne scatena gli eventi CommandManager.PreviewExecutedEvent e CommandManager.ExecutedEvent. Quest’ultimo evento percorre quindi la strada che partendo dall’elemento destinatario lo porterà all’elemento radice in cerca di un oggetto che gestisca l’evento ed esegua quindi il comando corrispondente. Il controllo TextBox, per esempio, intercetta già i comandi di Cut, Copy, Paste, abilitandoli nel caso sia presente del testo selezionato. Quindi se nel codice precedente l’utente sta immettendo del testo in una casella, selezionando Edit->Cut, eseguirà il comando ApplicationCommands.Cut che intercettato dalla TextBox taglierà il testo correntemente selezionato.

Nel caso invece il keyboard focus fosse un altro elemento, toccherebbe allo sviluppatore gestire il comando associandolo ai metodi da eseguire. La classe CommandBinding permette di indicare sia il metodo da chiamare per sapere se è possibile eseguire un comando, sia il metodo per la sua esecuzione. Questo legame può essere fatto popolando la collezione CommandBindings che ogni UIElement espone. Nell’esempio seguente il comando apri viene scatenato da un pulsante, ma intercettato ed eseguito a livello di StackPanel.

<Menu DockPanel.Dock="Top">
  <MenuItem Header="_Edit">
    <MenuItem Header="Cut" Command="ApplicationCommands.Cut" />
  </MenuItem>
</Menu>

I due handler sono definiti nel code behind in questo modo:

C#

void Open_Executed(object sender, ExecutedRoutedEventArgs e)
{
    // Apri file
}

void Open_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    // Il comando può essere sempre eseguito
    e.CanExecute = true;
}

VB

Sub Open_Executed(sender As Object, e As ExecutedRoutedEventArgs)
    ‘ Apri file
End Sub

Sub Open_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
    ‘ Il comando può essere sempre eseguito
    e.CanExecute = true
End Sub

E’ importante il punto in cui si effettua il binding poiché la sua esecuzione è limitata allo scope in cui è definito. Nell’esempio precedente se il destinatario del comando è un controllo esterno allo StackPanel, gli handler non verranno eseguiti, infatti comandi considerati globali trovano la loro migliore collocazione sulla radice degli elementi (solitamente la Window).

Qualora ce ne fosse la necessità si possono utilizzare più CommandBinding per il medesimo comando e utilizzare quest’ultimo in più pulsanti, menu o toolbar. Facoltativamente sui controlli ICommandSource, oltre ad indicare l’ICommand, è possibile indicare il target dell’evento (di default il keyboard focus) con la proprietà CommandTarget e un parametro, con CommandParameter, utile all’esecuzione.

E’ ormai uso comune associare a questi comandi anche degli shortcut eseguibili da tastiera o mouse, perciò è possibile, in alternativa o in unione ai command source, legare gli input ai command. In modo analogo ai CommandBinding gli UIElement dispongono di una proprietà InputBindings per legare un input ad uno specifico comando. L’esempio seguente permette di aprire un file premendo Ctrl+O o premendo il pulsante centrale del mouse.

<StackPanel>
<StackPanel.InputBindings>
<KeyBinding Command="ApplicationCommands.Open"
Key="O"
Modifiers="Control" />
<MouseBinding Command="ApplicationCommands.Open"
MouseAction="MiddleClick" />
</StackPanel.InputBindings>
...

Se i comandi già definiti in WPF non bastano, occorre semplicemente definire un nuovo membro statico che restituisca un RoutedCommand al quale poi fare riferimento. Ciò può essere fatto in una qualsiasi classe, come per esempio la finestra stessa dell’applicazione.

C#

public partial class Window1 : System.Windows.Window
{

    public static RoutedUICommand PopupCommand = new RoutedUICommand("Popup...", "popup", typeof(Window1));
…
}

VB

Public Partial Class Window1
    Inherits System.Windows.Window

    Public Shared PopupCommand As New RoutedUICommand("Popup...", "popup", GetType(Window1))
…
End Class

Basta poi usare la sintassi XAML, che fa uso dei prefissi per mappare un namespace .NET, così da sfruttare il comando come fatto in precedenza.

<StackPanelxmlns:local="clr-namespace:WindowsApplication1">
<StackPanel.InputBindings>
<KeyBinding Command="local:Window1.PopupCommand"
Key="F2" />
</StackPanel.InputBindings>
...

 

Conclusioni

WPF tra le tante novità introduce un nuovo modello di gestione degli eventi molto potente e versatile. La gestione degli input è inoltre aperta a futuri nuovi device, mentre i comandi si propongono come un modo univoco di modellare le applicazioni e identificare le operazioni disponibili. Questo modello inoltre favorisce la vera separazione tra la programmazione, costituita dal codice scritto, e il disegno che dev’essere fatto sempre più esclusivamente in XAML. L’uso dei comandi, a differenza degli eventi, permette al designer di non preoccuparsi dell’implementazione, della gestione del click o di un altro evento, e consente al programmatore di concentrarsi esclusivamente sulla stesura del codice.