ItemsControl, ListBox, ComboBox, TreeView

De Banane Atomic
Aller à la navigationAller à la recherche

ItemsControl

Composant qui représente une collection d'éléments. Ces éléments peuvent être hétérogène.
ListBox, ComboBox et TreeView héritent d'ItemsControl.

Xaml.svg
<ItemsControl Margin="10" ItemsSource="{Binding Path=MyTodoList}">
    <!-- définit un DataTemplate pour le rendu visuel des éléments de la collection -->
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Ellipse Fill="Silver"/>
                <TextBlock Text="{Binding Path=Priority}"
                           HorizontalAlignment="Center"/>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

    <!-- L'ItemsControl n'a pas de visuel par défaut. La propriété Template peut
    être utilisée pour spécifier un ControlTemplate qui définira l'apparence de
    l'ItemsControl. -->
    <ItemsControl.Template>
        <ControlTemplate TargetType="{x:Type ItemsControl}">
            <Border BorderBrush="Aqua" BorderThickness="1" CornerRadius="15">
                <ItemsPresenter/>
            </Border>
        </ControlTemplate>
    </ItemsControl.Template>

    <!-- Par défaut, le conteneur des éléments de la collection est un
    StackPanel. Il est possible de spécifier son propre contenant via la
    propriété ItemsPanel. -->
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <!-- La propriété ItemContainerStyle permet de définir un style pour les
    éléments qui contiennent les données. -->
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Control.Width" Value="100"/>
            <Setter Property="Control.Margin" Value="5"/>
            <!-- Par défaut à Left -->
            <Setter Property="Control.HorizontalContentAlignment"
                    Value="Stretch"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

Accès au DataContext de l'ItemsControl depuis un DataTemplate

Xaml.svg
<ItemsControl ItemsSource="{Binding Path=Property00}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}{0} - {1}">
                        <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ItemsControl}"
                                 Path="DataContext.Property01" />
                        <Binding Path="Property1" />
                    </MultiBinding>
                </TextBlock.Text>

Grid et ItemsControl

Xaml.svg
<!-- Permet aux grilles de partager leurs propriétés de taille -->
<ItemsControl ItemsSource="{Binding Path=MyTodoList}" Grid.IsSharedSizeScope="True">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <!-- Définit le groupe au sein duquel les propriétés de
                    taille sont partagés -->
                    <ColumnDefinition SharedSizeGroup="GridGroup1" />
                    <ColumnDefinition SharedSizeGroup="GridGroup1"/>
                </Grid.ColumnDefinitions>
                            
                <Label Grid.Column="0">A</Label>
                <Label Grid.Column="1">B</Label>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Template de visualisation et d'édition

Xaml.svg
<ListBox ItemsSource="...">
    <ListBox.Resources>
        <views:ControlFactoryConverter x:Key="ControlFactoryConverter"/>
    </ListBox.Resources>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <!-- Template de visualisation -->
            <ContentControl x:Name="Valeur" Content="{Binding}">
                <ContentControl.ContentTemplate>
                    <DataTemplate>
                        <Label Content="{Binding Path=Value}" />
                    </DataTemplate>
                </ContentControl.ContentTemplate>
            </ContentControl>
                        
            <DataTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Valeur" Property="ContentTemplate"
                            Value="{Binding Path=., Converter={StaticResource ResourceKey=ControlFactoryConverter}}">
                    </Setter>
                </Trigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

List et Binding

Lorsque l'on bind un ItemControl sur une propriété de type List, l'affichage n'est pas à jour avec l'élément bindé malgré l'appel de la méthode OnPropertyChanged.
Solutions :

  • Forcer le rafraichissement des éléments visuel : itemsControl.Items.Refresh()
  • Recréer une nouvelle List à chaque get sur la propriété bindée (new List ou yield return)
  • Utiliser une ObservableCollection

Filtre

Dans le code-behind

Xaml.svg
<!-- TextBox qui va permettre de choisir le filtre a appliquer -->
<TextBox TextChanged="TextBox_TextChanged"/>

<!-- Liste contenant des éléments à filtrer -->
<ItemsControl Name="list" ItemsSource="{Binding Path=Items}"/>
Csharp.svg
// dès que la valeur du filtre change il faut mettre à jour le visuel de la liste.
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    // on récupère donc la CollectionView de l'ItemControl
    var collectionView = CollectionViewSource.GetDefaultView(list.ItemsSource);
    // et on lui applique le filtre
    collectionView.Filter = item => 
    {
        var contact = item as Contact;
        var tb = sender as TextBox;
        return contact.Lastname.ToLower().StartsWith(tb.Text.ToLower());
    }
}

Dans le XAML

Xaml.svg
<MainControl xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">

