Action

1 post

Utilisation des Images resources avec Windows Phone

Je vous propose un petit retour d’expérience sur l’utilisation des ressources dans le développement Windows Phone dans un modèle M-V-VM.

Différentes façon de coder

L’image que l’on souhaite afficher provient des ressources et devrait pouvoir s’afficher à travers une propriété Image de type BitmapImage dans un ViewModel. A noter que le dans l’exemple ci dessous, UIThreadPool permet d’afficher cette image même lorsque celle-ci est alimentée depuis un Thread qui tourne en tâche de fond.

SynchronizationContext UIThread = SynchronizationContext.Current;

public BitmapImage Image
       {
           get { return _image; }
           set
           {
               UIThread.Post(_ =>
                   {
                       _image = value;
                       RaisePropertyChanged("Image");
                   }, null);
           }
       }

Comment afficher une image incluse dans les ressources de l’application ?

Attention les 2 premières méthodes décrites sont à proscrire et à ne surtout pas reproduire. Allez jusqu’à la fin de l’article pour voir comment implémenter correctement l’affichage d’une image ressource.

Méthode Stream

Lorsque l’on est habitué à récupérer une image d’un flux http ou même de l’isolated Storage on peut être tenté d’uniformiser le code et d’implémenter un Stream pour récupérer l’image stockée dans une application :

public static void LoadResourceImage2(string resourceName, Action<BitmapImage> action)
        {
            var resource = Application.GetResourceStream(new Uri(resourceName, UriKind.Relative));
            {
                if (resource != null)
                {
                    Stream stream = resource.Stream;
                    {
                        if (stream != null)
                        {
                            Deployment.Current.Dispatcher.BeginInvoke(() =>
                            {
                                StreamToBitmapAction(action, stream);
                                stream.Dispose();
                                stream = null;
                            });
                        }
                        else
                            throw new NullReferenceException();
                    }
                };
            }
        }

L’affichage de l’image est retournée par une méthode générique qui prend un Stream en entrée et retourne un BitmapImage en asynchrone dans une Action.

