[Universal App 8.1] ListView : Trucs et astuces

Le contrôle ListView est très populaire dans les applications universelles.
Et pour cause, Il est facile à mettre en place, possède un Header et un Footer, permet d’afficher des listes d’items groupés, tout en utilisant des TemplateSelectors.
Malgré tout, il souffre de problèmes assez déroutants pour les novices.
Dans cet article, je vous propose de décrire les problèmes les plus courants et de les corriger quand cela s’avère possible.

Les événements de manipulation

Vous cherchez à contrôler un autre élément de votre UI à partir des events de manipulation de votre ListView (ManipulationStart/Delta/Stop) ?
Je vous arrête tout de suite, ce n’est plus possible en Universal App.
En effet, afin d’obtenir des performances proche du système, Le Listview interdit tout bonnement l’accès à ces événements pourtant bien pratique.
Pour être plus précis, cela concerne tous les contrôles utilisant en interne un ScrollViewer (ListView, GridView, FlipView,…)
Il ne reste que les événements PointerPressed et PointerRelease qui restent actifs. Aie !

Le Scrolling dans la liste

Heureusement, il est quand même possible d’accéder à la position du Scrolling MAIS pas nativement.
En effet, le ListView contient un ScrollViewer que l’on va devoir rechercher car il n’est pas public.
Pour ce faire, dans le template du ListView (que vous trouverez à cette adresse) je récupère le nom du ScrollViewer chargé d’effectuer le scrolling de la ListView. Ne cherchez pas, il s’appelle « ScrollViewer » 😛

Ensuite, j’ajoute une méthode qui permet de rechercher un enfant dans l’arborescence visuelle du ListView (en gros le template) pour retrouver le Scrollviewer par son nom.

        private FrameworkElement FindVisualChild(DependencyObject element, string nameOfChildToFind)
        {
            for( int x = 0; x < VisualTreeHelper.GetChildrenCount(element); x++ )
            {
                var child = VisualTreeHelper.GetChild(element, x);
            
                if( child is FrameworkElement)
                {
                    string name = (string)child.GetValue(FrameworkElement.NameProperty);
                
                    if( name == nameOfChildToFind)
                    {
                        return (FrameworkElement)child;
                    }
                    else if( VisualTreeHelper.GetChildrenCount(child) > 0 )
                    {
                        return this.FindVisualChild(child, nameOfChildToFind);
                    }
                }
            }

            return null;
        }

et je n’ai plus qu’à m’inscrire aux événements de changement de vue :

ScrollViewer scrollViewer = (ScrollViewer)this.FindVisualChild(listview, "ScrollViewer");
        
if( scrollViewer != null)
{
   scrollViewer.ViewChanging += scrollViewer_ViewChanging;
}

puis dans l’événement, je suis désormais capable de récupérer la position du scrolling (ici verticale) :

void scrollViewer_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
{
     this.TextBlockScrollingValue.Text = e.NextView.VerticalOffset.ToString();
}

Pour affecter la position du scroll (avec animation ou non), j’utilise la méthode ChangeView du ScrollViewer :

// Je scrolle verticalement à la position 0 sans animation
bool isAnimated = false;
this.ScrollViewer.ChangeView(null, 0, null, !isAnimated);

Aligner à droite dans un item

Comme tous les membres de sa famille (ListBox, LongListSelector), ListView souffre d’un mal ancien et mystérieux.
Les items qui la composent sont alignés à gauche et ne prennent pas toute la place qui pourraient leur être alloués !
Il n’est pas possible, par exemple, d’aligner un textblock afin qu’il touche le bord droit de la ListView sans avoir recours à une astuce.
On va tout simplement affecter la propriété « HorizontalContentAlignment » à « Stretch » du ListViewItem chargé de rendre le contenu de l’item afin qu’il s’étende à la taille du ListView qui le contient.
Tout se passe en définissant un style dans ItemContainerStyle :

<ListView Grid.Row="1" ItemsSource="{Binding Names}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextBlock HorizontalAlignment="Right" FontSize="20" Text="{Binding}"></TextBlock>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Ca gondole !

Même si l’on a recourt à l’astuce ci dessus qui nous permet d’avoir un ListViewItem de même taille que le ListView ce n’est pas toujours suffisant.
En effet, on retrouve souvent un comportement étrange lorsque l’on scrolle rapidement une liste dont les items sont complexes.
Je ne suis pas sûr de la cause exacte de ce problème mais il semblerait que le calcul interne du Layout du ListViewItem ne s’effectue pas correctement lorsqu’un de ses éléments change de taille en cours de route (une image qui s’affiche après chargement par exemple).
On observe alors une tendance à se décaler vers la droite puis à revenir vers la gauche. ça gondole !
Inutile de vous dire que ce genre de phénomène ne fait pas du tout pro dans une application.

Sur internet vous trouverez des tricks qui incitent à positionner un Binding sur la taille de l’itemTemplate afin que celle-ci soit fixée à partir de la taille de la ListView :

