Il nuovo e fiammante Visual Studio 2010 Beta 1 ha partorito il primo progettino… e non poteva che essere un behavior per WPF.
Ci sono ancora 2 cose, e probabilmente molte altre ;-), che l’utente è abituato ad avere in campi diversi ma per certi versi complementari:
Interazione con l’(eco)sistema attraverso la tastiera: l’utente quando ad esempio esegue una ricerca è decisamente abituato a:
- inserire i criteri di ricerca, come ad esempio un elenco di keyword, all’interno di una TextBox;
- premere invio;
e non a prendere il mouse e “pigiare” il bottone “cerca” o a spostare il focus, tabbando, sul pulsante “cerca” e qui premere invio.
Feedback visuali: sempre parlando di ricerche avere feedback sui risultati di una ricerca;
Come al solito la mia musa ispiratrice è Outlook 2007, che avrà una montagna di magagne, ma in termini di UX è semplicemente fenomenale:
Il primo “problema” sembra di facile soluzione ma utilizzando WPF in coppia con il nostro fido Model-View-ViewModel la cosa non è fattibile per il semplice motivo che, ad esempio, TextBox non espone una proprietà “Command” non ci resta quindi alternativa che farlo:
<TextBox local:TextBoxManager.Command="{Binding Path=Browse}" local:TextBoxManager.CommandParameter="Foo" ... />
Tralascio tutta la solita “tiritera” sulla dichiarazione delle attached properties, della classe statica etc. etc… e mi concentro sull’unica parte interessante: quello che ci interessa fare è monitorare i tasti che vengono premuti (PreviewKeyDown) e se corrispondono ad una determinata sequenza (InputGesture) eseguire il command associato, per impostazione predefinita se il command non ha associato nessuna InputGesture viene intercettata la pressione del tasto “Invio”:
onPreviewKeyDown = ( s, e ) =>
{
var d = ( DependencyObject )s;
var cmd = GetCommand( d );
var prm = GetCommandParameter( d );
if( cmd.CanExecute( prm ) )
{
var gestures = GetGestures( cmd );
if( ( ( gestures.None() && e.Key == Key.Enter ) || gestures.Where( gesture => gesture.Matches( d, e ) ).Any() ) )
{
cmd.Execute( prm );
e.Handled = true;
}
}
};
Recuperiamo un riferimento al comando e all’eventuale parametro, se il comando può essere eseguito, verifichiamo le gesture e in caso affermativo eseguiamo; “gestures” è una collection di input gesture che viene estratta così:
static IEnumerable<InputGesture> GetGestures( ICommand source )
{
Ensure.That( source ).Named( "source" ).IsNotNull();
IEnumerable<InputGesture> gestures = null;
if( source is DelegateCommand )
{
var cmd = ( DelegateCommand )source;
if( cmd.InputBindings != null && cmd.InputBindings.Count > 0 )
{
gestures = cmd.InputBindings.OfType<InputBinding>().Select( ib => ib.Gesture );
}
}
else if( source is RoutedCommand )
{
var cmd = ( RoutedCommand )source;
if( cmd.InputGestures != null && cmd.InputGestures.Count > 0 )
{
gestures = cmd.InputGestures.OfType<InputGesture>();
}
}
else
{
throw new NotSupportedException( String.Format( "Unsupported command type: {0}", source ) );
}
return gestures ?? new InputGesture[ 0 ];
}
Viene fatta così perchè abbiamo, o meglio io ho, la necessità di distinguere se il command è di tipo RoutedCommand (nativo di Wpf), che è la base di tutti i Command che hanno InputGesture, o se è di tipo DelegateCommand (nativo di San Corrado) che è la base di tutti i miei command ed espone le gesture in maniera leggermente diversa.
Venendo invece al “problema” feedback visuale Wpf dimostra un’altra volta tutta la sua potenza:
Il risultato “Nessun elemento…” è ottenibile con un semplicissimo xaml del tipo:
<ListView>
<ListView.View>
<GridView>
<GridViewColumn Header="Nome" />
</GridView>
</ListView.View>
<local:EmptyPlaceHolderService.Content>
<TextBlock Text="Nessun elemento..." />
</local:EmptyPlaceHolderService.Content>
</ListView>
Naturalmente quando ci sono elementi, o quando compaiono dinamicamente degli elementi, il “place holder” scompare automaticamente. Anche in questo caso le cose essenziali, anche se è un filino più complesso:
- vogliamo poter gestire non solo la ListView, ma in generale gli ItemsControl;
- dobbiamo trovare un sistema per generalizzare il sistema in modo che funzioni sia se manipoliamo manualmente la collection Items sia se siamo in binding: ItemContainerGenerator;
Per prima cosa quindi quando la attached property Content cambia ci agganciamo all’evento Loaded del controllo:
static void OnContentChanged( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
Ensure.That( d.GetType() ).Is<ItemsControl>();
d.CastTo<ItemsControl>().Loaded += onLoaded;
}
Quando il controllo è definitivamente caricato:
onLoaded = ( s, e ) =>
{
var control = ( ItemsControl )s;
var key = control.ItemContainerGenerator;
if( !managedItemsControls.ContainsKey( key ) )
{
managedItemsControls.Add( key, control );
key.ItemsChanged += onItemsChanged;
control.Unloaded += onUnloaded;
if( control.Items.None() )
{
ShowEmptyContent( control );
}
}
};
ci mettiamo all’opera, le cose degne di nota sono:
- L’uso di ItemContainerGenerator che l’oggetto responsabile della creazione degli item in un ItemsControl, è lui che “sa”, a prescindere da come interagiamo con il controllo;
- Utilizziamo un dictionary (managedItemsControl) per tenere un legame tra l’ItemContainerGenerator e il controllo, ci servirà dopo, siamo obbligati a questo perchè non c’è mezzo di risalire da un ItemContainerGenerator al controllo che lo sta usando;
- Infine se il controllo non contiene nessun elemento visualizziamo il nostro adorner;
L’altro passaggio degno di nota è la gestione dell’evento ItemsChanged:
onItemsChanged = ( s, e ) =>
{
var key = ( ItemContainerGenerator )s;
ItemsControl control;
if( managedItemsControls.TryGetValue( key, out control ) )
{
if( control.Items.Any() )
{
RemoveEmptyContent( control );
}
else
{
ShowEmptyContent( control );
}
}
};
Recuperiamo dal nostro dictionary, sulla base dell’ItemContainerGenerator che ha scatenato l’evento, il controllo e facciamo quello che dobbiamo fare. Remove e Show EmptyContent altro non fanno che visualizzare o nascondere l’adorner:
static void RemoveEmptyContent( UIElement control )
{
AdornerLayer layer = AdornerLayer.GetAdornerLayer( control );
Debug.WriteLineIf( layer == null, "EmptyPlaceHolderService: cannot find any AdornerLayer" );
if( layer != null )
{
Adorner[] adorners = layer.GetAdorners( control );
if( adorners != null )
{
adorners.OfType<EmptyContentAdorner>()
.ForEach( adorner =>
{
adorner.Visibility = Visibility.Hidden;
layer.Remove( adorner );
} );
}
}
}
static void ShowEmptyContent( Control control )
{
AdornerLayer layer = AdornerLayer.GetAdornerLayer( control );
Debug.WriteLineIf( layer == null, "EmptyPlaceHolderService: cannot find any AdornerLayer" );
if( layer != null )
{
Adorner[] adorners = layer.GetAdorners( control );
if( !( adorners != null && adorners.OfType<EmptyContentAdorner>().Any() ) )
{
layer.Add( new EmptyContentAdorner( control, control.GetValue( ContentProperty ) ) );
}
}
}
.m