Adorner Saga: “Empty Place Holder” & TextBox.Command
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;
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:
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”:<TextBox local:TextBoxManager.Command="{Binding Path=Browse}" local:TextBoxManager.CommandParameter="Foo" ... />
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ì: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; } } };
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.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 ]; }
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:
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:<ListView> <ListView.View> <GridView> <GridViewColumn Header="Nome" /> GridView> ListView.View> <local:EmptyPlaceHolderService.Content> <TextBlock Text="Nessun elemento..." /> local:EmptyPlaceHolderService.Content> ListView>
- 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;
Quando il controllo è definitivamente caricato:static void OnContentChanged( DependencyObject d, DependencyPropertyChangedEventArgs e ) { Ensure.That( d.GetType() ).Is<ItemsControl>(); d.CastTo<ItemsControl>().Loaded += onLoaded; }
ci mettiamo all’opera, le cose degne di nota sono: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 ); } } };
- 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;
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:onItemsChanged = ( s, e ) => { var key = ( ItemContainerGenerator )s; ItemsControl control; if( managedItemsControls.TryGetValue( key, out control ) ) { if( control.Items.Any() ) { RemoveEmptyContent( control ); } else { ShowEmptyContent( control ); } } };
.mstatic 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 ) ) ); } } }