{"id":448,"date":"2013-04-23T22:23:38","date_gmt":"2013-04-23T20:23:38","guid":{"rendered":"http:\/\/dev.bratched.fr\/fr\/wpf-listview-dimages-mvvm-et-drag-and-drop-partie-2-2\/"},"modified":"2013-04-23T22:23:38","modified_gmt":"2013-04-23T20:23:38","slug":"wpf-listview-dimages-mvvm-et-drag-and-drop-partie-2","status":"publish","type":"post","link":"https:\/\/bratched.com\/fr\/2013\/04\/23\/wpf-listview-dimages-mvvm-et-drag-and-drop-partie-2\/","title":{"rendered":"WPF, ListView d\u2019images, MVVM et Drag and Drop\u2026 \u2013 Partie 2"},"content":{"rendered":"<h1 class=\"jfdefaulttext\">Introduction<\/h1>\n<h2>Vu pr\u00e9c\u00e9demment<\/h2>\n<p>Nous avons vu dans la premi\u00e8re partie comment impl\u00e9menter une liste de fichiers en WPF avec le pattern M-V-VM. Nous allons voir dans cette partie comment ajouter les vignettes images \u201cThumbnails\u201d repr\u00e9sentant les images miniatures de ces fichiers.<\/p>\n<p><a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop07-jpg.jpg\"><img decoding=\"async\" loading=\"lazy\" title=\"title\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop07-thumb-jpg.jpg\" alt=\"alt\" width=\"324\" height=\"228\" border=\"0\" \/><\/a><\/p>\n<p>L\u2019exemple utilis\u00e9 reprend l\u2019application pr\u00e9c\u00e9dente (sans les vignettes) qui sera compl\u00e9t\u00e9e au fur et \u00e0 mesure.<\/p>\n<p>Pour revoir la premi\u00e8re partie :<\/p>\n<p><a href=\"\/fr\/index\/wpf\/53-wpf-listview-dimages-mvvm-et-drag-and-drop-partie-1.html\">WPF, ListView d\u2019images, MVVM et Drag and Drop\u2026 \u2013 Partie 1<\/a><\/p>\n<h2>Ajout des vignettes<\/h2>\n<p>Le premier r\u00e9flex est de modifier la partie View-Mod\u00e8le pour exposer la lecture du fichier image. Nous avons besoin du nom de fichier complet avec le chemin sur le disque dur. Nous ajoutons pour cela une nouvelle classe <strong>ImageFileViewModel <\/strong>qui va h\u00e9riter de FileViewModel. Cette classe va s\u2019occuper uniquement de l\u2019affichage des vignettes.<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/tmpEB.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Ajout de ImageFileViewModel.cs dans le ViewModel\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-tmpeb-thumb-png.png\" alt=\"Ajout de ImageFileViewModel.cs dans le ViewModel\" width=\"240\" height=\"74\" border=\"0\" \/><\/a><\/p>\n<p>Il nous faut ensuite ajouter la propri\u00e9t\u00e9 FileName dans la classe <strong>ImageFileViewModel<\/strong>. La propri\u00e9t\u00e9 va renvoyer le nom complet de l\u2019objet <strong>ImageFile<\/strong> contenu dans <strong>_imageFile<\/strong>.<\/p>\n<p><strong>ImageFileCollectionViewModel<\/strong> retourne d\u00e9sormais une collection d\u2019objet <strong>ImageFileViewModel<\/strong>.<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:fede7007-7d86-4083-ad1a-ac773209835a\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>public string FileName\n{\n  get { return _imageFile.FileName; }\n}<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p>Puis dans la partie View, nous ajoutons l\u2019image dans le UserControl ImageFileListView.xaml<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/tmp26E.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Ajout de l'image dans ImageFileListView\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-tmp26e-thumb-png.png\" alt=\"Ajout de l'image dans ImageFileListView\" width=\"220\" height=\"87\" border=\"0\" \/><\/a><\/p>\n<p>Le Binding est fait sur la propri\u00e9t\u00e9 Source de l\u2019Image avec <strong>{Path=FileName}<\/strong><\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:782f3c32-276a-4b88-ac99-5c4da674593a\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;ListView SelectionMode=\"Extended\"  x:Name=\"ListViewImage\"\n         ItemsSource=\"{Binding Path=AllImages}\"  Margin=\"0,20,0,0\"&gt;\n   &lt;ListView.ItemTemplate&gt;\n       &lt;DataTemplate&gt;\n           &lt;StackPanel Orientation=\"Horizontal\"&gt;\n               &lt;Image Source=\"{Binding Path=FileName}\" Width=\"25\" Height=\"25\"\/&gt;\n               &lt;TextBlock Text=\"{Binding Path=ShortName}\" \/&gt;\n           &lt;\/StackPanel&gt;\n       &lt;\/DataTemplate&gt;\n    &lt;\/ListView.ItemTemplate&gt;\u00a0\n&lt;\/ListView&gt;<\/pre>\n<\/div>\n<p>Nous pouvons lancer l\u2019application qui fonctionne\u2026<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/image_46f595417ac4eaf819fad89f9502b778.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Premier r\u00e9sultat catastrophique\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-image-thumb-d11c497ce9319c95d98c9022d0860152-png.png\" alt=\"Premier r\u00e9sultat catastrophique\" width=\"465\" height=\"327\" border=\"0\" \/><\/a><\/p>\n<p>et pleurer\u2026 <a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileycry-gif.gif\"><img decoding=\"async\" loading=\"lazy\" title=\"SmileyCry\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileycry-thumb-gif.gif\" alt=\"SmileyCry\" width=\"32\" height=\"32\" border=\"0\" \/><\/a> parce que c\u2019est tr\u00e8s Lennnnnt\u2026<\/p>\n<p><!--more--><\/p>\n<h2>Ce qu\u2019il faut am\u00e9liorer\u2026<\/h2>\n<p>Le r\u00e9sultat est l\u00e0 mais l\u2019interface est inutilisable avec un grand nombre d\u2019images (Et encore moins utilisable si leur poids est important et si le support est lent comme une cl\u00e9 USB par exemple) :<\/p>\n<ol>\n<li>Le chargement prend du temps car WPF essaie d\u2019afficher toutes les vignettes en m\u00eame temps. Nous souhaitons un affichage des vignettes qui va apporter un <strong>Plus<\/strong> \u00e0 l\u2019utilisateur mais surtout conserver <strong>une interface fluide. <\/strong><\/li>\n<li>Chaque changement avec la ScrollBar va entrainer un g\u00e8le \u201cFreeze\u201d de l\u2019application en attendant que toutes les vignettes soient lues et charg\u00e9es int\u00e9gralement en m\u00e9moire.<\/li>\n<li>Chaque changement avec la ScrollBar va entrainer le chargement en m\u00e9moire de l\u2019image pour son traitement et provoquer ensuite un engorgement en m\u00e9moire sans lib\u00e9ration avant la fermeture de l\u2019application.<\/li>\n<li>Certaines images jpg, pr\u00e9c\u00e9demment transform\u00e9es avec un outils annexe en images verticales ne sont pas visibles. Mal reconnus avec WPF?<\/li>\n<\/ol>\n<h1>Multi-Threading<\/h1>\n<h2>Pr\u00e9sentation<\/h2>\n<p>La premi\u00e8re \u00e9tape passe par le multi-Thread. Pendant l\u2019affichage des vignettes, l\u2019utilisateur doit continuer \u00e0 pouvoir utiliser l\u2019application sans \u00eatre bloqu\u00e9. Nous allons donc proc\u00e9der \u00e0 la lecture des images dans un thread en dehors du thread principal de WPF qui se charge de l&rsquo;interaction Homme machine (Thread UI).<\/p>\n<ol>\n<li>Nous allons cr\u00e9er une propri\u00e9t\u00e9 <strong>Thumbnail<\/strong> (vignette) sur l\u2019objet <strong>ImageFileViewModel<\/strong> qui s\u2019occupera de ce m\u00e9canisme et qui retournera une <strong>ImageSource <\/strong>pr\u00eate \u00e0 \u00eatre utilis\u00e9e par l\u2019interface.\u00a0 La classe ImageSource est suffisamment g\u00e9n\u00e9rique pour pouvoir \u00eatre ensuite utilis\u00e9e par n\u2019importe quelle m\u00e9thode WPF.<\/li>\n<li>Lors de la lecture de cette propri\u00e9t\u00e9, l\u2019objet ImageFileViewModel sera ajout\u00e9 \u00e0 une liste \u201c_listThumbnail\u201d partag\u00e9e entre tous les objets ImageFileViewModel (propri\u00e9t\u00e9 d\u00e9clar\u00e9e en static) .<\/li>\n<li>Un Thread tournera en t\u00e2che de fond en d\u00e9synchronis\u00e9 et bouclera sur la liste (pile) en d\u00e9pilant de la liste les images trait\u00e9es et redimensionn\u00e9es en thumbnails.<\/li>\n<li>Un message de notification WPF sera ensuite envoy\u00e9 \u00e0 la vue pour lui demander de rafraichir l\u2019image Thumbnail pr\u00eate \u00e0 \u00eatre affich\u00e9e.<\/li>\n<\/ol>\n<p>Voici un petit sch\u00e9ma pour essayer de r\u00e9sumer tout cela.<\/p>\n<p><a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop06-jpg.jpg\"><img decoding=\"async\" loading=\"lazy\" title=\"Multi Threading wpf UI graph\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop06-thumb-jpg.jpg\" alt=\"Multi Threading wpf UI graph\" width=\"612\" height=\"367\" border=\"0\" \/><\/a><\/p>\n<h2>D\u00e9tail<\/h2>\n<h3>Cr\u00e9ation de la propri\u00e9t\u00e9 Thumbnail dans l\u2019objet .<\/h3>\n<p>D\u00e9claration de la propri\u00e9t\u00e9 Thumbnail en ImageSource afin de cr\u00e9er la vignette avec les m\u00e9canismes de WPF.<br \/>\nDans le <strong>get<\/strong>,\u00a0 si l\u2019image n\u2019est pas initialis\u00e9e nous appelons <strong>LoadImage<\/strong> qui va empiler l\u2019objet \u00e0 la liste <strong>_listThumbnail<\/strong><\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:971bda85-9505-44fa-8f7d-fe3f4e183284\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private static readonly List&lt;ImageFileViewModel&gt; _listThumbnail \n= new List&lt;ImageFileViewModel&gt;();\nprivate ImageSource _thumbnail = null;\nprivate bool _loading = false;\n ...\npublic ImageSource Thumbnail\n{\n  get\n  {\n      if (_thumbnail == null)\n      {\n          LoadImage();\n          return null;\n      }\n      return _thumbnail;\n  }\n}\n\ninternal void LoadImage()\n{\n  if (!_loading)\n  {\n      _loading = true;\n      AddPhotoToLoad(this);\n  }\n}<\/pre>\n<\/div>\n<p><strong>LoadImage<\/strong> va ajouter l\u2019image dans la liste. _loading va permettre de savoir si la vignette est d\u00e9j\u00e0 charg\u00e9e et \u00e9viter de refaire le traitement pour rien.<\/p>\n<p>Nous sommes dans de l\u2019appel multi-thread, afin de ne pas ajouter 2 images au m\u00eame moment et de cr\u00e9er des conflits, nous devons verrouiller le m\u00e9canisme au moment de l\u2019ajout et notifier aux autres Thread que l\u2019ajout a bien \u00e9t\u00e9 effectu\u00e9.<\/p>\n<p>Le verrouillage se fait avec le <strong>lock <\/strong>d\u2019un objet <strong>static<\/strong> partag\u00e9 et l\u2019objet <strong>_loadThumbImageEvent<\/strong> va nous permettre de notifier un changement entre les Threads \u00e0 travers un \u00e9v\u00e9nement.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:fd11404a-b2a3-4992-ad5f-8ee630b12350\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private static readonly object _lockListThumbnail = new object();\nprivate static AutoResetEvent _loadThumbImageEvent = new AutoResetEvent(false);    \n    ...\n\nprivate static void AddPhotoToLoad(ImageFileViewModel p)\n{\n  lock (_lockListThumbnail)\n  {\n      _listThumbnail.Add(p);\n  }\n  _loadThumbImageEvent.Set();\n}<\/pre>\n<\/div>\n<p>Nous allons maintenant traiter les images empil\u00e9es dans <strong>_listThumbnail<\/strong> de fa\u00e7on d\u00e9synchronis\u00e9e.<\/p>\n<p>Nous avons besoin maintenant du Thread qui va boucler et traiter les \u00e9l\u00e9ments pr\u00e9sents dans la liste _listThumbnail lorsque le processeur aura le temps.<\/p>\n<p>La d\u00e9claration et la cr\u00e9ation du Thread ne se fait qu\u2019une seule fois dans le constructeur static.<\/p>\n<p>Le Thread s\u2019ex\u00e9cute en fond de tache (<strong>IsBackground = true<\/strong>) afin de permettre la fermeture de l\u2019application sans attendre la fin du Thread et en priorit\u00e9 basse (<strong>ThreadPriority.BelowNormal<\/strong>) pour ne pas p\u00e9naliser l\u2019interface. Au lancement, le Thread ex\u00e9cute la m\u00e9thode static <strong>LoaderThreadLoop()<\/strong><\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:965826bd-e47e-41ae-9186-baa788b85f94\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>static ImageFileViewModel()\n{\n  _loaderThreadThumbnail = new Thread(new ThreadStart(LoaderThreadLoop));\n  _loaderThreadThumbnail.IsBackground = true; \n  _loaderThreadThumbnail.Priority = ThreadPriority.BelowNormal;\n  _loaderThreadThumbnail.Start();\n}<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p>La m\u00e9thode LoaderThreadLoop boucle de fa\u00e7on infinie et va traiter la pile d\u2019image avec la m\u00e9thode <strong>ProcessLoadImageStack<\/strong>(). L\u2019attente de 10 ms permet de redonner la main au Thread principal WPF et de rendre ainsi\u00a0 l\u2019application plus fluide.<\/p>\n<p>_LoadThumbImageEvent.WaitOne() attend une notification par Set() envoy\u00e9 lors du LoadImage.<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:018a668f-750a-48cb-9ac5-65eb2dc89272\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private static void LoaderThreadLoop()\n{\n    do\n    {\n        _loadThumbImageEvent.WaitOne();\n        while (ProcessLoadImageStack())\n        {\n            Thread.Sleep(10);\n        }\n    }\n    while (true);\n}\n<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p><strong>ProcessLoadImageStack()<\/strong> doit maintenant r\u00e9cup\u00e9rer le dernier \u00e9l\u00e9ment de la liste, l\u2019enlever de la liste puis appeler l\u2019instruction de lecture de l\u2019image et de g\u00e9n\u00e9ration de la vignette\u00a0 <strong>DoLoadImage<\/strong>().<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:647787d0-68e0-4953-92a9-2d3e8e188455\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private static bool ProcessLoadImageStack()\n{\n  ImageFileViewModel p = null;\n  if (_listThumbnail.Count &gt; 0)\n  {\n      lock (_lockListThumbnail)\n      {\n          if (_listThumbnail.Count &gt; 0)\n          {\n              p = _listThumbnail[_listThumbnail.Count - 1];\n              _listThumbnail.Remove(p);\n          }\n      }\n  }\n  if (p != null)\n      p.DoLoadImage();\n  return true;\n}<\/pre>\n<\/div>\n<p><strong>DoLoadImage<\/strong>() va charger l\u2019image dans un <strong>Stream<\/strong> puis le traitement va \u00eatre r\u00e9alis\u00e9 par des actions WPF.<\/p>\n<p>Le chargement de l\u2019image en Stream s\u2019effectue dans la partie Model. Nous ajoutons le code Stream LoadImage() suivant dans l\u2019interface<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:f22fd370-304e-4f59-9d06-bc71b25a72b2\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>namespace wpfListViewSample02.Model\n{\n    public interface IImageFile\n    {\n        ...\n        Stream LoadImage();\n    }\n\n}\n<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p>Puis l\u2019impl\u00e9mentation dans l\u2019objet m\u00e9tier<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:5cdd018d-cc32-41ff-a5e9-13114a17c965\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>namespace wpfListViewSample02.Model\n{\n    public class ImageFile: IImageFile\n    {\n        ...\n\n        public Stream LoadImage()\n        {\n            byte[] buffer = File.ReadAllBytes(FileName);\n            return new MemoryStream(buffer);\n        }\n    }\n}<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p>Dans DoLoadImage, nous chargeons maintenant le Stream afin d\u2019effectuer un traitement de vignette.<\/p>\n<p>Un probl\u00e8me se pose de nouveau, les actions WPF ne peuvent se faire que dans le thread WPF UI qui est unique. DoLoadImage est lanc\u00e9 par le thread asynchrone qui traite les images quand le processeur n\u2019est pas occup\u00e9.<\/p>\n<p>&nbsp;<\/p>\n<p>Pour raccrocher les actions WPF au thread WPF, nous utilisons l\u2019instruction <strong>Dispatcher.Invoke <\/strong>qui repr\u00e9sente le Thread unique UI de WPF de l\u2019application. Le dispatcher utilis\u00e9 est celui de l\u2019application WPF rep\u00e9rable par <strong>Dispatcher.CurrentDispatcher<\/strong>.<\/p>\n<p>Il prend comme param\u00e8tre la priorit\u00e9 du traitement (basse, pas besoin de se presser pour ce traitement les autres actions de WPF sont prioritaires), les param\u00e8tres \u00e9chang\u00e9s par le d\u00e9l\u00e9gu\u00e9 ici un type <strong>delegate<\/strong> sans argument : <strong>NoArgumentDelegate<\/strong> puis le traitement dans le <strong>delegate<\/strong>. Le traitement appelle une autre instruction <strong>GenerateThumbnail<\/strong>(de l\u2019objet Stream pr\u00e9c\u00e9demment charg\u00e9) qui va transformer l\u2019image Stream en vignette ImageSource avec WPF et retourner l\u2019image dans la propri\u00e9t\u00e9 Thumbnail.<\/p>\n<p>A la fin du traitement, nous vidons le Stream et notifions \u00e0 WPF que la propri\u00e9t\u00e9 Thumbnail a chang\u00e9e (elle contient la vignette image g\u00e9n\u00e9r\u00e9e) et donc que la vue doit \u00eatre rafraichie dans l\u2019interface visuelle WPF <strong>OnPropertyChanged(\u201cThumbnail\u201d)<\/strong>.<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:43b931f5-541b-43d9-991e-abc9aa242aa4\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private delegate void NoArgumentDelegate();\nprotected virtual void DoLoadImage()\n{ \n  if (!_loading || this._imageFile == null) { return; }\n  if (this._imageFile.IsAvailable)\n  {\n      Stream mem = null;\n      try\n      {\n          mem = this._imageFile.LoadImage();\n          if ((!_loading))\n          {\n              mem.Dispose();\n              return;\n          }\n          Dispatcher.CurrentDispatcher.Invoke\n          (DispatcherPriority.ApplicationIdle, (NoArgumentDelegate)\n              delegate()\n              {\n              _thumbnail = GeneratedThumbnail(mem);\n              }\n          );\n      }\n      finally\n      {\n          if (mem != null)\n          {\n              mem.Close();\n          }\n          _loading = false;\n          this.OnPropertyChanged(\"Thumbnail\");\n      }\n  }\n}\n<\/pre>\n<\/div>\n<p>Un dernier petit code pour montrer comment la vignette est g\u00e9n\u00e9r\u00e9e par WPF. A noter que dans le cas o\u00f9 nous ne trouvons pas de vignette, nous retournons un image vide et surtout pas Null sinon nous monopolisons un Traitement qui ne finira jamais (if (Thumbnail==null) \u2026 LoadImage) .<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:e4c1b147-eec0-4755-bdef-4e7b2e429267\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>protected ImageSource GeneratedThumbnail(Stream mem)\n{\n  mem.Position = 0;\n  BitmapDecoder imgDecoder = BitmapDecoder.Create\n      (mem, BitmapCreateOptions.None, BitmapCacheOption.None);\n  if ((imgDecoder != null) \n      &amp;&amp; (imgDecoder.Frames.Count &gt; 0) \n      &amp;&amp; (imgDecoder.Frames[0] != null) \n      &amp;&amp; (imgDecoder.Frames[0].Thumbnail != null))\n      return imgDecoder.Frames[0].Thumbnail;\n  else\n      return new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgr32, null);\n}<\/pre>\n<\/div>\n<p>Dans ImageFileListView.xaml, nous rempla\u00e7ons la propri\u00e9t\u00e9 Source <strong>{Path=FileName}<\/strong> de l\u2019Image avec {Path=Thumbnai<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:b11b00c1-c30f-4fb9-9eed-5acda969dfea\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre> &lt;Image Source=\"{Binding Path=Thumbnail}\" Width=\"25\" Height=\"25\"\/&gt;<\/pre>\n<\/div>\n<p>Voil\u00e0 nous pouvons lancer de nouveau l\u2019application.<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/image_435bc8d3d210777ddba392b9aec96359.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Listview application with Thread\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-image-thumb-f920dcdf2920c1a987f3ec97737ea663-png.png\" alt=\"Listview application with Thread\" width=\"417\" height=\"293\" border=\"0\" \/><\/a><\/p>\n<p>C\u2019est d\u00e9j\u00e0 beaucoup plus fluide et utilisable. <a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-iconsmile-gif.gif\"><img decoding=\"async\" loading=\"lazy\" title=\"iconsmile\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-iconsmile-thumb-gif.gif\" alt=\"iconsmile\" width=\"29\" height=\"29\" border=\"0\" \/><\/a> . Les images mal reconnues (sans vignettes EXIF) s\u2019affichent alors en noir.<\/p>\n<h2>Optimisons encore un peu\u2026<\/h2>\n<p>Avec un gros volume d\u2019images, nous avons encore plusieurs probl\u00e8mes visuels :<\/p>\n<ol>\n<li>Toutes les images vignettes restent en m\u00e9moire, ce qui va repr\u00e9senter plusieurs centaines de Mo.<\/li>\n<li>Lorsque nous jouons plusieurs fois avec l\u2019ascenseur de haut en bas, les images sont empil\u00e9es sans limites et il faut attendre parfois longtemps avant d\u2019avoir l\u2019affichage de la vignette attendue.<\/li>\n<\/ol>\n<h1>Virtualizing<\/h1>\n<h2>VirtualizingStackPanel<\/h2>\n<p>Le composant <strong>ListView<\/strong> contient une propri\u00e9t\u00e9 importante <strong>VirtualizingStackPanel <\/strong>qui va nous permettre de r\u00e9soudre la probl\u00e9matique de l\u2019espace m\u00e9moire. En effet, il n\u2019est pas utile de conserver l\u2019ensemble des vignettes pour toutes les images parcourues et nous pouvons lib\u00e9rer les images non visibles.<\/p>\n<p>Nous allons donc indiquer dans <strong>ImageListView.xaml<\/strong><\/p>\n<ol>\n<li>L\u2019utilisation de la virtualisation : <strong>VirtualizingStackPanel.IsVirtualizing=\u201dTrue\u201d<\/strong><\/li>\n<li>L\u2019\u00e9v\u00e9nement qui sera ex\u00e9cut\u00e9 lors du recyclage des objets <strong>VirtualizingStackPanel.CleanUpVirtualizedItem = \u201cListViewImage_CleanUpVirtualizedItem\u201d<\/strong><\/li>\n<\/ol>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:e15a7316-ecc0-4642-844b-4fe454711a50\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;ListView SelectionMode=\"Extended\"  x:Name=\"ListViewImage\"\n         ItemsSource=\"{Binding Path=AllImages}\"  Margin=\"0,20,0,0\"\n         VirtualizingStackPanel.IsVirtualizing=\"True\"\n         VirtualizingStackPanel.CleanUpVirtualizedItem=\"ListViewImage_CleanUpVirtualizedItem\"&gt;\n   &lt;ListView.ItemTemplate&gt;\n       &lt;DataTemplate&gt;\n           &lt;StackPanel Orientation=\"Horizontal\"&gt;\n               &lt;Image Source=\"{Binding Path=Thumbnail}\" Width=\"25\" Height=\"25\"\/&gt;\n               &lt;TextBlock Text=\"{Binding Path=ShortName}\" \/&gt;\n           &lt;\/StackPanel&gt;\n       &lt;\/DataTemplate&gt;\u00a0\n   &lt;\/ListView.ItemTemplate&gt;    \n&lt;\/ListView&gt;<\/pre>\n<\/div>\n<p>Dans le code de la vue nous appelons la lib\u00e9ration de l\u2019\u00e9l\u00e9ment visuel de l\u2019objet ViewModel.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:b972cf29-bc86-426d-9c33-4de959bd7019\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>if (e.Value is ImageFileViewModel)\n{\n (e.Value as ImageFileViewModel).CleanUp();\n}<\/pre>\n<\/div>\n<p>Puis dans la partie ViewModel dans l\u2019objet ImageFileViewModel nous impl\u00e9mentons <strong>CleanUp()<\/strong>.<\/p>\n<ol>\n<li>Comme la liste est partag\u00e9e dans un thread ind\u00e9pendant, nous validons son acc\u00e8s avec lock auparavant.<\/li>\n<li>Dans le cas o\u00f9 la vignette doit \u00eatre supprim\u00e9e car non visible, il est inutile de le traiter dans la liste. Nous l\u2019enlevons de la liste,<\/li>\n<li>Nous supprimons ensuite la vignette et indiquons que le chargement n\u2019est pas effectu\u00e9 (_loading = false)\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:c511ba1f-46e5-44f0-af59-779e4e4acb13\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>internal void CleanUp()\n{\n  lock (_lockListThumbnail)\n  {\n       _listThumbnail.Remove(this);\n       _loading = false;\n       _thumbnail = null;\n  }\n}<\/pre>\n<\/div>\n<\/li>\n<\/ol>\n<p>Pour v\u00e9rifier combien d\u2019\u00e9l\u00e9ments sont r\u00e9ellement charg\u00e9s dans l\u2019interface, je vous propose d\u2019ajouter un bouton sur l\u2019interface.<\/p>\n<p>La m\u00e9thode GetVisualCount parcourt l\u2019affichage d\u2019un objet et compte le nombre d\u2019objets visuels WPF enfants\u00a0du type .<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:5b40ca01-c519-4360-839e-9e459ee09abb\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private void BtnVirtualizeCount_Click(object sender, RoutedEventArgs e)\n{\n  MessageBox.Show(\n  GetVisualCount&lt;ImageFileViewModel&gt;(this.listView1.ListViewImage).ToString()\n  , \"\");\n}\n\nprivate static int GetVisualCount&lt;T&gt;(DependencyObject visual) where T : class\n{\n  int visualCount = 0;\n  if (visual is ContentControl \n    &amp;&amp; (visual as ContentControl).Content is T) visualCount++;\n  int childCount = VisualTreeHelper.GetChildrenCount(visual);\n  for (int i = 0; i &lt; childCount; i++)\n  {\n      DependencyObject childVisual = VisualTreeHelper.GetChild(visual, i);\n      visualCount += GetVisualCount&lt;T&gt;(childVisual);\n  }\n  return visualCount;\n}<\/pre>\n<\/div>\n<p>Voici le r\u00e9sultat, montrant que le nombre d\u2019objets visuels affich\u00e9s correspond au nombre d\u2019images affich\u00e9es dans l\u2019interface. Le calcul des vignettes se fait \u00e0 chaque fois, ainsi si l\u2019on descend \u00e0 la fin de la liste et que l\u2019on revient au d\u00e9but, le calcul s\u2019effectuera une deuxi\u00e8me fois pour les vignettes.<\/p>\n<p>Remarque : Si le calcul est tr\u00e8s long, il est encore possible de g\u00e9rer une liste limit\u00e9e d\u2019images dans une structure de type collection servant alors de cache (voir dans la suite &#8211; partie 3).<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/tmp1E7.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Listview application with Thread and Virtualizing\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-tmp1e7-thumb-png.png\" alt=\"Listview application with Thread and Virtualizing\" width=\"372\" height=\"263\" border=\"0\" \/><\/a><\/p>\n<h2>Encore un peu plus rapide<\/h2>\n<p>Depuis WPF 3.5 SP1, un autre mode de Virtualization existe pour r\u00e9utiliser les \u00e9l\u00e9ments WPF existants et \u00e9viter de cr\u00e9er puis d\u00e9truire \u00e0 chaque fois les objets virtualis\u00e9s. Il en r\u00e9sulte une grande am\u00e9lioration de la v\u00e9locit\u00e9 d\u2019affichage pour les \u00e9l\u00e9ments WPF virtualis\u00e9s. Dans l\u2019interface WPF <strong>ImageListView.xaml<\/strong>,nous ajoutons la propri\u00e9t\u00e9 <strong>VirtualizingStackPanel.VirtualizationMode = \u201cRecycling<\/strong>\u201d (qui est par d\u00e9faut <strong>= \u00ab\u00a0Standard\u00a0\u00bb).<\/strong><\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:62965897-26c3-4abd-9cb7-6797510aa95f\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;ListView SelectionMode=\"Extended\"  x:Name=\"ListViewImage\"\n         ItemsSource=\"{Binding Path=AllImages}\"  Margin=\"0,20,0,0\"\n         VirtualizingStackPanel.IsVirtualizing=\"True\"\n         VirtualizingStackPanel.VirtualizationMode= \"Recycling\"\n         VirtualizingStackPanel.CleanUpVirtualizedItem=\"ListViewImage_CleanUpVirtualizedItem\"&gt;\n   &lt;ListView.ItemTemplate&gt;\n       &lt;DataTemplate&gt;\n           &lt;StackPanel Orientation=\"Horizontal\"&gt;\n               &lt;Image Source=\"{Binding Path=Thumbnail}\" Width=\"25\" Height=\"25\"\/&gt;\n               &lt;TextBlock Text=\"{Binding Path=ShortName}\" \/&gt;\n           &lt;\/StackPanel&gt;\u00a0\u00a0\n       &lt;\/DataTemplate&gt;\n   &lt;\/ListView.ItemTemplate&gt;\n&lt;\/ListView&gt;<\/pre>\n<\/div>\n<p>En faisant l\u2019essai vous constaterez encore une am\u00e9lioration de la rapidit\u00e9 non n\u00e9gligeable.<\/p>\n<h1>La gestion des vignettes sans donn\u00e9es EXIF avec WPF<\/h1>\n<h2>TransformedBitmap<\/h2>\n<p>Il nous reste \u00e0 g\u00e9rer\u00a0 le cas o\u00f9 la vignette n\u2019existe pas dans l\u2019information EXIF de l\u2019image. Pour cela nous modifions un peu GeneratedThumbnail(Stream mem).<\/p>\n<p>GeneratedThumbnail essaie de trouver la vignette stock\u00e9e ( <strong>imgDecoder.Frames[0].Thumbnail<\/strong>) dans le cas o\u00f9 on ne trouve rien, une exception est lanc\u00e9e et tempThumbnail est null.<\/p>\n<p>Dans le cas ou frame (ImgDecoder.Frames[0] est vide, on appelle <strong>TransformedBitmap<\/strong> qui retaille l\u2019image trouv\u00e9e et retourne une ImageSource avec <strong>ScaleTransform<\/strong>. L\u2019image retourn\u00e9e fait ici une hauteur ou une largeur maximale de 240 pixels.<\/p>\n<p>Dans le dernier cas o\u00f9 l\u2019image n\u2019est toujours pas reconnue nous retournons le bitmap noir.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:d041b327-2f14-4741-981a-9216e8f7fcbc\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>private ImageSource GeneratedThumbnail(Stream mem)\n{\n  ImageSource tempThumbnail = null;\n  BitmapFrame frame = null;\n  try\n  {\n      mem.Position = 0;\n      BitmapDecoder imgDecoder = BitmapDecoder.Create\n      (mem, BitmapCreateOptions.None, BitmapCacheOption.None);\n      if (imgDecoder != null &amp;&amp; imgDecoder.Frames.Count &gt; 0)\n          frame = imgDecoder.Frames[0];\n      if (frame != null)\n        tempThumbnail = frame.Thumbnail;\n  }\n  catch\n  {\n      tempThumbnail = null;\n  }\n\n  if (tempThumbnail == null &amp;&amp; frame != null) \n      tempThumbnail = TransformedThumbnail(frame);\n  if (tempThumbnail != null)\n      return tempThumbnail;\n  else\n      return new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgr32, null);\n}\n\nprivate ImageSource TransformedThumbnail(BitmapFrame frame)\n{\n  TransformedBitmap tempThumbnail = new TransformedBitmap();\n  tempThumbnail.BeginInit();\n  try\n  {\n    tempThumbnail.Source = frame as BitmapSource;\n    int decodeH = 240;\n    int decodeW = 240;\n    int pixelH = frame.PixelHeight;\n    int pixelW = frame.PixelWidth;\n    if (pixelH \/ pixelW &gt; 0 &amp;&amp; pixelH &gt; decodeH) \n        decodeW = (frame.PixelWidth * decodeH) \/ pixelH;\n    else\n        if (pixelW &gt; decodeW)\n            decodeH = (frame.PixelHeight * decodeW) \/ pixelW;\n    double scaleX = decodeW \/ (double)pixelW;\n    double scaleY = decodeH \/ (double)pixelH;\n    TransformGroup transformGroup = new TransformGroup();\n    transformGroup.Children.Add(new ScaleTransform(scaleX, scaleY));\n    tempThumbnail.Transform = transformGroup;\n  }\n  finally\n  {\n      tempThumbnail.EndInit();\n  }\n  WriteableBitmap writable = new WriteableBitmap(tempThumbnail);\n  return writable;\n}<\/pre>\n<\/div>\n<h1>Objets Freezable et Freeze() et animation d\u2019attente<\/h1>\n<h2>Probl\u00e9matique<\/h2>\n<p>Le traitement pr\u00e9c\u00e9dent est assez lourd et peut occuper du temps processeur dans le cas de transformation en vignettes d\u2019images de taille importante.<\/p>\n<p>Afin d\u2019optimiser toujours un peu plus l\u2019interface, il serait int\u00e9ressant de ne pas monopoliser le thread UI WPF pour r\u00e9aliser cette manipulation. Nous allons pour cela utiliser la m\u00e9thode Freeze() des objets WPF d\u00e9rivant de l\u2019objet <strong>Freezable<\/strong>.<\/p>\n<h2>Impl\u00e9mentation de Freeze()<\/h2>\n<p>Cette m\u00e9thode va permettre un acc\u00e8s en lecture seule \u00e0 WPF au objets que nous manipulons dans un autre Thread.<\/p>\n<p>Dans la pr\u00e9c\u00e9dente m\u00e9thode <strong>DoLoadImage<\/strong>, La transformation <strong>GeneratedThumbnail<\/strong> \u00e9tait effectu\u00e9e dans le delegate() (Dans le thread WPF UI).<\/p>\n<p>Nous le d\u00e9pla\u00e7ons dans le Thread de chargement pr\u00e9c\u00e9demment mis en place et utilisons un objet interm\u00e9diaire \u201ctempImgSrc\u201d. \u201c<strong>tempImgSrc.Freeze()\u201d<\/strong> permet de geler l\u2019objet en \u00e9criture pour WPF et de le rendre accessible en lecture au Thread WPF.<\/p>\n<p>Nous affectons ensuite tempImgSrc \u00e0 la propri\u00e9t\u00e9 Thumbnail dans le Thread WPF UI.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:b6ebdbe6-7f85-4e41-9ba9-d22e37058238\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>protected virtual void DoLoadImage()\n{ \n  if (!_loading || this._imageFile == null) { return; }\n  if (this._imageFile.IsAvailable)\n  {\n      Stream mem = null;\n      try\n      {\n          mem = this._imageFile.LoadImage();\n          if ((!_loading))\n          {\n              mem.Dispose();\n              return;\n          }\n          ImageSource tempImgSrc = GeneratedThumbnail(mem);\n          tempImgSrc.Freeze();  \n          Dispatcher.CurrentDispatcher.Invoke\n            (DispatcherPriority.ApplicationIdle, (NoArgumentDelegate)\n            delegate()\n            {\n                \/\/ old _thumbnail = GeneratedThumbnail(mem);\n                _thumbnail = tempImgSrc;\n            });\n      }\n      finally\n      {\n          if (mem != null)\n          {\n              mem.Close();\n          }\n          _loading = false;\n          this.OnPropertyChanged(\"Thumbnail\");\n      }           \n  }\n}\n<\/pre>\n<\/div>\n<p>Le sch\u00e9ma g\u00e9n\u00e9ral ressemble \u00e0 cela maintenant :<\/p>\n<p>&nbsp;<\/p>\n<p><a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop06b-jpg.jpg\"><img decoding=\"async\" loading=\"lazy\" title=\"WpfDragDrop06b\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-wpfdragdrop06b-thumb-jpg.jpg\" alt=\"WpfDragDrop06b\" width=\"605\" height=\"495\" border=\"0\" \/><\/a><\/p>\n<h2>Animation d\u2019attente<\/h2>\n<p>Derni\u00e8re \u00e9tape, afficher une petite animation d\u2019attente lors de la g\u00e9n\u00e9ration des vignettes qui peut parfois prendre un peu de temps.<\/p>\n<p>Afin de faciliter cette manipulation, nous allons ajouter une propri\u00e9t\u00e9 <strong>IsLoaded <\/strong>dans la classe ImageFileViewModel de la couche ViewModel.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:f56290c9-9470-431b-95a5-3ed28e0f5487\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>public bool IsLoaded\n{\n  get\n  {\n      return (_thumbnail!=null);\n  }\n}\n<\/pre>\n<\/div>\n<p>Cette propri\u00e9t\u00e9 retourne true si l\u2019image est pr\u00e9sente et false si l\u2019image n\u2019est pas encore charg\u00e9e.<\/p>\n<p>Dans le <strong>DoLoadImage<\/strong>, nous ajoutons une notification \u00e0 WPF du changement de IsLoaded lorsque l\u2019image est charg\u00e9e :<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:72eeb884-c51c-45a7-8f7a-dab21d15bc43\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>protected virtual void DoLoadImage()\n{ \n  if (!_loading || this._imageFile == null) { return; }\n  if (this._imageFile.IsAvailable)\n  {\n      Stream mem = null;\n      try\n      ...\n      finally\n      {\n          if (mem != null)\n          {\n              mem.Close();\n          }\n          _loading = false;\n          OnPropertyChanged(\"Thumbnail\");\n          OnPropertyChanged(\"IsLoaded\");\n      }           \n  }\n}<\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<p>Dans la partie View, nous allons afficher un sablier avec une petite animation lorsque l\u2019image est en cours de chargement.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:75551e12-745a-4285-bbca-7791949bbbe4\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;StackPanel Orientation=\"Horizontal\" Height=\"25\"&gt;\n     &lt;Image x:Name=\"ThumbnailImage\" \n     Visibility=\"Collapsed\" \n     Height=\"20\" Width=\"20\" \n     Source=\"{Binding Path=Thumbnail}\"&gt;\n         &lt;Image.BitmapEffect&gt;\n             &lt;DropShadowBitmapEffect ShadowDepth=\"2\" \/&gt;\n         &lt;\/Image.BitmapEffect&gt;\n     &lt;\/Image&gt;\n     &lt;Image x:Name=\"WaitingImage\" \n     Visibility=\"Visible\" \n     Height=\"20\" Width=\"20\" \n     Source=\"\/View\/Hourglass.png\"&gt;\n     &lt;\/Image&gt;\n     &lt;TextBlock Text=\"{Binding Path=ShortName}\" \/&gt;\n&lt;\/StackPanel&gt;<\/pre>\n<\/div>\n<p>Deux images \u201c<strong>ThumbnailImage<\/strong>\u201d et \u201c<strong>WaitingImage<\/strong>\u201d sont affich\u00e9es ou cach\u00e9es en fonction de la propri\u00e9t\u00e9 \u201cIsLoaded\u201d<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:aa808046-37fc-4609-b987-c2dc0dae99d7\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;DataTemplate.Triggers&gt;\n&lt;DataTrigger Binding=\"{Binding Path=IsLoaded}\" Value=\"True\"&gt;\n    &lt;Setter Property=\"Visibility\" TargetName=\"ThumbnailImage\" Value=\"Visible\"\/&gt;\n    &lt;Setter Property=\"Visibility\" TargetName=\"WaitingImage\" Value=\"Collapsed\"\/&gt;    \n&lt;\/DataTrigger&gt;\n\n&lt;DataTrigger Binding=\"{Binding Path=IsLoaded}\" Value=\"False\"&gt;\n    &lt;Setter Property=\"Visibility\" TargetName=\"WaitingImage\" Value=\"Visible\"\/&gt;\n    &lt;Setter Property=\"Visibility\" TargetName=\"ThumbnailImage\" Value=\"Collapsed\"\/&gt;\n&lt;\/DataTrigger&gt;<\/pre>\n<\/div>\n<p>Pour afficher le sablier avec une petite animation, nous ajoutons une ressource <strong>Storyboard<\/strong> \u201cWaitingTimeLine\u201d et un d\u00e9clenchement de cette animation avec un arr\u00eat de l\u2019animation lors du changement d\u2019\u00e9tat de la valeur IsLoaded.<\/p>\n<p><strong>DataTrigger.EnterActions<\/strong> permet de d\u00e9clencher l\u2019animation<br \/>\n<strong>DataTrigger.ExitActions<\/strong> permet d\u2019arr\u00eater l\u2019action pr\u00e9c\u00e9demment d\u00e9finie<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:da95b248-aff0-4cd6-8b87-3074850e37c8\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;DataTemplate.Triggers&gt;\n    &lt;DataTrigger Binding=\"{Binding Path=IsLoaded}\" Value=\"True\"&gt;\n        &lt;Setter Property=\"Visibility\" TargetName=\"ThumbnailImage\" Value=\"Visible\"\/&gt;\n        &lt;Setter Property=\"Visibility\" TargetName=\"WaitingImage\" Value=\"Collapsed\"\/&gt;\n    &lt;\/DataTrigger&gt;\n    \n    &lt;DataTrigger Binding=\"{Binding Path=IsLoaded}\" Value=\"False\"&gt;\n        &lt;Setter Property=\"Visibility\" TargetName=\"WaitingImage\" Value=\"Visible\"\/&gt;\n        &lt;Setter Property=\"Visibility\" TargetName=\"ThumbnailImage\" Value=\"Collapsed\"\/&gt;\n        \n        &lt;DataTrigger.EnterActions&gt;\n            &lt;BeginStoryboard x:Name=\"WaitingTimeline_BeginStoryboard\" \n            Storyboard=\"{StaticResource WaitingTimeline}\"\/&gt;\n        &lt;\/DataTrigger.EnterActions&gt;\n        &lt;DataTrigger.ExitActions&gt;\n            &lt;StopStoryboard BeginStoryboardName=\"WaitingTimeline_BeginStoryboard\"\/&gt;\n        &lt;\/DataTrigger.ExitActions&gt;\n    &lt;\/DataTrigger&gt;\u00a0\n&lt;\/DataTemplate.Triggers&gt;<\/pre>\n<\/div>\n<p>L\u2019animation <strong>Storyboard<\/strong> \u201cWaitingTimeLine\u201d est d\u00e9finie ainsi :<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:2c3c0d7a-410f-4c94-9a7e-811236c3bb23\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;DataTemplate.Resources&gt;\n&lt;Storyboard x:Key=\"WaitingTimeline\" Timeline.DesiredFrameRate=\"10\"&gt;\n    &lt;DoubleAnimationUsingKeyFrames BeginTime=\"00:00:00\" RepeatBehavior=\"Forever\" \n        Storyboard.TargetName=\"WaitingImage\" \n        Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TransformGroup.Children)[0]\n                                   .(RotateTransform.Angle)\"&gt;\n        &lt;SplineDoubleKeyFrame KeyTime=\"00:00:00\" Value=\"-15\"\/&gt;\n        &lt;SplineDoubleKeyFrame KeyTime=\"00:00:03\" Value=\"15\"\/&gt;\n    &lt;\/DoubleAnimationUsingKeyFrames&gt;\n&lt;\/Storyboard&gt;\n&lt;\/DataTemplate.Resources&gt;<\/pre>\n<\/div>\n<p>l\u2019animation charg\u00e9e en ressource va s\u2019appliquer \u00e0 la transformation \u00ab\u00a0rotation\u201d de l\u2019image \u201cWaitingImage\u201d.<\/p>\n<p>Ce Storyboard fait varier un nombre de \u201315 \u00e0 +15 selon une chronologie d\u00e9finie. Ce nombre correspondra \u00e0 une inclinaison en degr\u00e9 de la rotation du sablier.<\/p>\n<p>Encore une petite astuce d\u2019optimisation, les animations en WPF sont param\u00e9tr\u00e9es par d\u00e9faut pour g\u00e9n\u00e9rer <strong>60 fps<\/strong> (frame per seconds) , ce qui est compl\u00e8tement inutile dans notre cas. Nous pouvons accepter une animation \u00e0 10 images par secondes, nous ajoutons donc la propri\u00e9t\u00e9 <strong>TimeLine.DesiredFrameRate=\u201d10\u201d<\/strong>. Cela ne changera rien \u00e0 l\u2019affichage et lib\u00e9rera encore un peu de puissance CPU \/ GPU (processeur principal ou de la carte graphique).<\/p>\n<p>&nbsp;<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:a5063b83-f330-48df-835d-d5fa7c6205a8\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>&lt;StackPanel Orientation=\"Horizontal\" Height=\"25\"&gt;\n     &lt;Image x:Name=\"ThumbnailImage\" \n     Visibility=\"Collapsed\" \n     Height=\"20\" Width=\"20\" \n     Source=\"{Binding Path=Thumbnail}\"&gt;\n         &lt;Image.BitmapEffect&gt;\n             &lt;DropShadowBitmapEffect ShadowDepth=\"2\" \/&gt;\n         &lt;\/Image.BitmapEffect&gt;\n     &lt;\/Image&gt;\n     &lt;Image x:Name=\"WaitingImage\" \n     Visibility=\"Visible\" \n     Height=\"20\" Width=\"20\" \n     Source=\"\/View\/Hourglass.png\"&gt;\n         &lt;Image.RenderTransform&gt;\n             &lt;TransformGroup&gt;\n                 &lt;RotateTransform Angle=\"0\" CenterX=\"10\" CenterY=\"10\"\/&gt;\n             &lt;\/TransformGroup&gt;\n         &lt;\/Image.RenderTransform&gt;\n     &lt;\/Image&gt;\n     &lt;TextBlock Text=\"{Binding Path=ShortName}\" \/&gt;\n&lt;\/StackPanel&gt;<\/pre>\n<\/div>\n<p>Ex\u00e9cutons l\u2019application<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/image_14e62ffcd9dc5f9fb476192ac04937e5.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Application Listview with waiting animation\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-image-thumb-7a44540f5131184e9ceda0b0230e5298-png.png\" alt=\"Application Listview with waiting animation\" width=\"444\" height=\"312\" border=\"0\" \/><\/a><\/p>\n<p>L\u2019interface utilisateur est fluide et l\u2019application consomme une charge m\u00e9moire tr\u00e8s raisonnable, m\u00eame avec un nombre tr\u00e8s important d\u2019images.<\/p>\n<p><a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileylol-gif.gif\"><img decoding=\"async\" loading=\"lazy\" title=\"Smileylol\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileylol-thumb-gif.gif\" alt=\"Smileylol\" width=\"32\" height=\"32\" border=\"0\" \/><\/a><\/p>\n<h1>Les tests unitaires<\/h1>\n<p>Et les tests unitaires alors ?<\/p>\n<p>Je le r\u00e9p\u00e8te une nouvelle fois pour bien insister, toute l\u2019organisation M-V-VM, comme toute organisation de projet en couche (MVC, MVP,\u2026) a comme principal objectif de pouvoir automatiser les tests de l\u2019application et donc de faire gagner beaucoup de temps sur la phase finale du d\u00e9veloppement et sur les maintenances.<\/p>\n<p>Premier probl\u00e8me : La classe ImageFileViewModel utilise un constructeur static pour g\u00e9rer la liste des Thumbnails.<\/p>\n<h2>Comment faire des tests unitaires avec un constructeur static et comment le r\u00e9initialiser ?<\/h2>\n<p>Sans r\u00e9initialisation, les tests unitaires qui s\u2019ex\u00e9cutent les uns \u00e0 la suite des autres vont se polluer et g\u00e9n\u00e9rer des cas d\u2019erreurs l\u00e0 o\u00f9 il ne devrait pas y en avoir ou pire, s\u2019ex\u00e9cuter avec succ\u00e8s alors qu\u2019ils ne devraient chuter. Le seul moyen est de r\u00e9initialiser l\u2019objet \u00e0 chaque test, sauf que ici, petit probl\u00e8me\u2026 la classe \u00e0 un constructeur static, donc construit \u00e0 la premi\u00e8re rencontre de cette classe par le premier test unitaire.<\/p>\n<p>Un moyen rencontr\u00e9 est de r\u00e9initialiser les attributs static que les objets utilisent et d\u2019utiliser la r\u00e9flexion pour rappeler le constructeur static.<\/p>\n<p>Avec l\u2019attribut \u201c[<strong>TestInitialize<\/strong>]\u201d, nous nous assurons de r\u00e9initialiser un \u00e9tat propre \u00e0 chaque d\u00e9but de tous les tests.<\/p>\n<p>le <strong>ci.Invoke<\/strong> permet d\u2019appeler le constructeur static r\u00e9cup\u00e9r\u00e9 par r\u00e9flexion avec <strong>typeof<\/strong> puis <strong>TypeInitializer<\/strong>.<\/p>\n<p>ImageFileViewModel_Accessor est un objet permettant d\u2019acc\u00e9der aux attributs priv\u00e9s de l\u2019objet ImageFileViewModel. Gr\u00e2ce \u00e0 cet Accessor, nous pouvons r\u00e9initialiser la liste des Thumbnails en la vidant.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:9af6f558-2962-4e9f-87e3-6639f8e5a994\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>[TestInitialize()]\npublic void MyTestInitialize()\n{\n  \/\/ call the static constructor and clear static attribute _ListThumbnails\n  Type staticType = typeof(ImageFileViewModel);\n  ConstructorInfo ci = staticType.TypeInitializer;\n  object[] parameters = new object[0];\n  ci.Invoke(null, parameters);\n  ImageFileViewModel_Accessor._listThumbnail.Clear();\n}<\/pre>\n<\/div>\n<p>Avec ce m\u00e9canisme chaque objet de type ImageFileViewModel est remis \u00e0 vide avant de commencer un nouveau test.<\/p>\n<p>Un autre probl\u00e8me va concerner les tests unitaires sur les comportements multi-Thread avec WPF.<\/p>\n<h2>Comment tester les comportements Multi-Thread WPF avec des tests unitaires?<\/h2>\n<p>Le probl\u00e8me se pose pour tester la propri\u00e9t\u00e9 \u201cThumbnail\u201d.<\/p>\n<p>Je rappelle ici le code \u00e0 tester<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:a96fa81d-b39d-4be0-9f0f-b2bb0ddfd10b\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>public ImageSource Thumbnail\n{\n  get\n  {\n      if (_thumbnail == null)\n      {\n          LoadImage();\n          return null;\n      }\n      return _thumbnail;\n  }\n}<\/pre>\n<\/div>\n<p>Nous devons tester, apr\u00e8s la fin du traitement dans le thread, que la liste des _listThumbnail est vide et que la propri\u00e9t\u00e9 Thumbnail n\u2019est pas null.<\/p>\n<p>Pour attendre le d\u00e9roulement d\u2019un Dispatcher, la classe <strong>DispatcherFrame<\/strong> va venir \u00e0 notre secours.<\/p>\n<h3>La classe DispatcherFrame<\/h3>\n<p><strong>DispatcherFrame<\/strong> avec le <strong>Dispatcher<\/strong> poss\u00e8dent une action d\u2019attente du Dispatcher.CurrentDispatcher<\/p>\n<p>\u201c<strong>Dispatcher.PushFrame(frame)<\/strong>\u201d\u00a0 va attendre jusqu\u2019\u00e0 ce que l\u2019objet frame de type DispatcherFrame initialise la propri\u00e9t\u00e9 \u201c<strong>Continue = false<\/strong>\u201d.<\/p>\n<p>Continue = false dans notre cas devrait se d\u00e9rouler lorsque la propri\u00e9t\u00e9 Thumbnail indique \u00e0 WPF qu\u2019elle contient une information et que la View WPF doit se rafraichir, c\u2019est \u00e0 dire au moment ou \u201c<strong>OnPropertyChanged(\u00ab\u00a0Thumbnail\u00a0\u00bb);\u201d<\/strong> est appel\u00e9 par une des m\u00e9thodes lanc\u00e9es \u00e0 la suite de LoadImage.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:1e9a69eb-0bc7-4c75-aff6-f013405c2215\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>PropertyChangedEventHandler waitForModelHandler = new PropertyChangedEventHandler(\ndelegate(object sender, PropertyChangedEventArgs e)\n{\n    if (e.PropertyName == \"Thumbnail\")\n    {\n        frame.Continue = false;\n    }\n    });\ntempimageFileViewModel.PropertyChanged += waitForModelHandler;<\/pre>\n<\/div>\n<p>Pour ajouter frame.Continue \u00e0 ce moment, nous utilisons un <strong>EventHandler de changement de propri\u00e9t\u00e9 PropertyChangedEventHandler <\/strong>et nous initialisons frame.Continue = false si la propri\u00e9t\u00e9 \u201cThumbnail\u201d est notifi\u00e9e d\u2019un changement.<\/p>\n<p>Le test unitaire ressemble \u00e0 \u00e7a maintenant.<\/p>\n<p>Nous utilisons un fichier jpg pour initialiser le fichier image \u00e0 transformer en vignette.<\/p>\n<div id=\"scid:57F11A72-B0E5-49c7-9094-E3A15BD5B5E6:4101b6a2-bbba-49e0-ac57-182929108ac4\" class=\"wlWriterEditableSmartContent\" style=\"margin: 0px;padding: 0px;float: none\">\n<pre>[TestMethod()]\n[DeploymentItem(@\"test01.jpg\")]\npublic void ThumbnailTest()\n{\n  IImageFile imageFile = new ImageFile();\n  (imageFile as ImageFile).FileName \n    = Path.Combine(Directory.GetCurrentDirectory(), @\"test01.jpg\");\n\n  ImageFileViewModel tempimageFileViewModel = new ImageFileViewModel(imageFile);\n  PrivateObject param0 = new PrivateObject(tempimageFileViewModel);\n  ImageFileViewModel_Accessor imageFileViewModel \n    = new ImageFileViewModel_Accessor(param0);\n  \/\/ check no items in _listThumbnail\n  Assert.AreEqual(0, ImageFileViewModel_Accessor._listThumbnail.Count);\n  \/\/ Implement DispatcherFrame for waiting and testing Dispatcher WPF process\n  DispatcherFrame frame = new DispatcherFrame();\n  PropertyChangedEventHandler waitForModelHandler = new PropertyChangedEventHandler(\n      delegate(object sender, PropertyChangedEventArgs e)\n      {\n          if (e.PropertyName == \"Thumbnail\")\n          {\n              frame.Continue = false;\n          }\n      });\n  tempimageFileViewModel.PropertyChanged += waitForModelHandler;\n  \/\/ Launch Thumbnail process            \n  var thumb = tempimageFileViewModel.Thumbnail;\n  \/\/ Waiting frame.continue=false\n  Dispatcher.PushFrame(frame);\n  \/\/ Check thumbnail not null\n  Assert.AreNotEqual(imageFileViewModel._thumbnail, null);\n  \/\/ Check _listThumbnail empty\n  Assert.AreEqual(0, ImageFileViewModel_Accessor._listThumbnail.Count);            \n}<\/pre>\n<\/div>\n<p>Notez l\u2019attribut \u201c<strong>[DeploymentItem(@\u00a0\u00bbtest01.jpg\u00a0\u00bb)]<\/strong>\u201cqui permet d\u2019utiliser le fichier copi\u00e9 test01.jpg au moment du test unitaire. Pensez \u00e9galement , dans ce cas, \u00e0 modifier la propri\u00e9t\u00e9 du fichier dans la solution Visual Studio pour indiquer de toujours copier ce fichier dans le r\u00e9pertoire de destination.<\/p>\n<h2>Exemple des tests unitaires<\/h2>\n<p>Il ne reste plus qu\u2019\u00e0 ex\u00e9cuter l\u2019ensemble des tests pour terminer<\/p>\n<p><a href=\"http:\/\/www.bratched.com\/images\/stories\/tmpA3DC.png\"><img decoding=\"async\" loading=\"lazy\" title=\"Unit test results\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-tmpa3dc-thumb-png.png\" alt=\"Unit test results\" width=\"599\" height=\"240\" border=\"0\" \/><\/a><\/p>\n<p>Tout fonctionne \u00e0 merveille<\/p>\n<p><a href=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileylol-gif.gif\"><img decoding=\"async\" loading=\"lazy\" title=\"Smileylol\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-smileylol-thumb-gif.gif\" alt=\"Smileylol\" width=\"32\" height=\"32\" border=\"0\" \/><\/a><\/p>\n<h1>Conclusion<\/h1>\n<p>Nous venons de voir comment impl\u00e9menter une ListView avec une interface multi-Thread en WPF, la gestion des images et des vignettes EXIF, la virtualisation et les m\u00e9canismes d\u2019attente en WPF.<\/p>\n<p>Tout cela dans une impl\u00e9mentation par couche M-V-VM qui nous permet de r\u00e9aliser les tests unitaires sans se soucier de la partie visible XAML\/WPF.<\/p>\n<p>Dans les prochaines parties, nous verrons comment g\u00e9rer les images en cache, le Drag &amp; Drop avec notre ListView et comment lui apporter un look TreeView.<\/p>\n<p>Voici le lien pour t\u00e9l\u00e9charger la solution avec le code source du projet et les tests unitaires<\/p>\n<p><a title=\"download http:\/\/www.bratched.com\/download\/WPFListView02.zip\" href=\"http:\/\/www.bratched.com\/download\/WPFListView02.zip\"><img decoding=\"async\" loading=\"lazy\" title=\"download32x32\" src=\"http:\/\/dev.bratched.fr\/fr\/wp-content\/uploads\/sites\/2\/importedmedia\/blogmedia-download32x32-b64acb5051597c4460f5796cc7975efe-gif.gif\" alt=\"download32x32\" width=\"32\" height=\"32\" border=\"0\" \/><\/a> <a title=\"Download http:\/\/www.bratched.com\/download\/WPFListView02.zip\" href=\"\/download\/WPFListView02.zip\">wpfListView02.zip<\/a><\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Vu pr\u00e9c\u00e9demment Nous avons vu dans la premi\u00e8re partie comment impl\u00e9menter une liste de fichiers en WPF avec le pattern M-V-VM. Nous allons voir dans cette partie comment ajouter les vignettes images \u201cThumbnails\u201d repr\u00e9sentant les images miniatures de ces fichiers. L\u2019exemple utilis\u00e9 reprend l\u2019application pr\u00e9c\u00e9dente (sans les vignettes) qui sera compl\u00e9t\u00e9e au fur et [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[9,2,10,11,12,3,4,13,14,15],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/posts\/448"}],"collection":[{"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/comments?post=448"}],"version-history":[{"count":0,"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/posts\/448\/revisions"}],"wp:attachment":[{"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/media?parent=448"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/categories?post=448"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bratched.com\/fr\/wp-json\/wp\/v2\/tags?post=448"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}