<ComboBox>
    <!-- La CollectionViewSource doit être définie dans une ressource pour avoir accès au DataContext -->
    <ComboBox.Resources>
        <CollectionViewSource x:Key="viewSource"
                              Source="{Binding Path=Items}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Nom" /> <!-- tri sur la propriété Nom -->
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </ComboBox.Resources>

    <ComboBox.ItemsSource>
        <Binding Source="{StaticResource ResourceKey=viewSource}"/>
    </ComboBox.ItemsSource>
</ComboBox>

ScrollBar

Xaml.svg
<ScrollViewer ScrollViewer.VerticalScrollBarVisibility="Auto">
    <ItemsControl ItemsSource="..." />
</ScrollViewer>

Empêcher les Items de déborder sur la droite

Xaml.svg
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" />

RenderTransform et ZIndex

Pour éviter le chevauchement avec les autres éléments, il faut définir le ZIndex de l'élément zoomé.
Le changement de ZIndex ne peut se faire que dans un fils direct de Panel.
Les éléments de l'ItemsControl sont encapsulés dans des ContentPresenter, il faut donc modifier le ZIndex du ContentPresenter et non celui de l'élément (ici la TextBox).

Xaml.svg
<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBlock RenderTransformOrigin="0.5 0.5"
                       Text="{Binding}">
                <TextBlock.Style>
                    <Style TargetType="{x:Type TextBlock}">
                        <Style.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="AliceBlue" />
                                <Setter Property="RenderTransform">
                                    <Setter.Value>
                                        <ScaleTransform ScaleX="2"
                                                        ScaleY="2" />
                                    </Setter.Value>
                                </Setter>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </DataTemplate>
    </ItemsControl.ItemTemplate>

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="{x:Type ContentPresenter}">
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Panel.ZIndex" Value="99" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

ItemTemplateSelector

Permet de choisir des templates différent pour chaque item.

Xaml.svg
<UserControl.Resources>
    <namespace:MyTemplateSelector x:Key="myTemplateSelector "/>
<UserControl.Resources>

<ItemsControl ItemTemplateSelector="{StaticResource ResourceKey=myTemplateSelector}">
    <ItemsControl.Resources>
        <DataTemplate x:Key="Item1DataTemplate">
            
        </DataTemplate>
        <DataTemplate x:Key="Item2DataTemplate">
            
        </DataTemplate>
    </ItemsControl.Resources>
</ItemsControl>
Csharp.svg
public class MyTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement element = container as FrameworkElement;

        if (element != null && item != null)
        {
            // si l'item est du type Item1 on lui applique le DataTemplate Item1DataTemplate
            if (item is Item1)
            {
                return element.FindResource("Item1DataTemplate") as DataTemplate;
            }
            else if (item is Item2)
            {
                return element.FindResource("Item2DataTemplate") as DataTemplate;
            }
        }

        return null;
    }
}

Items qui prennent toute la largeur

Xaml.svg
<ListBox HorizontalContentAlignment="Stretch" />

<!-- autre solution -->
<ListBox.ItemContainerStyle> 
    <Style TargetType="ListBoxItem"> 
        <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter> 
    </Style> 
</ListBox.ItemContainerStyle>

ListBox

Possède la propriété SelectedItem par rapport à ItemsControl.

Redéfinir les couleurs de séléction

Xaml.svg
<ListBox ItemsSource="...">
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Style.Triggers>
                <Trigger Property="IsSelected" Value="True" >
                    <Setter Property="Background" Value="Transparent"/>
                    <Setter Property="Foreground" Value="White" />
                </Trigger>
            </Style.Triggers>
            <Style.Resources>
                <!-- Background of selected item when focussed -->
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Black" />
                <!-- Background of selected item when not focussed -->
                <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="LightGreen" />

                <!-- Arrondis la sélection -->
                <Style TargetType="Border">
                    <Setter Property="CornerRadius" Value="10"/>
                </Style>
            </Style.Resources>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

Redéfinir le visuel de sélection

Dans cet exemple, la sélection est représenté par une bordure noire de taille 3.

Xaml.svg
<ListBox>
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type Type=ListBoxItem}">
            <Setter Property="ListBoxItem.FocusVisualStyle" Value="{x:Null}" />
            <Setter Property="ListBoxItem.Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border Name="Border" BorderBrush="Black"
                                SnapsToDevicePixels="true">
                            <ContentPresenter />
                        </Border>

                        <ControlTemplate.Triggers>
                            <Trigger Property="IsSelected" Value="true">
                                <Setter TargetName="Border"
                                        Property="BorderThickness" Value="3"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

ListView vs ListBox

