Introduction
Vu précédemment
Nous avons vu dans la première partie comment implémenter une liste de fichiers en WPF avec le pattern MVVM.
Dans la deuxième partie, nous avons vus comment optimiser l’interface utilisateur avec le VirtualizingStackPanel, le multi Thread, les animations d’attente WPF. Nous avons eu également un aperçu de la manipulation des images pour utiliser lorsqu’elles existent les informations EXIF ou encore une transformation classique de ces images.
Je vous propose de voir dans cette troisième partie :
- La mise en cache des images (améliorons l’interface utilisateur encore un petit peu)
- Le Drag and Drop avec une sélection multiple d’éléments
Optimisation avec mise en cache
Implémentation d’un système de cache
Le but est de garder les x dernières images déjà chargée afin d’éviter un rechargement permanent de ces images.
Pour cela nous mettons en place une liste partagée en appel static, ainsi que son verrou (nous sommes dans un environnement multi-thread) afin de garantir l’écriture correcte des données.
const int maxThumbnailInCache = 300; private static readonly List<ImageFileViewModel> _cacheThumbnail = new List<ImageFileViewModel>(); private static readonly object _lockCacheThumbnail = new object();
Puis nous implémentons la méthode static AddThumbnailInCache
private static void AddThumbnailInCache(ImageFileViewModel p) { if (p == null) return; if (_cacheThumbnail.Contains(p)) { lock (_lockCacheThumbnail) { _cacheThumbnail.Remove(p); } } lock (_lockCacheThumbnail) { _cacheThumbnail.Add(p); if (_cacheThumbnail.Count > maxThumbnailInCache) lock (_lockCacheThumbnail) { _cacheThumbnail.Remove(_cacheThumbnail[0]); } } }
La méthode teste si l’élément est présent. Si il est présent, l’élément est replacé à la fin de la liste.
Si le nombre d’éléments dépasse le maximum d’élément fixés dans le cache (maxThumbnailInCache), l’élément en haut (le plus ancien) est supprimé.
Cette méthode est appelée lorsque l’image est chargée en mémoire.
public ImageSource Thumbnail { get { if (_thumbnail == null) { LoadImage(); return null; } AddThumbnailInCache(this); return _thumbnail; } }
Nous modifions maintenant l’instruction de libération des éléments pour ne supprimer que ceux absent de la liste
internal bool CleanUp() { if (!_cacheThumbnail.Contains(this)) { lock(_lockCacheThumbnail) { lock (_lockListThumbnail) { _listThumbnail.Remove(this); _loading = false; _thumbnail = null; } } return false; } else return true; }
Puis dans la partie View, nous demandons à ne pas décharger l’élément si CleanUp n’a pas réussi (cas où l’on garde l’élément en cache).
namespace wpfListViewSample03.View { public partial class ImageFileListView : UserControl { public ImageFileListView() { InitializeComponent(); } private void ListViewImage_CleanUpVirtualizedItem (object sender, CleanUpVirtualizedItemEventArgs e) { if (e.Value is ImageFileViewModel) { e.Cancel = (e.Value as ImageFileViewModel).CleanUp(); } } } }
Exécution :
Et voila, 316 éléments (300 en cache + 16 affichés) sont gardés en cache, la vignette (thumbnail) ne sera pas ainsi constamment régénérée.
Les tests unitaires
Les tests unitaires associés sont assez simples. Il faudra penser à ajouter le vidage de la liste _CacheThumbnail.
[TestInitialize()] public void MyTestInitialize() { // Clear Thread processus ImageFileViewModel_Accessor._loaderThreadThumbnail.Abort(); // call the static constructor Type staticType = typeof(ImageFileViewModel); ConstructorInfo ci = staticType.TypeInitializer; object[] parameters = new object[0]; ci.Invoke(null, parameters); // and clear static attribute _ListThumbnails and _cacheThumbnails ImageFileViewModel_Accessor._listThumbnail.Clear(); ImageFileViewModel_Accessor._cacheThumbnail.Clear(); }
Le Drag And Drop dans une ListBox WPF avec M-V-VM
Drag and Drop, Quoi de plus ?
Le drag and drop de ListBox (ou ListView) a déjà été traité dans de nombreux articles de qualité dont je me suis inspiré pour réaliser cette prochaine version.
Voici la liste de ces articles
- http://geekswithblogs.net/hinshelm/archive/2009/08/14/wpf-drag-amp-drop-behaviour.aspx
- http://bea.stollnitz.com/blog/?p=53
- http://www.codeproject.com/KB/WPF/ListViewDragDropManager.aspx
- http://blogs.msdn.com/jaimer/archive/2007/07/12/drag-drop-in-wpf-explained-end-to-end.aspx
- http://geekswithblogs.net/sonam/archive/2009/03/02/listview-dragdrop-in-wpfmultiselect.aspx
Ce qui manquait, une version générale (chaque petit bout étant dans dans la plupart de ces articles) avec
- La gestion du déplacement d’un Item dans une ListBox ou ListView. La plupart font une copie des éléments mais ne gèrent pas les déplacements
- La multi-sélection, la copie ou le déplacements de plusieurs éléments sélectionnés.
- Le scroll automatique lorsque l’on déplace les éléments en dehors de la fenêtre
- Une séparation entre la gestion du drag and drop et la couche visuelle d’affichage des Adorners (Aspect graphique indiquant la position des éléments glissés)
J’ai donc réalisé un premier “DragAndDropManager” sans les fioritures graphiques puis un héritier qui va uniquement implémenter les aspect d’affichage (« Adorners »).
Organisation du projet Drag And Drop Manager.
Toute la gestion du drag and drop se retrouve dans un projet unique DragDropManager qui pourra être repris dans un projet indépendant pour vos ListView ou ListBox.
L’organisation du projet est la suivante
La partie Model n’est pas spécifique WPF.
Elle contient 2 classes
- DragDropManagerList Le gestionnaire de DragAndDrop permettant de gérer des ensembles, et les mécanismes de début, fin d’opérations de DragAndDrop.
- MouseUtilities inclue une fonction permettant de localiser le curseur de la souris au moment du Drag. Il faut savoir que la grosse difficulté lors du drag and drop est de pouvoir gérer la position du curseur. Microsoft en effet n’a pas prévu jusqu’à la version 3.5 de pouvoir suivre le mouvement de la souris lors de cette opération.
La partie ViewModel contient 2 classes principales
- DragDropManagerListBoxViewModel gère les fonctions de drag et drop de la souris avec les composants WPF ListBox. Elle dérive de la classe DragDropManagerList en utilisant ces fonctionnalités de base de DragDropManagerList. Cette classe peut être utilisée si vous n’avez pas besoin des Adorners
- DragDropManagerListBoxAdornerViewModel gère l’affichage des 2 Adorners
2 classes définissent les Adorners
- DropAdorner affiche la partie visuelle (vignette + libellé de la sélection) en transparence sous le curseur de la souris
- InsertionAdorner affiche une ligne noire indiquant l’emplacement destination des éléments à insérer dans la liste.
Les 2 classes Adorners ont été reprises des projets cités plus haut. Il est possible d’écrire ses propres Adorners pour personnaliser le type d’affichage
L’ensemble des classes permettant la gestion du DragAndDrop sont résumées ici
La gestion des éléments multiples dans le Drag en Drop
Le drag and drop d’éléments multiples va passer par une structure Dictionary _draggedItems.
La clé du dictionnaire va contenir la position de l’élément et sa valeur contiendra l’objet de type T Générique.
protected Dictionary<int, T> _draggedItems = new Dictionary<int, T>();
La classe DragDropManagerList implémente ensuite 3 méthodes statiques permettant de gérer le Drop des éléments sélectionnés (_draggedItems).
- DoDrop sera appelé au moment du Drop des éléments dans la liste. Il retourne les nouveaux éléments copiés sous forme d’un dictionnaire (SI les éléments sont déplacés retourne null). En fonction de la touche de clavier Ctrl utilisé, les éléments seront soit copiés soit déplacés.
- CopyDraggedItems gère la copie des éléments
- MoveDraggedItems gère le déplacement des éléments
protected static Dictionary<int, T> DoDrop(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems , int index, DragDropEffects effects, DragDropKeyStates keyStates) { T dropTarget = itemsSource[index]; if (draggedItems.ContainsValue(dropTarget)) return null; if ((DragDropEffects.Move & effects) == DragDropEffects.Move && (DragDropKeyStates.ControlKey & keyStates) != DragDropKeyStates.ControlKey) effects = DragDropEffects.Move; if ((DragDropEffects.Copy & effects) == DragDropEffects.Copy) effects = DragDropEffects.Copy; switch (effects) { case DragDropEffects.Move: { MoveDraggedItems(itemsSource, draggedItems, index); draggedItems.Clear(); return null; } case DragDropEffects.Copy: { Dictionary<int, T> newItems = CopyDraggedItems(itemsSource, draggedItems, index); draggedItems.Clear(); return newItems; } } return null; } protected static Dictionary<int, T> CopyDraggedItems(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems, int index) { Dictionary<int, T> newItems = new Dictionary<int, T>(); int i = 0; foreach (int key in draggedItems.Keys.OrderBy(ii => ii)) { T draggedItemClone = draggedItems[key].Clone() as T; if (draggedItemClone != null) { itemsSource.Insert(index + i, draggedItemClone); newItems.Add(index + i, draggedItemClone); i++; } } return newItems; } protected static void MoveDraggedItems(ObservableCollection<T> itemsSource, Dictionary<int, T> draggedItems, int index) { int dest = index; int origin = 0; Dictionary<int, T> completedDraggedItems = draggedItems; T ModelDest = itemsSource[dest]; foreach (int key in completedDraggedItems.Keys) { origin = itemsSource.IndexOf(completedDraggedItems[key]); if (itemsSource[dest] != completedDraggedItems[key]) { if (draggedItems.ContainsKey(key)) { ModelDest = draggedItems[key]; } if (key > index) dest++; itemsSource.Move(origin, dest); } } }
La classe implémente le mécanisme de gestion du déplacement visuel des éléments durant le Drag.
Les méthodes seront surchargées dans les classes héritées pour gérer les particularités visuelles (ListBox, Adorners,…).
protected void DoDragOperation() { InitializeDragOperation(); try { PerformDragOperation(); } finally { FinishDragOperation(); } }
La gestion du Drag and Drop en M-V-VM
L’initialisation du DragAndDropManager va se faire de la façon suivante dans la couche View.
public ImageFileListView() { InitializeComponent(); new DragDropManagerListBoxViewModel <ImageFileViewModel>(ListViewImage); }
Le constructeur initialise le composant visuel ListBox ListViewImage avec le type objet ImageFileViewModel.
Dans la classe DragDropManagerListBoxViewModel, les évènements suivants sont attrapés et redirigés pour la gestion du Drag and Drop.
public virtual ListBox ListBox { get {return _listBox;} set { if (_listBox != null) { _listBox.PreviewMouseMove -= listBoxPreviewMouseMove; _listBox.PreviewMouseLeftButtonDown -= listBoxPreviewMouseLeftButtonDown; _listBox.PreviewMouseLeftButtonUp -= listBoxPreviewMouseLeftButtonUp; _listBox.DragOver -= listBoxDragOver; _listBox.Drop -= listBoxDrop; } _listBox = value; _listBox.AllowDrop = true; if (_listBox != null) { if (!_listBox.AllowDrop) _listBox.AllowDrop = true; _listBox.PreviewMouseMove += listBoxPreviewMouseMove; _listBox.PreviewMouseLeftButtonDown += listBoxPreviewMouseLeftButtonDown; _listBox.PreviewMouseLeftButtonUp += listBoxPreviewMouseLeftButtonUp; _listBox.DragOver += listBoxDragOver; _listBox.Drop += listBoxDrop; } } }
- PreviewMouseLeftButtonDown : Bouton de la souris enfoncé : utilisé pour initialisé l’opération de glisser (drag) et déterminer les éléments à placer dans _draggedItems.
- PreviewMouseMove : déplacement de la souris : utilisé pour l’affichage des Adorners, l’affichage du curseur (selon appuie du bouton CTRL et la sélection de l’item de destination
- DragOver : Survol des éléments lorsque les items sont sélectionnés. Permet de rafraichir les Adorners et de gérer le déplacement de la barre de scrolling lorsque le curseur est proche des limites Haut et Bas.
- Drop : Action définitive du Drag and Drop, Copie ou déplacement des éléments
- PreviewMouseLeftButtonUp : Bouton de la souris relâché, vide les éléments sélectionnés (_draggedItems.Clear) .
Le déplacement automatique des éléments durant le Drag and Drop
protected virtual void listBoxDragOver(object sender, DragEventArgs e) { AutoScrollCursorBounds(); } private void AutoScrollCursorBounds() { Point ptMouse = DragDropManager.MouseUtilities.GetMousePosition(_listBox); Rect bounds = VisualTreeHelper.GetDescendantBounds(_listBox); if (ptMouse.Y > bounds.Height - 10 || ptMouse.Y < 10) _listBox.ScrollIntoView(GetCurrentListBoxItem()); }
Durant le déplacement des éléments sélectionnés, la méthode AutoScrollCursorBounds() est appelée. Cette méthode regarde la position de la Souris, la compare aux bords de la liste et réalise le scroll sur l’élément sélectionné si elle est proche d’un bord.
La gestion des Adorners (InsertionAdorner et DropAdorner)
Elle s’effectue avec la classe DragDropManagerListBoxAdornerViewModel.
InsertionAdorner et DropAdorner sont 2 classes trouvées sur les différents forums.
Josh Smith a écrit le DropAdorner et l’InsertionAdorner provient du site de bea Stollnitz.
La création de ces 2 descendants de Adorners intervient à 2 moment différents.
Le DropAdorner est créé dès le début de l’opération de Drag (PreviewMouseMove appelle DoDragOperation qui lance InitializeDragOperation()) pour afficher la vignette des éléments en déplacement.
protected override void InitializeDragOperation() { base.InitializeDragOperation(); if (ListBox.SelectedItem != null) { T draggeditem = ListBox.SelectedItem as T; if (draggeditem != null) { ListBoxItem lvi = GetVisualItem(draggeditem) as ListBoxItem; if (lvi != null) CreateAdornerLayer(lvi); } } }
Le InsertionAdorner est créé uniquement au survol d’un élément sélectionnable et détruit dès que le curseur change de ligne.
private void ListBoxDragEnter(object sender, DragEventArgs e) { if (dragAdorner != null && dragAdorner.Visibility != Visibility.Visible) { UpdateDragAdornerLocation(); dragAdorner.Visibility = Visibility.Visible; } object draggedItem = e.Data.GetData(format.Name); if (draggedItem != null) { CreateInsertionAdorner(); } } private void ListBoxPreviewDragLeave(object sender, DragEventArgs e) { object draggedItem = e.Data.GetData(format.Name); if (draggedItem != null) { RemoveInsertionAdorner(); } }
Lors de l’arrêt de la suppression du déplacement par relâchement de la souris, les Adorners sont supprimés.
protected override void FinishDragOperation() { RemoveDragAdorner(); RemoveInsertionAdorner(); base.FinishDragOperation(); }
Au cours du déplacement du pointeur de la souris, la position du DropAdorner est mise à jour.
Le nouveau positionnement est calculé et passé à l’objet Adorner (dragAdorners.SetOffsets(left, top);
private void UpdateDragAdornerLocation() { if (this.dragAdorner != null) { Point ptCursor = DragDropManager.MouseUtilities.GetMousePosition(ListBox); double left = ptCursor.X - ptMouseDown.X; double top = ptCursor.Y; dragAdorner.SetOffsets(left, top); } }
Le DragAndDrop avec Adorners s’utilise simplement, vous devez utiliser la classe DragDropManagerListBoxAdornerViewModel dans le constructeur de la façon suivante :
public ImageFileListView() { InitializeComponent(); new DragDropManagerListBoxAdornerViewModel <ImageFileViewModel>(ListViewImage); }
Le problème de sélection avec la multi-sélection et le Drag and Drop
Vous l’avez peut être déjà remarqué, la multi-sélection pose un problème avec le Drag and Drop dans une LisView.
Le bouton de la souris lorsqu’il est pressé au moment du Drag sur un élément déjà sélectionné provoque la dé-sélection de tous les autres éléments.
Comme il s’agit d’un problème de composant visuel WPF, nous allons traiter ce disfonctionnement dans la partie View en modifiant le comportement du ListViewItem.
Pour cela il faut surcharger la classe ListViewItem et modifier le type d’éléments renvoyé par le ListView
public class ListViewMultiSelect : ListView { protected override DependencyObject GetContainerForItemOverride() { return new ListViewItemMultiSelect(); } }
Ensuite, le nouveau ListViewItem (ici ListViewItemMultiSelect) modifie le comportement du click.
- Si l’élément n’est pas déjà sélectionné, la sélection est faite normalement à la pression du bouton de la souris
- Si l’élément est sélectionné, rien n’est fait au moment de la pression du bouton (afin de garder les autres éléments sélectionnés) et la gestion de la sélection est reportée au moment du relâchement du bouton de la souris.
public class ListViewItemMultiSelect : ListViewItem { private bool _mouseWasDownAndSelected = false; protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e) { if (!IsSelected) { // it wasn't selected, so just do the normal thing base.OnPreviewMouseLeftButtonDown(e); return; } // it was selected, so we're going to totally ignore the mouse down... e.Handled = true; // but we will mark it ... _mouseWasDownAndSelected = true; } protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e) { // if we were watching this one, we'll unselect it if it were already selected if (IsSelected && _mouseWasDownAndSelected) { IsSelected = _mouseWasDownAndSelected = false; } base.OnMouseLeftButtonUp(e); } }
Il ne reste plus qu’à modifier le UserControl ImageFileListView.xaml de la partie View du projet pour utiliser ce nouveau composant Multi Select.
<local:ListViewMultiSelect SelectionMode="Extended" ... >
Tests unitaires
Les tests unitaires du Drag and Drop
Le drag and drop peut être testé avec les tests unitaires grâce au découpage MVVM.
Une classe SimpleTestClass a été spécialement créée afin de réaliser les tests
public class SimpleTestClass : Object, ICloneable { public object Clone() { return new SimpleTestClass(); } }
Le test unitaire peut ainsi être implémenté
[TestMethod()] [DeploymentItem("DragDropManager.dll")] public void MoveDraggedItemsTest() { ObservableCollection<SimpleTestClass> collection = new ObservableCollection<SimpleTestClass>(); SimpleTestClass a1 = new SimpleTestClass(); SimpleTestClass a2 = new SimpleTestClass(); SimpleTestClass a3 = new SimpleTestClass(); SimpleTestClass a4 = new SimpleTestClass(); collection.Add(a1); collection.Add(a2); collection.Add(a3); collection.Add(a4); Dictionary<int, SimpleTestClass> newItems = new Dictionary<int, SimpleTestClass>(); newItems.Add(3, collection[3]); newItems.Add(1, collection[1]); // Test Count and Order moved Items DragDropManagerList_Accessor<SimpleTestClass> .MoveDraggedItems(collection, newItems, 2); Assert.AreEqual(4, collection.Count); Assert.AreEqual(a1, collection[0]); Assert.AreEqual(a3, collection[1]); Assert.AreEqual(a4, collection[2]); Assert.AreEqual(a2, collection[3]); }
Les autres tests unitaires se retrouvent dans le projet zip.
Les tests unitaires de la partie View WPF
Ce type de test n’est pas courant car normalement le modèle M-V-VM doit nous permettre d’éviter de tester un comportement WPF.
Seulement ici dans le cas du drag and drop, nous souhaitons tester un comportement essentiel de la fonction GetVisualItem.
Cette fonction est très utilisée pour faire le lien entre le curseur de la souris et la donnée pointée par ce curseur.
GetVisualItem permet de récupérer l’élément visuel WPF correspondant à la donnée object “Bindé” dans le composant visuel. Exemple GetVisualItem(data1) va retourner le sous élément de type ListBoxItem contenant l’objet data1.
Pour tester de façon unitaire cette fonction, il faut donc créer un ObservableCollection qui contiendra les données et une ListBox UI WPF qui contiendra les éléments affichés. Comme nous sommes dans un test unitaire, il n’y a malheureusement pas création de fenêtre WPF ni donc de génération interne des composants ListBoxItem liés à l’ObservableCollection. Ainsi le code suivant ne fonctionnera pas car le composant ListBox ne contiendra pas d’UI éléments générés.
public void GetVisualItemTest1Helper<T>() where T : class, ICloneable { ListBox listbox = new ListBox(); T testClass = new Object() as T; ObservableCollection<T> collection = new ObservableCollection<T>(); collection.Add(testClass); listbox.ItemsSource = collection; DragDropManagerListBoxViewModel<T> dragdropManagerListBoxViewModel = new DragDropManagerListBoxViewModel<T>(listbox); PrivateObject param0 = new PrivateObject(dragdropManagerListBoxViewModel); DragDropManagerListBoxViewModel_Accessor<T> target = new DragDropManagerListBoxViewModel_Accessor<T>(param0); Assert.IsTrue(target.GetVisualItem(testClass) is ListBoxItem); }
Afin de simuler l’affichage WPF, j’ai construit une méthode qui va s’occuper de la génération des sous ensembles WPF comme si nous étions au moment de la construction d’une fenêtre WPF.
private static void PrepareUIItems(ListBox listbox) { IItemContainerGenerator itemContainerGenerator = ((IItemContainerGenerator)(listbox.ItemContainerGenerator)); using (itemContainerGenerator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward, true)) { DependencyObject cntr = null; do { cntr = itemContainerGenerator.GenerateNext(); if (cntr != null) itemContainerGenerator.PrepareItemContainer(cntr); } while (cntr != null); } }
Remarque 1 : Le “using … StartAt” est très important car c’est uniquement à la sortie de ce using que le ItemContainerGenerator WPF aura terminé entièrement la préparation de l’organisation de ces éléments internes.
Remarque 2: Il est impératif d’utiliser l’interface IItemContainerGenerator pour pouvoir appeler StartAt,…
En insérant cette méthode dans le code à tester, la fonction appelant du composant WPF peut être testée et le test unitaire fonctionne maintenant correctement.
public void GetVisualItemTestHelper<T>() where T : class, ICloneable { ListBox listbox = new ListBox(); T testClass = new Object() as T; DragDropManagerListBoxViewModel<T> dragdropManagerListBoxViewModel = new DragDropManagerListBoxViewModel<T>(listbox); PrivateObject param0 = new PrivateObject(dragdropManagerListBoxViewModel); DragDropManagerListBoxViewModel_Accessor<T> target = new DragDropManagerListBoxViewModel_Accessor<T>(param0); Assert.IsNull((target.GetVisualItem(0) as ListBoxItem)); ObservableCollection<T> collection = new ObservableCollection<T>(); collection.Add(testClass); listbox.ItemsSource = collection; // Generate WPF UI Items PrepareUIItems(listbox); Assert.IsTrue(target.GetVisualItem(0) is ListBoxItem); Assert.IsNull((target.GetVisualItem(1) as ListBoxItem)); } [TestMethod()] [DeploymentItem("DragDropManager.dll")] public void GetVisualItemTest() { GetVisualItemTestHelper<GenericParameterHelper>(); }
Remarque : Le helper créé avec l’assistant de test unitaire Visual Studio nous simplifie la tâche et nous permet de ne pas devoir créer une classe spécifique : le test peut ainsi être effectué avec le Generic .
Conclusion
Nous venons de voir comment implémenter le DragAndDrop suivant le modèle MVVM pour une ListView ou ListBox.
La Multi-Sélection est présent, la sélection et la destination des éléments est améliorée avec des Adorners, la copie ainsi que le déplacement des éléments sont gérés et le scroll est automatique avec le déplacement de la souris sur les bords.
Dans les prochaines parties, nous verrons comment gérer ces mêmes fonctionnalités avec un look TreeView.
Voici le lien pour télécharger la solution avec le code source du projet et les tests unitaires