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:
image
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:
image
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