ListView est une spécialisation de ListBox.

  • Elle permet 4 vues differentes: iconview, small icon-view, list-view, details-view.
  • Un affichage en plusieurs colonnes est possible.
Xaml.svg
<UserControl.Resources>
    <Style x:Key="GroupStyle"
           TargetType="TextBlock">
        <d:Style.DataContext>
            <x:Type Type="local:ItemViewModel" />
        </d:Style.DataContext>
        <!-- ... -->
    </Style>

    <DataTemplate x:Key="ItemCellTemplate"
                  DataType="local:ItemViewModel">
        <TextBlock Text="{Binding GroupName}"
                   Style="{StaticResource GroupStyle}" />
    </DataTemplate>

    <!--  -->
    <Style TargetType="ListViewItem">
        <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    </Style>
</UserControl.Resources>

<ListView ItemsSource="{Binding Data}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name"
                            Width="150"
                            DisplayMemberBinding="{Binding Name}" />
            <GridViewColumn Header="Group"
                            Width="150"
                            CellTemplate="{StaticResource ItemCellTemplate}"> <!-- to apply a style on the cell, redefine the CellTemplate-->
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

ScrollIntoView

Permet de forcer le défilement jusqu'à un élément de la liste. Utile quand on modifie par le code l'élément sélectionné.

Csharp.svg
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var listBox = (ListBox)sender;
    listBox.ScrollIntoView(listBox.SelectedItem);
}

Auto scroll vers la fin lors de l’ajout d'un élément

CollectionChanged

Csharp.svg
// l'ItemsSource de myListBox doit être bindé à une ObservableCollection
var collection = myListBox.Items.SourceCollection as INotifyCollectionChanged;
collection.CollectionChanged += ListBox_CollectionChanged;