<ListView x:Name="ListViewNames" ItemsSource="{Binding Names}">
    <ListView.ItemTemplate>
        <DataTemplate>
          <Grid With="{Binding Path=ActualWidth,ElementName=ListViewNames}">
            <TextBlock HorizontalAlignment="Right" FontSize="20" Text="{Binding}"></TextBlock>
          </Grid>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

JE VOUS DECONSEILLE FORTEMENT CETTE PRATIQUE !

Dèjà, côté performance c’est un Binding de plus sur chaque item visible affiché.
Mais le plus dangereux c’est que cela ne marche pas toujours !
Et pour cause, ActualWidth n’est pas une propriété qui notifie ses changements.
Si la ListView change ou recalcule sa taille (lors d’un passage de Collapsed à Visible par exemple), ses items seront parfois avertis trop tard et certains items (les premiers en général) n’apparaitront pas.

Préférez plutôt cette technique qui consiste à fixer une fois la taille du panel interne du ListView d’après sa taille:

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <ItemsStackPanel
            Loaded="ItemsStackPanel_Loaded">
        </ItemsStackPanel>
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

et côté C# :

private void ItemsStackPanel_Loaded(object sender, RoutedEventArgs e)
{
    ItemsStackPanel panel = sender as ItemsStackPanel;
    panel.Width = this.ListViewGondole.ActualWidth;
}

Plus de transitions

Les contrôles WinRT ont pris la sale manie d’ajouter des transitions partout dans leurs comportements.
Dans une ListView on peut les voir lorsque l’on ajoute ou retire un élément de la liste.
Parfois, cela donne des effets intéressant, parfois, c’est loin d’être idéal.
Pour supprimer les transitions d’une ListView on fera donc comme suit :

<ListView>
  <ListView.ItemContainerTransitions>
    <TransitionCollection>
    </TransitionCollection>
  </ListView.ItemContainerTransitions>
</ListView>

Drag and drop et Reorder Items

Bien qu’il fonctionne sous Windows, le drag and drop ne marche pas côté Windows Phone.
En revanche ReorderItem fonctionne très bien sur les deux plateformes.

En gros il suffit de modifier les propriétés suivantes du ListView:

Pour Windows:

<ListView CanReorderItems="True" CanDragItems="True" AllowDrop="True"/>

Pour Windows Phone:

<ListView ReorderMode = "Enabled" />

Je vous renvoie sur la page de Toss net qui explique très bien la mise en place, vidéo à l’appui !
http://www.peug.net/2014/10/29/reorder-items-universal-app/

Les Blocs noirs

Parfois, lorsque la Listview peine à afficher ses éléments, des blocs noirs apparaissent lors des scrollings.
Si vous voulez régler le problème rapidement, il suffit de mettre la propriété ShowsScrollingPlaceholders à False.
Ainsi, les blocs deviendront transparents et cela sera moins choquant pour l’utilisateur.

<ListView ShowsScrollingPlaceholders="False" />

Pour faire les choses bien vous pouvez gérer les PlaceHolders à la main.
Malheureusement, cela sera aux dépens du Binding des itemTemplate du Listview.
Cela se met en place relativement simplement en s’abonnant à ContainerContentChanging du ListView.
Il faudra ensuite afficher les éléments par couches successives comme décrit dans la documentation MSDN

ListView inversée

Dans certain cas il peut être utile de pouvoir inverser le fonctionnement d’une Listview. Par exemple, dans le cadre de l’affichage d’un chat :

wp_ss_20150610_0001

La première chose à laquelle on pense face à ce problème est d’inverser le fonctionnement interne des items, c’est à dire faire des Insert(0,item) à la place de Add(item).
Si vous devez gérer des collections incrémentales, cela peut devenir un cauchemar car, rapidement, la ListView perdra les pédales en affichant n’importe quoi.

Le plus simple reste de jouer avec le GPU et d’inverser la Liste puis chacun de ses éléments:

<ListView RenderTransformOrigin="0.5,0.5">
  <ListView.RenderTransform>
    <ScaleTransform ScaleY="-1"/>
  </ListView.RenderTransform>

<ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
        <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
        <Setter Property="RenderTransform">
            <Setter.Value>
                <ScaleTransform ScaleY="-1"></ScaleTransform>
            </Setter.Value>
        </Setter>
    </Style>
</ListView.ItemContainerStyle>

</ListView>

La vue de la liste sera donc inversée mais pas le model. C’est finalement plutôt logique.

Pour conclure

Lorsque l’on regarde l’ensemble des astuces à mettre en place pour obtenir un ListView prêt à l’emploi, je me dis que ce contrôle mériterait d’être revu par les équipes de développement de Microsoft afin qu’il soit plus facilement intégrable par des novices. C’est dommage car, une fois maîtrisé, le contrôle fait bien son travail. Mieux ! Il n’a jamais été aussi rapide et customisable !

Je remercie Gwendal « Virgule » Marchand pour sa relecture attentive :)

leave your comment