I. Introduction

Nous allons montrer, à travers un exemple, la puissance d'utilisation des Listboxes en Silverlight.

Notre exemple nous permettra de mettre en place des items d'une ListBox non sélectionnables par l'utilisateur, c'est-à-dire en gérant la souris et le clavier. En revanche, la sélection restera possible en fixant les propriétés SelectedItem et SelectedIndex.

II. Prérequis

Ces exemples ont été développés en Silverlight 3 mais devraient fonctionner dans les versions supérieures.

III. Création du projet

Commençons par créer notre projet Silverlight.

image

Puis :

image

IV. Un item non sélectionnable par la souris

Commençons par créer une classe ListBoxSelectableItem, héritée de ListBoxItem, dans notre projet Silverlight.

image


Cette classe comportera la nouvelle propriété CanSelect, une Dependency Property (ajoutée via le snippet propdp). Cette propriété nous permettra de déterminer si l'item peut être sélectionné ou non par l'utilisateur.

 
Sélectionnez

public class ListBoxSelectableItem : ListBoxItem
{
    /// <summary>
    /// CanSelect
    /// </summary>
    public bool CanSelect
    {
        get { return (bool)GetValue(CanSelectProperty); }
        set { SetValue(CanSelectProperty, value); }
    }

    // Using a DependencyProperty as the backing store for CanSelect.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CanSelectProperty =
        DependencyProperty.Register("CanSelect", typeof(bool), typeof(ListBoxSelectableItem), new PropertyMetadata(true));
}

Dans le ListBoxSelectableItem, afin d'empêcher la souris de sélectionner l'item, surchargeons la méthode de gestion de pression du bouton gauche de la souris

 
Sélectionnez

/// <summary>
/// Surcharge de l'evenement de pression du bouton gauche de la souris
/// </summary>
/// <param name="e"></param>
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    e.Handled = !this.CanSelect;
    base.OnMouseLeftButtonDown(e);
}

Le e.Handled permet de prendre en compte ou non l'événement de la souris selon la valeur de la propriété CanSelect de l'item.

V. Test de notre nouvel item

Afin de vérifier que la souris est prise ou non en compte par nos items, mettons en place un test rapide dans notre MainPage.xaml.

Ajoutons tout d'abord un namespace « my » se référant à notre Assembly.


Puis passons à notre test :

 
Sélectionnez

<StackPanel Orientation="Vertical" Width="200">        
    
    <!-- Affichage de l'index selectionne de la ListBox -->
    <TextBlock FontWeight="Bold" Text="{Binding ElementName=ListBox, Path=SelectedIndex}"></TextBlock>

    <!-- la ListBox -->
    <ListBox x:Name="ListBox">
        <!-- Un item de type controle -->
        <TextBlock Text="Hello"></TextBlock>
        <!-- Un item selectionnable -->
        <my:ListBoxSelectableItem CanSelect="True" Content="Selectionnable"></my:ListBoxSelectableItem>
        <!-- Un item non selectionnable -->
        <my:ListBoxSelectableItem CanSelect="False" Content="Non selectionnable avec la souris"></my:ListBoxSelectableItem>
        <!-- Un item Classique -->
        <ListBoxItem Content="Bye"></ListBoxItem>
    </ListBox>
    
</StackPanel>


La ListBox utilisée est une ListBox classique. Plusieurs types d'items se côtoient à l'intérieur : des contrôles, des ListBoxItem et notre ListBoxSelectableItem.

Descendant de ListBoxItem, notre ListBoxSelectableItem est considérée comme un ListBoxItem classique par la ListBox. Ce n'est pas tout à fait le cas pour le contrôle TextBlock, comme nous le verrons tout à l'heure.


Pour l'instant, testons :

image

nous pouvons sélectionner l'item 1 « Selectionnable », mais pas l'item 2 « Non Selectionnable avec la souris »

Comment se comporte l'application avec le clavier ?

image


Pas tout à fait comme nous le souhaiterions, puisque l'utilisateur est capable de sélectionner l'élément 2 alors qu'il n'est pas sélectionnable normalement. Le clavier n'étant pas encore géré la situation est tout à fait normale.

VI. Une Listbox qui empêche la sélection par clavier

Lorsque l'utilisateur frappe une touche du clavier sur le ListBoxSelectableItem, il est déjà trop tard, l'item est déjà sélectionné. Il faut intercepter l'événement en amont afin qu'il ne soit pas diffusé plus bas. En surchargeant le contrôle ListBox nous pourrons récupérer cet événement :

Commençons par ajouter une nouvelle classe ListBoxSelectable héritée de ListBox.

image


Nous prenons le parti dans notre ListBoxSelectable de remplacer le ListBoxItem de base par notre propre ListBoxSelectableItem pour faciliter notre développement. Ainsi la propriété CanSelect sera nécessairement présente dans tous les containers de chaque item.

Pour ce faire, il est nécessaire d'ajouter deux méthodes à notre classe, qui permettent d'insérer un container autour de l'item si besoin est.

 
Sélectionnez

public class ListBoxSelectable : ListBox
{
    /// <summary>
    /// L'item est'il déjà un ListBoxSelectorItem ?
    /// </summary>
    /// <param name="item"></param>
    /// <returns></returns>
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is ListBoxSelectableItem;
    }

    /// <summary>
    /// Obtenir l'item
    /// </summary>
    /// <returns></returns>
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ListBoxSelectableItem();
    }
}