private void ListBox_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Add)
    {
        var border = VisualTreeHelper.GetChild(lb, 0) as Decorator;
        var scrollViewer = border.Child as ScrollViewer;
        scrollViewer.ScrollToEnd();

ScrollChanged

Xaml.svg
<ListBox ScrollViewer.ScrollChanged="ListBox_ScrollChanged"/>
Csharp.svg
private void ListBox_ScrollChanged(object sender, RoutedEventArgs e)
{
    if (((ScrollChangedEventArgs)e).ExtentHeightChange > 0.0)
        ((ScrollViewer)e.OriginalSource).ScrollToEnd();

ItemTemplate et ListBoxItem

Xaml.svg
<ListBox xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding Path=Content, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
        </DataTemplate>
    </ListBox.ItemTemplate>

    <!-- ItemTemplate ne s'applique pas aux ListBoxItem -->
    <ListBoxItem>Menu #1</ListBoxItem>
    <ListBoxItem>Menu #2</ListBoxItem>

    <!-- La solution: utiliser la propriété Items -->
    <ListBox.Items>
        <sys:String>Menu #1</sys:String>
        <sys:String>Menu #2</sys:String>
    </ListBox.Items>
</ListBox>

ComboBox et Binding

Liaison des valeurs d'un dictionnaire, accessible par la propriété Dictionary, avec la liste des éléments de la ComboBox.
Liaison de l'item sélectionné avec la propriété Valeur du DataContext courant.

Xaml.svg
<ComboBox ItemsSource="{Binding Path=Dictionary.Values}" SelectedItem="{Binding Path=Valeur}" />

Liaison d'une liste, accessible par la propriété List, avec la liste des éléments de la ComboBox.
La propriété DisplayMemberPath permet de spécifier quelle propriété sera utilisée pour afficher les item.
La propriété SelectedValuePath permet de spécifier quelle propriété sera utilisée pour comparer la l'item sélectionné avec la liste des items de la ComboBox.

Xaml.svg
<ComboBox ItemsSource="{Binding Path=List}"
          DisplayMemberPath="Nom"
          SelectedValuePath="Nom"
          SelectedValue="{Binding Path=Valeur}" />

ComboBox avec une liste d'éléments codée en dur:

Xaml.svg
<ComboBox SelectedIndex="0">
    <ComboBox.Items>
        <ComboBoxItem Content="string"/>
        <ComboBoxItem Content="double"/>
    </ComboBox.Items>
</ComboBox>

<ComboBox SelectedIndex="0" ItemsSource="12345"/>

ComboBox et Enum

Xaml.svg
<Window xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:MyNamespace">
    <Window.Resources>
        <ObjectDataProvider x:Key="MyEnumODP" MethodName="GetValues"
                            ObjectType="{x:Type System:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="local:MyEnum"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>

    <ComboBox ItemsSource="{Binding Source={StaticResource MyEnumODP}}"
              SelectedItem="{Binding Path=MySelectedItem, Mode=OneWayToSource}"
              SelectedIndex="1" />
    <!-- SelectedIndex: pour la sélection du 2ème élément.
         Si SelectedItem est présent il faut le passer en Mode=OneWayToSource -->

TreeView

Permet un affichage hiérarchique des données.

TreeView statique

Xaml.svg
<TreeView>
    <TreeViewItem Header="Groupe 1">
        <TreeViewItem Header="Element 11"/>
        <TreeViewItem Header="Element 12"/>
    </TreeViewItem>
    <TreeViewItem Header="Groupe 2">
        <TreeViewItem Header="Element 21"/>
        <TreeViewItem Header="Element 22"/>
    </TreeViewItem>
</TreeView>

Groupement depuis une List

Même exemple que précédemment, mais cette fois le TreeView est binder sur une List.

Csharp.svg
public enum Groupe { Groupe1, Groupe2 }
public class Element
{
    public string Description { get; set; }
    public Groupe Groupe { get; set; }
}

// dans le DataContext
public ObservableCollection<Element> Elements { get; set; }

Elements = new ObservableCollection<Element>();
Elements.Add(new Element() { Description = "Element 11", Groupe = Groupe.Groupe1 });
Elements.Add(new Element() { Description = "Element 12", Groupe = Groupe.Groupe1 });
Elements.Add(new Element() { Description = "Element 21", Groupe = Groupe.Groupe2 });
Elements.Add(new Element() { Description = "Element 22", Groupe = Groupe.Groupe2 });
Xaml.svg
<...Resources>
    <!-- Définition d'une CollectionViewSource qui va permettre de faire des groupes à partir de la propriété Elements du DataContext -->
    <CollectionViewSource x:Key="cvs" Source="{Binding Path=Elements}">
        <CollectionViewSource.GroupDescriptions>
            <!-- Les groupes se font suivant la propriété Groupe de l'objet Element -->
            <PropertyGroupDescription PropertyName="Groupe"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>
</...Resources>

<!-- On remplace la CollectionViewSource par défaut du TreeView par notre CVS
     qui est déjà bindée à la propriété Elements du DataContext -->
<!-- Groups est une propriété de ICollectionView et contient une liste de CollectionViewGroupInternal -->
<TreeView ItemsSource="{Binding Source={StaticResource ResourceKey=cvs}, Path=Groups}">
    <TreeView.ItemTemplate>
        <!-- Binding sur la propriété Items de CollectionViewGroupInternal elle contient la liste des éléments du groupe -->
        <HierarchicalDataTemplate ItemsSource="{Binding Path=Items}">
            <HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate>
                    <!-- Binding sur la propriété Description de Element -->
                    <TextBlock Text="{Binding Path=Description}"/>
                </DataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>

            <!-- Binding sur la propriété Name de CollectionViewGroupInternal elle contient le nom du groupe -->
            <TextBlock Text="{Binding Path=Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Performance

Optimizing WPF Application Performance

Virtualisation

Seul les éléments à afficher sont créés.

Xaml.svg
<!-- ItemsControl : utilisation d'un VirtualizingStackPanel -->
<ItemsControl ItemsSource="{Binding Path=Items}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

<!-- ListBox : virtualisé par défaut, on modifie son mode -->
<ListBox ItemsSource="{Binding Path=Items}" VirtualizingStackPanel.VirtualizationMode="Recycling" />

Modes

  • Standard : mode par défaut, les conteneurs des éléments sont libérés dès qu'ils ne sont plus affichés.
  • Recycling : les conteneurs des éléments qui ne sont plus affichés sont réutilisés par les éléments à afficher.

A eviter

  • les StackPanel horizontaux

Affichage

Writing More Efficient ItemsControls
L'ItemsControl va appliquer l'ItemTemplate à chacun de ses éléments. Simplifier l'ItemTemplate permet donc d'améliorer les performances :

  • Réduire le nombre de Control : utiliser un Control avec MultiBinding et StringFormat plutôt que plusieurs Control
  • Freeze les freezable objects, ils seront considérés comme constants (appel de la méthode Freeze ou utilisation de l'option the PresentationOptions:Freeze)
  • Réduire le nombre de Bindings : utiliser un Presenter (Vue-Modèle?), sur-couche de la classe métier mise en forme pour faciliter les Bindings.
  • Créer son propre composant du type FrameworkElement pour l'ItemTemplate.
  • Créer son propre composant du type FrameworkElement pour l'ItemsControl afin que lors de la modification d'un Item on ne redessine pas tous les Items mais seulement celui qui a changé.

Scrolling

Xaml.svg
<!-- Ne rafraichit pas la ListBox pendant le déplacement de la ScrollBar -->
<ListBox ScrollViewer.IsDeferredScrollingEnabled="True" />