[new:22/06/2010] Le pendant d’un Framework réduit au strict minimum pour être caché dans un plugin est qu’une application Silverlight devient vite un peu grassouillette dès qu’elle commence à faire autre chose qu’afficher deux carrés animés pour une démo… Et qui dit taille qui grossit dit téléchargement long. Et pour une application Web le pire des ennemis, la pire tare qui fait que l’utilisateur zappe avant même d’avoir vu quelque chose c’est bien le temps de chargement, directement lié à la taille de l’application. Les bonnes pratiques (mais aussi le simple bon sens et le sens du commerce) impliquent de réduire la taille des applications pour que le cœur ne soit pas plus gros qu’une image jpeg. Mais le reste ? Il suffit de le télécharger selon les besoins ! Comment ? C’est ce que nous allons voir…
Halte aux applications joufflues !
Comme je le disais en introduction il faut être très attentif à la taille des applications Silverlight qui a tendance à devenir très vite importante dès lors qu’il s’agit d’une vraie application faisant quelque chose d’utile.
Rappelons que Silverlight est avant tout un mode de conception conçu pour le Web. Et que le Web c’est très particulier, c’est un esprit très différent du desktop. J’aimerais insister là dessus, mais ce n’est pas le sujet du billet.
Donc rappelez-vous d’une chose : Silverlight c’est du Web, et pour le Web les applications doivent être légères. Légères graphiquement (séduisantes, aérées), légères fonctionnellement (on ne refait pas une grosse appli de gestion en Silverlight c’est stupide), légères en taille.
Pour conserver une taille raisonnable à une application Silverlight il y a plusieurs choses à respecter en amont (la légèreté évoquée ci-avant) et plusieurs techniques à appliquer en aval.
- Par exemple si l’application utilise des ressources nombreuses ou encombrantes (images, vidéos, musiques…) elles doivent être téléchargées en asynchrone et non pas être placées dans le fichier Xap.
- Autre exemple, ne pas tout faire dans une même application. Si le fonctionnel à couvrir est assez gros, mieux vaut créer des applications différentes qui s’appellent les unes les autres ou sont appelées via un menu fixe.
- On tirera aussi grand avantage à utiliser la mise en cache des assemblages (option qui réclame quelques manipulations que nous verrons dans un prochain billet).
- On peut aussi utiliser MEF et le lazy loading, désormais disponible sous Silverlight. C’est un excellent choix d’architecture, et j’envisage aussi un prochain billet sur le sujet.
- Mais on peut ponctuellement et sans impliquer l’utilisation d’un Framework de type MEF tirer avantage de quelques lignes de codes pour charger, à la demande, des assemblages stockés en divers endroits sur le Web. C’est ce que nous allons voir maintenant.
L’exemple live
Quelques mots sur l’exemple (vous ne pourrez y jouer qu’une fois ou bien il faudra faire un F5 dans le browser pour recharger la page) :
Le but
Nous avons une application principale qui sait faire beaucoup de choses. Tellement de choses qu’il semble plus logique, pour diminuer la taille de l’application principale, de placer les écrans, les modules et autres classes annexes des fichiers qui ne seraient chargés qu’à la demande (de l’application ou de l’utilisateur) et en asynchrone.
La démo
L’application principale est un écran d’accueil avec un bouton permettant d’instancier deux UserControl. Pour simplifier ces derniers ne sont que des images vectorielles (une fraise et un citron repris des exemples de Expression Design 4). Ces UserControl sont placés dans une DLL externe qui ne fait pas partie du Xap de l’application que vous voyez en ce moment (ci-dessous).
Lorsque vous cliquerez sur le bouton ce dernier va disparaitre et un compteur affichant un pourcentage de progression le remplacera (il s’agit juste d’un texte dans une fonte très grande). Arrivé à 100% de chargement une fraise et un citron seront affichés.
Selon l’état du serveur, du réseau et de votre connexion, vous aurez, ou non, le temps de voir défiler le pourcentage d’avancement. Parfois même, si quelque chose ne va pas, la fraise et le citron ne s’afficheront pas, et le pourcentage deviendra le message d’alerte “Failed!” (raté !). Si cela arrive, rafraichissez la page par F5 et tentez une nouvelle fois votre chance… Une application réelle prendrait en charge autrement cette situation, bien entendu.
Maintenant à vous de jouer : cliquez !
[silverlight:source=/SLSamples/SLDyn/SLDynLoad.xap;width=400;height=400]
Bon, on ne joue pas des heures avec ce genre de démo surtout que finalement elle montre le résultat mais pas le “comment”.
Comment ça marche ?
Toute la question est là. Comment s’opère la magie et qu’y gagne-t-on ?
On gagne gros !
Répondons à cette dernière question : L’application chargée par ce billet est un Xap qui fait un peu moins de 5 Ko (5082 octets pour être précis, soit 4,96 Ko). La fraise et le citron étant des vecteurs convertis en PNG avec une résolution très satisfaisante ils ne “rentreraient” pas dans un si petit panier vous vous en doutez… En revanche l’affichage de l’application est instantané. Le lecteur qui ne souhaiterait pas cliquer sur le bouton n’aurait donc téléchargé que 4,96 Ko.
Les deux UserControl qui contiennent les images sont eux placés dans une DLL (une librairie de contrôles Silverlight) qui pèse exactement 65 Ko (66 560 octets).
Le gain est donc évident même sur un exemple aussi petit : au lieu de près de 70 ko, l’application principale est chargée et opérationnelle avec moins de 5 Ko. Soit 7 % seulement de l’application complète, près de 93 % de téléchargement en moins ! C’est gigantesque !
Appliquée sur plusieurs DLL exposant des UserControl (ou des classes, des pages, des dialogues…) beaucoup plus nombreux et beaucoup plus riches, donc plus réels, et même si la “carcasse” (le shell) accueillant tout cela grossirait un peu c’est certain, cette technique permet un gain phénoménal et une amélioration très sensible de l’expérience utilisateur.
La technique
Commençons par créer une nouvelle solution avec un projet de type “Silverlight Class Library”. Cela produira une DLL. A l’intérieur de cette dernière nous allons créer plusieurs classes. Pour l’exemple vu ici il s’agit de deux UserControl affichant chacun une image PNG placée en ressource de la DLL. La première classe s’appelle Strawberry et la seconde Lemon.
C’est fini pour la partie DLL externe … Compilez, en mode Release c’est mieux, prenez la DLL se trouvant dans bin\release et placez là sur votre serveur, dans un répertoire de votre choix. Je passe le fait que ce répertoire doit être accessible en lecture depuis l’extérieur (voir la configuration IIS) et le fait que le serveur doit posséder dans sa racine un fichier ClientAccessPolicy.xml autorisant les applications Silverlight à venir piocher dans le répertoire en question. Vous trouverez un exemple de ce fichier dans les sources du projet de démo.
Ne reste plus qu’à créer le shell, la station d’accueil en quelque sorte.
Dans la même solution, ajoutez cette fois-ci une application Silverlight classique (qui produira un Xap donc). La démo fournie est un simple carré de 400x400 avec un texte en bas et un bouton qui déclenche la partie intéressante. Comment ce bouton est placé, comment il s’efface, comment et quand apparaît le pourcentage d’avancement, tout cela est sans grand intérêt ici et le code source fourni vous en dira plus long que des phrases.
Ce qui compte c’est ce qui ce cache derrière le clic du bouton.
1: var dnl = new WebClient();
2: dnl.OpenReadCompleted += dnl_OpenReadCompleted;
3: dnl.DownloadProgressChanged += dnl_DownloadProgressChanged;
4: dnl.OpenReadAsync(new Uri(
5: "https://www.e-naxos.com/slsamples/sldyn/SLDynShape.dll"),
6: UriKind.Absolute);
D’abord nous créons une instance de WebClient. Ensuite nous programmons deux événements : OpenReadCompleted, le plus important, c’est lui qui sera appelé en fin de téléchargement, et DownloadProgressChanged, celui qui permet d’afficher le pourcentage d’avancement ce qui est purement cosmétique (une application réelle pourrait éviter cet affichage si elle lance le téléchargement “en douce” dès que l’application principale est chargée par exemple, masquant ainsi le temps de chargement).
Enfin, nous appelons la méthode OpenReadAsync en lui passant en paramètre l’Uri complète de la dll. Je publie de bonne grâce le code source complet avec l’adresse réelle de l’exemple, alors pour vos essais, soyez sympa utilisez votre serveur au lieu d’appeler ma dll fraise-citron ! Ma bande passante vous remercie d’avance :-)
L’appel à OpenReadAsync déclenche le téléchargement de la ressource de façon asynchrone, c’est à dire que l’application peut continuer à faire autre chose. Par exemple la démo affiche le pourcentage de progression du téléchargement, ce qui est une activité comme une autre et qui démontre que l’application principale n’est pas bloquée ce qui est très important pour l’expérience utilisateur (la fluidité, éviter les blocages de l’interface sont mêmes des b-a-ba en la matière).
Passons sous silence l’affichage du pourcentage, pour arriver directement au second moment fort : le gestionnaire de l’événement OpenReadCompleted :
1: void dnl_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
2: {
3: txtPercent.Visibility = Visibility.Collapsed;
4: try
5: {
6: AssemblyPart assemblyPart = new AssemblyPart();
7: extLib = assemblyPart.Load(e.Result);
8:
9: var control = (UserControl) extLib.CreateInstance
10: ("SLDynShape.Strawberry");
11: control.HorizontalAlignment = HorizontalAlignment.Left;
12: control.VerticalAlignment = VerticalAlignment.Top;
13: control.Margin = new Thickness(10, 10, 0, 0);
14: gridBase.Children.Add(control);
15:
16: control = (UserControl) extLib.CreateInstance
17: ("SLDynShape.Lemon");
18: control.HorizontalAlignment = HorizontalAlignment.Right;
19: control.VerticalAlignment = VerticalAlignment.Bottom;
20: control.Margin=new Thickness(0,0,10,40);
21: gridBase.Children.Add(control);
22: } catch
23: {
24: txtPercent.Visibility = Visibility.Visible;
25: txtPercent.Text = "Failed!";
26: }
27: }
Les choses importantes se jouent en quelques lignes :
ligne 6 : Nous créons une instance de la classe AssemblyPart.
Ligne 7 : nous initialisons la variable extLib en demandant à l’AssemblyPart de se charger depuis “e.Result” c’est à dire de transformer le tas d’octets qui est arrivé en un assemblage .NET fonctionnel.
La variable extLib est déclarée comme suit :
1: private Assembly extLib;
Dans l’exemple elle pourrait être totalement local à la méthode. Mais la réalité on préfèrera comme cela est montré conserver un pointeur sur l’instance de l’assemblage qui vient d’être chargé. Il sera ainsi possible à tout moment d’y faire référence et d’instancier les classes qu’ils offrent.
Ligne 9 : Nous créons une instance de la fraise en utilisant CreateInstance de l’assemblage extLib et en passant en paramètre le nom complet de la classe désirée.
Le reste n’est que présentation (positionnement du UserControl, ajout à l’arbre visuel, création du citron de la même façon, etc).
Conclusion
Avec trois lignes de code efficace il est donc possible de gagner 93 % du temps de téléchargement d’une application, voire plus. Avec une pointe de ruse en plus on peut même arriver à totalement masquer le chargement si l’utilisateur est occupé par autre chose.
Le chargement étant asynchrone l’application n’est jamais bloquée. En cas de réussite du téléchargement elle peut offrir de nouveaux services, en cas d’échec elle continue à fonctionner normalement (même si certaines fonctions provenant des DLL externes ne sont pas accessibles).
En créant un fichier XML décrivant les DLL externes et leur Uri, fichier qui serait téléchargé en premier par l’application, il est même possible d’ajouter facilement des fonctions à une application Silverlight sans tout recompiler, pendant même que des utilisateurs s’en servent (ceux ayant déjà chargé le XML ne verront les nouveaux modules qu’au prochain accès mais ne seront pas perturbés par la mise à jour).
On voit ici le bénéfice en termes de maintenance, de coût et de temps de celle-ci, qu’elle soit évolutive ou corrective. Avec de l’imagination on peut utiliser le système pour offrir certains modules et pas d’autres selon le profil utilisateur (location de logiciel, achat de fonctions à la demande, etc).
Bref, il s’agit d’une poignée de lignes de code en or.
A vous d’en tirer profit !
Et Stay Tuned !
les sources du projet (VS 2010 ou Blend 4) :