La méthode IsItemItsOwnContainerOverride permet de déterminer si l'item est ou non un container de type ListBoxSelectableItem tandis que GetContainerForItemOverride renvoie une nouvelle instance de container.

Ainsi, comme le contrôle TextBlock « Hello » de notre exemple n'est pas un ListBoxSelectableItem, le ListBox doit donc lui rajouter automatiquement un container de type ListBoxSelectableItem.


Rajoutons ensuite l'événement de gestion de frappe clavier vers le bas. Cet événement ne gère que les touches de déplacement HAUT et BAS comme la ListBox originale.

 
Sélectionnez

/// <summary>
/// Frapper une touche
/// </summary>
/// <param name="e"></param>
protected override void OnKeyDown(KeyEventArgs e)
{
    switch(e.Key)
    {
        case Key.Up :
        case Key.Down :
            e.Handled = true;
            break;
    }

    base.OnKeyDown(e);
}

Comme pour la souris, fixons le e.Handled à True pour signifier que l'on prend en charge la gestion de la touche HAUT et BAS, et qu'elle ne doit pas se diffuser vers les contrôles enfants. Les autres touches sont gérées normalement par la ListBox.

Fonctionnellement, lorsque nous appuyons sur la touche BAS, l'item au dessous doit être sélectionné et pour la touche HAUT, l'item au dessus doit être sélectionné.

Or, nous souhaitons que certains items ne soient plus sélectionnables. Nous devons donc écrire une méthode chargée de récupérer le premier item sélectionnable dans le sens de la touche.

 
Sélectionnez

/// <summary>
/// Trouver un item selectionnable vers le bas (isNext = true) ou vers le haut (isNext = false)
/// </summary>
/// <param name="isNext"></param>
/// <returns></returns>
private object FindSelectableItem(bool isNext)
{
    int index = this.SelectedIndex;
    int maxIndex = this.Items.Count;

    if (isNext == true)
    {
        for (int i = index + 1; i < maxIndex; i ++)
        {
            ListBoxSelectorItem item = this.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxSelectorItem;

            if (item.CanSelect == true)
            {
                return this.Items[i];
            }
        }
    }
    else            
    {
        for (int i = index - 1; i > -1; i--)
        {
            ListBoxSelectorItem item = this.ItemContainerGenerator.ContainerFromIndex(i) as ListBoxSelectorItem;

            if (item.CanSelect == true)
            {
                return this.Items[i];
            }
        }
    }

    return null;
}

Notez la méthode this.ItemContainerGenerator.ContainerFromIndex de la ListBox qui permet d'obtenir le container ListBoxSelectableItem par son index.


Ajoutons la méthode FindSelectableItem à notre gestion des touches :

 
Sélectionnez

/// <summary>
/// Frapper une touche
/// </summary>
/// <param name="e"></param>

protected override void OnKeyDown(KeyEventArgs e)
{
    switch(e.Key)
    {
        case Key.Up :
        case Key.Down :

        bool isNext = e.Key == Key.Down ? true : false;
        
        object item = this.FindSelectableItem(isNext);

        if (item != null)
        {
            this.SelectedItem = item;
        }

        e.Handled = true;
        break;
    }

    base.OnKeyDown(e);
}

VI. Test final

Remplaçons la ListBox classique de notre test original par notre ListBoxSelectable et retirons le dernier item ListBoxItem qui n'est plus d'actualité désormais.

 
Sélectionnez

<!-- la ListBox -->
<my:ListBoxSelectable x:Name="ListBox">
    <!-- Un item de type controle -->
    <TextBlock Text="Hello"></TextBlock>
    <!-- Un item selectionnable -->
    <my:ListBoxSelectableItem CanSelect="True" Content="Selectionnable"></my:ListBoxSelectableItem>
    <!-- Un item non selectionnable -->
    <my:ListBoxSelectableItem CanSelect="False" Content="Non selectionnable avec la souris et le clavier"></my:ListBoxSelectableItem>
    <!-- Un item de type controle -->
    <TextBlock Text="Bye"></TextBlock>
</my:ListBoxSelectable>


Lançons et sélectionnons l'index 1.

image

Puis appuyons sur la touche du bas et l'item passe à un index 3 (en sautant l'index 2 non sélectionnable).


Tout fonctionne parfaitement cette fois-ci !

VIII. Utilisation possible

Quelle peut être l'utilité de ce genre d'item ? Si nous considérons qu'une ListBox peut servir de menu, il peut être utile de mettre en place des séparateurs non sélectionnables entre ces menus, par exemple.

IX. Conclusion

En conclusion, vous avez pu découvrir quelques-uns des mécanismes internes de la ListBox. Ces mécanismes nous ont permis de rajouter une fonctionnalité simple mais puissante.

Vous pouvez télécharger le projet ici.

X. Amélioration possible

Nous aurions aussi pu mettre en place une Attached Property plus souple que l'héritage de la ListBox.

Cela fera peut-être l'objet d'un article prochainement.

XI. Remerciements

Je voudrais remercier Benjamin Roux, Philippe Vialatte, jacques_jean, Benoit Gemin, Nathalie Burel et Alessandra Sada pour leur relecture technique et leur apport à l'article.