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.
|
<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
|
<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
|
<!-- 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
|
<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
|
<!-- 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}"/>
|
|
// 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
|
<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
|
<ScrollViewer ScrollViewer.VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="..." />
</ScrollViewer>
|
|
<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).
|
<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.
|
<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>
|
|
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;
}
}
|
|
<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
|
<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.
|
<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
- avec une propriété View
- GridView permet un affichage en plusieurs colonnes
- default selection mode is Extended instead of Single for a ListBox
- Elle permet 4 vues differentes: iconview, small icon-view, list-view, details-view.
|
<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}"
HorizontalAlignment="Right" />
</DataTemplate>
<!-- to change the Horizontal Alignment of a Column in a ListView -->
<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é.
|
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
|
// 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
|
<ListBox ScrollViewer.ScrollChanged="ListBox_ScrollChanged"/>
|
|
private void ListBox_ScrollChanged(object sender, RoutedEventArgs e)
{
if (((ScrollChangedEventArgs)e).ExtentHeightChange > 0.0)
((ScrollViewer)e.OriginalSource).ScrollToEnd();
|
ItemTemplate et ListBoxItem
|
<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.
|
<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.
|
<ComboBox ItemsSource="{Binding Path=List}"
DisplayMemberPath="Nom"
SelectedValuePath="Nom"
SelectedValue="{Binding Path=Valeur}" />
|
ComboBox avec une liste d'éléments codée en dur:
|
<ComboBox SelectedIndex="0">
<ComboBox.Items>
<ComboBoxItem Content="string"/>
<ComboBoxItem Content="double"/>
</ComboBox.Items>
</ComboBox>
<ComboBox SelectedIndex="0" ItemsSource="12345"/>
|
|
<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
|
<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.
|
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 });
|
|
<...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.
|
<!-- 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
|
<!-- Ne rafraichit pas la ListBox pendant le déplacement de la ScrollBar -->
<ListBox ScrollViewer.IsDeferredScrollingEnabled="True" />
|