private static void StreamToBitmapAction(Action<BitmapImage> actionBitmap, Stream stream)
        {
            BitmapImage bitmapImage = new BitmapImage();
            bitmapImage.CreateOptions = BitmapCreateOptions.None; 
            bitmapImage.SetSource(stream);
            stream.Dispose();
            if (actionBitmap != null)
                actionBitmap(bitmapImage);

L’utilisation de ce Stream peut ensuite se faire de la façon suivante :

CacheImageService.LoadResourceImage2(resourceName, b => Image = b ) ;

Méthode async et await

Nous pouvons également utiliser async/Await pour rendre le code plus compréhensible et éviter ce meli-mélo d’actions imbriquées. A noter que le code peut également fonctionner avec Windows Phone 7 si vous ajoutez le paquet nuget Mycrosoft Async et que vous utilisez à minima Visual Studio 2012 (donc avec Windows 8). (Remarque : La version Visual Studio 2010 avec WP7 et le paquet Nuget Async semble fonctionner. Cependant l’application se comportement de façon aléatoire avec le async/await donc je vous recommande d’éviter, sauf à vouloir chercher durant des heures pourquoi votre application n’exécute pas à chaque fois le code async/await). La version Visual Studio 2012 et WP7 fonctionne très bien en revanche. Voici donc un exemple d’implémentation possible avec le async await.

public static async Task<BitmapImage> LoadResourceImage1(string resourceName)
        {
            using (var imageStream = await LoadStreamResourceAsync(resourceName))
            {
                return await InitBitmapImageAsync(imageStream);

            }
            return null;
        }

        public static async Task<Stream> LoadStreamResourceAsync(string resourceName)
        {

            return await Task.Factory.StartNew<Stream>(() =>
            {
                if (resourceName == null)
                {
                    throw new ArgumentException("Ressource Name is null");
                }
                Stream stream = null;
                var resource = Application.GetResourceStream(new Uri(resourceName, UriKind.Relative));
                {
                    if (resource != null)
                        stream = resource.Stream;
                }
                return stream;
            }, CancellationToken.None, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
        }

        private static async Task<BitmapImage> InitBitmapImageAsync(Stream imageStream)
        {
            BitmapImage image = null;
            DispatcherSynchronizationContext dsc = new DispatcherSynchronizationContext(Deployment.Current.Dispatcher);
            await Task.Factory.StartNew(() =>
            {
                dsc.Send(_ =>
                {
                    if (imageStream != null)
                    {
                        image = new BitmapImage();
                        image.CreateOptions = BitmapCreateOptions.None;
                        image.SetSource(imageStream);
                    }
                }, null);
            });
            return image;
        }

Méthode new BitmapImage

Enfin, la dernière méthode est déconcertante de simplicité :

 public static void LoadResourceImage3(string resourceName, Action<BitmapImage> action)
        {
        Uri uri = new Uri(resourceName, UriKind.RelativeOrAbsolute);
        Deployment.Current.Dispatcher.BeginInvoke(() =>
            {
                if (action != null)
                    action(new BitmapImage(uri));
            });            
        }

Difficile de faire plus simple Non ? Remarque : le “Deployment.Current.Dispatcher.BeginInvoke” est très important ici pour pouvoir utiliser cette méthode dans un Thread en arrière plan : new BitmapImage nécessite en effet le Thread principal de l’interface utilisateur (le ThreadUI) pour s’exécuter (l’instruction …Dispatcher… est là pour indiquer que l’exécution se fait dans le Thread principal).

Tests de performance

Capture Benchmark affichage ressource Image Pour savoir laquelle de ces 3 méthodes est la plus rapide, j’ai réalisé un petite application Benchmark qui charge 100 images avec la répartition suivante :

  • 50 images png petites (300×300 pour 25ko)
  • 50 images png grandes (1000×1000 pour 437ko)

Voici les résultats moyens observés sur un Nokia Lumia 920

  • Méthode Stream avec async await : 5 secondes !
  • Méthode Stream avec Action : 3,5 secondes
  • Méthode new BitmapImage : 0,435 secondes ! (10 x plus rapide)

Cette petite application permet également de tester en réel les images dans des listes de 100 éléments et ainsi d’apprécier l’expérience utilisateur. La dernière implémentation: “new BitmapImage(uri)”, la plus simple, est à privilégier pour charger des ressources images car elle offre des performances sans équivalent.

Content ou resource ?

On retrouve pas mal d’articles sur le stockage des ressources sous forme content ou resource. Tous les articles que j’ai pu rencontrer recommandent de privilégier le stockage de type « content » à celui de type « resource » dans nos Apps.

Ressources “Content”

Visual Studio Ressources Content Les images stockées avec une “Build Action” = content sont simplement ajoutées au fichier xap de votre application, et de ce fait, ne sont pas stockées dans la dll. Ces images sont accessibles directement par une syntaxe de cette forme à travers une Uri : « Assets/Testimage.png » (où Assets représente le répertoire où elles sont stockées).

Ressources “Resource”

Visual Studio Ressource Resource Les images avec un “Build Action” = Resource sont stockées directement dans la dll. Pour les utiliser, il faudra indiquer le chemin complet de cette dll de la façon suivante : “SampleCacheImageApp;component/Assets/Testimage2_res.png” (où SampleCacheImageApp représente le nom de l’application et /Assets le répertoire) La resource étant contenue dans la dll, il est alors normal que le chargement au démarrage de la dll prenne un tout petit peu plus de temps.

Tests de performance

Les tests de performances sont assez troublants. Une première phase de tests a été réalisée avec l’émulateur qui a nettement favorisé le stockage “Content”. Mais sur un vrai téléphone, les résultats sont totalement différents :

  • Rappel : Méthode new BitmapImage avec les images en » content » : 0,435 secondes !
  • Méthode new BitmapImage avec image en « resource » : 0,049 secondes !!!, soit presque 10x plus rapide.

Benchmark ressources buildaction content vs resource Alors faut-il systématiquement utiliser des ressources en “Build Action = content ” ? Personnellement, je viens de passer toutes mes ressources en “Action Build = Resource” pour toutes les listes nécessitant des centaines de chargements… Si quelqu’un peut me donner une explication à cette différence entre émulateur et matériel? Ce chiffre est-il constaté sur vos téléphones ? Ai-je commis une erreur dans l’écriture des tests ? Est-ce lié au nombre d’applications installées ? Le code source de l’application des tests benchmark se trouvent à cet emplacement : download-10-icon-256