I. Introduction▲
Comme toute techno Web un tant soit peu intéressante, Silverlight permet d'effectuer des requêtes vers un serveur et d'en exploiter les résultats.
Mais, entre l'objet WebClient et WebRequest : un choix s'impose !
Dans cet article, nous allons tenter (humblement) de comparer les différences entre ces deux techniques.
Nous verrons également à travers un exemple concret, le chargement d'un fichier texte et nous tenterons également de charger un assembly dynamiquement.
II. Sources▲
[FTP]Sources : BDV.Exemple.Silverlight.Requests.zip
[HTTP](lien de secours) Sources : BDV.Exemple.Silverlight.Requests.zip
Les sources sont structurées en trois assemblies :
- BDV.Exemple.Silverlight.Requests.Web
Projet Web dans lequel l'application Silverlight est exécutée.
- BDV.Exemple.Silverlight.Requests
Projet qui contient l'application Silverlight (XAP exécuté par l'application Web).
L'écran principal contient trois onglets :
le premier pour charger des données via un WebRequest ;
le second pour charger des données via un WebClient ;
le troisième, qui à son ouverture, charge dynamiquement un assembly.
- BDV.Exemple.Silverlight.DynamicLoadedAssembly
Projet de type Bibliothèque de classes Silverlight. Cet assembly est chargé dynamiquement par l'application Silverlight.
Il contient deux images et une classe.
III. Les requêtes : WebClient ou WebRequest ?▲
WebClient et WebRequest sont deux façons différentes d'aborder les requêtes vers le serveur.
Néanmoins, ils ont en commun deux choses :
- d'une part, les requêtes effectuées sont asynchrones (il n'est pas possible de faire autrement en Silverlight) ;
- d'autre part, l'objet WebClient et l'objet WebRequest utilisent le même cœur en interne : la classe BrowserHttpWebRequest.
III-A. L'objet WebRequest▲
Son utilisation est plus « Web philosophy », plus technique.
À la base, WebRequest est une classe abstraite. Les classes qui en héritent sont :
- HttpWebRequest
- FtpWebRequest
- FileWebRequest
L'unique moyen de disposer d'un objet XXXRequest, est d'appeler la méthode statique WebRequest.Create(…).
C'est cette dernière qui, en fonction de l'URI passée en paramètre, retourne un objet de l'un des trois types cité ci-dessus.
Il est important de savoir qu'en Silverlight 2, seul le type HttpWebRequest est disponible. (quid de SL3 !?).
III-A-1. Instanciation▲
/*
* Dans les sources servant pour l'article,
* méthode initiant la requête
*/
private
void
MakeMyWebRequestCall
(
)
{
HttpWebRequest request;
// Récupération de l'URI courante de l'application Silverlight
String uri =
Application.
Current.
Host.
Source.
AbsoluteUri;
// Pointe l'uri vers la ressource (ici fichier texte, mais cela aurait pu être un WebService)
uri =
uri.
Substring
(
0
,
uri.
IndexOf
(
"/ClientBin"
)) +
"/ClientBin/TextFile.txt"
;
// Création de la request...
request =
WebRequest.
Create
(
new
Uri
(
uri,
UriKind.
Absolute)) as
HttpWebRequest;
// Lance la requête, et pointe la fin de requête vers la méthode WebComplete
request.
BeginGetResponse
(
new
AsyncCallback
(
WebComplete),
request);
}
Dans la syntaxe ci-dessus, voici les points importants à noter.
- L'objet réellement utilisé pour effectuer la requête est de type HttpWebRequest (la conversion est explicite lors du Create).
Pour rappel, l'objet retourné est un HttpWebRequest, car l'Uri passée à la méthode Create(…) pointe indique le protocole http pour la ressource à traiter !
L'objet HttpWebRequest assure que la requête utilisera l'ID de session et le cookie courant.
Ce qui peut être exploitable au niveau du serveur (Exemple : sécurité gérée en ASPX dans l'objet session).
Ce mécanisme (HttpWebRequest) permet d'annuler la requête.
En effet, l'appel de la méthode request.Abort() permet d'arrêter l'opération, en fonction de critères propre à l'application.
- Lors de l'exécution de la requête (appel de BeginGetResponse(… …)),
les paramètres passés sont :
1) la méthode à appeler lorsque l'upload des données est terminé ;
2) la requête elle-même, afin de pouvoir récupérer le flux de données.
III-A-2. Exploitation du résultat▲
///
<
summary
>
/// Methode appelee lorsque l'upload des donnees est termine
///
<
/summary
>
///
<
param
name
=
"a"
><
/param
>
private
void
WebComplete
(
IAsyncResult a)
{
// AsyncState contient le paramètre passé lors de l'appel de BeginGetResponse => l'HttpWebRequest
HttpWebRequest request =
(
HttpWebRequest)a.
AsyncState;
// Provoque l'arrêt de la requête, et la récupération du résultat,
HttpWebResponse res =
(
HttpWebResponse)request.
EndGetResponse
(
a);
string
result;
if
(
res !=
null
)
{
Stream stream =
res.
GetResponseStream
(
);
// Recup du flux lie au resultat,
if
(
stream !=
null
)
{
// Traitement du flux...
using
(
StreamReader reader =
new
StreamReader
(
stream))
{
result =
reader.
ReadToEnd
(
);
}
Dispatcher.
BeginInvoke
(
delegate
(
) // nécessaire car le thread qui exécute la request est != de celui de l'interface graphique
{
this
.
m_lblResult.
Text =
System.
DateTime.
Now.
ToString
(
) +
" "
+
result;
}
);
}
}
}
Dans la syntaxe ci-dessus, voici les points importants à noter.
Tout d'abord, le mécanisme utilisé pour récupérer la réponse :
- le membre AsyncState du paramètre contient la requête (HttpWebRequest) passée en paramètre lors de l'appel de la méthode BeginGetResponse,
ce paramètre était (lors de l'appel) l'objet HttpWebRequest lui même ;
- l'appel de la méthode request.EndGetResponse met fin à la requête, et retourne la réponse. C'est cette dernière qui contient le flux uploadé.
Enfin, la récupération du stream contenu dans la réponse (HttpWebResponse),
qui se manipule comme tout flux classique.
Étant donné que la requête s'exécute de façon asynchrone, le thread utilisé n'est pas le même que celui qui gère l'interface graphique.
Dès lors, pour permettre la modification éventuelle d'un contrôle graphique lorsque la requête a fini son traitement,
il est nécessaire de passer par une invocation de méthode, d'où le Dispatcher.
III-B. L'objet WebClient▲
Après avoir vu le plus compliqué (WebRequest), il est intéressant de se pencher sur le cas WebClient.
Ce dernier encapsule un WebRequest, et simplifie grandement l'exécution des requêtes.
III-B-1. Instanciation▲
///
<
summary
>
/// Methode effectuant la WebClient call
///
<
/summary
>
private
void
MakeMyWebClientCall
(
)
{
WebClient objWebClient;
// Récupération de l'URI courante de l'application Silverlight
String uri =
Application.
Current.
Host.
Source.
AbsoluteUri;
// Pointe l'uri vers la ressource (ici fichier texte, mais cela aurait pu être un WebService)
uri =
uri.
Substring
(
0
,
uri.
IndexOf
(
"/ClientBin"
)) +
"/ClientBin/TextFile.txt"
;
objWebClient =
new
WebClient
(
);
// Abonnement a l'événement de fin de chargement...
objWebClient.
DownloadStringCompleted +=
new
DownloadStringCompletedEventHandler
(
WebClient_DownloadStringCompleted);
// Lance l'exécution du traitement asynchrone...
objWebClient.
DownloadStringAsync
(
new
Uri
(
uri,
UriKind.
Absolute));
}
La syntaxe ci-dessus est « plus simple » dans l'idée…
Il est important de noter les points suivants :
l'instanciation ne nécessite aucune subtilité (simple constructeur)… ;
le mécanisme permettant de traiter la fin de la requête se base sur l'abonnement à l'événement adéquat :
Événement |
Description |
---|---|
DownloadStringCompleted |
Cet événement est à utiliser avec l'appel de la méthode DownloadStringAsync pour exécuter le traitement d'upload de données String (flux Xml, Json, chaines de caractères). |
OpenReadCompleted |
Cet événement est à utiliser avec l'appel de la méthode OpenReadAsync pour exécuter le traitement d'upload de données binaires (fichiers binaires, images…). |
Étant donné que pour notre exemple nous traitons des chaines de caractères stoquées dans un fichier, nous utilisons l'événement DownloadStringCompleted en lien avec l'appel de la méthode DownloadStringAsync pour exécuter le traitement.
III-B-2. Exploitation du résultat▲
///
<
summary
>
/// Methode appelee lorsque l'upload est termine (en WebClient)
///
<
/summary
>
///
<
param
name
=
"sender"
><
/param
>
///
<
param
name
=
"e"
><
/param
>
private
void
WebClient_DownloadStringCompleted
(
object
sender,
DownloadStringCompletedEventArgs e)
{
if
(!
e.
Cancelled &&
e.
Error ==
null
)
this
.
m_lblResultWebClient.
Text =
System.
DateTime.
Now.
ToString
(
) +
" "
+
e.
Result;
}
Ici, pour le traitement des données retournées, nous vérifions qu'il n'y a pas eu d'erreur, et que l'opération n'a pas été annulée par l'utilisateur, ensuite, nous exploitons directement la chaine string dans le contrôle graphique (m_lblResultWebClient est un textblock). Ceci est possible sans invocation via le Dispatcher, car WebClient se mappe sur le Contexte utilisateur principal, et donc est exécuté dans le thread courant (ici celui de l'interface graphique).
III-C. Comparaison WebRequest - WebClient▲
Opération |
Description WebRequest |
Description WebClient |
---|---|---|
Instanciation |
Ne peut s'instancier que via l'appel d'une méthode statique (Pattern Fabrique), à laquelle il faut passer l'Uri de la ressource. |
Sans complexité, un constructeur classique… |
Exécution |
Nécessite de passer en paramètre à la méthode : |
Nécessite de s'abonner à l'événement lié au type de traitement demandé (cfr plus haut pour le lien méthode - événement) |
Exploitation des résultats |
Nécessite un traitement plus complexe du flux |
Juste récupérer la ressource contenue dans le paramètre de type DownloadStringCompletedEventArgs |
En conclusion, il est important de bien comprendre les deux mécanismes.
WebClient est plus simple d'utilisation, mais ne permet pas (facilement) de sous-traiter le comportement dans un autre thread.
Cette fonctionnalité pouvant s'avérer utile dans les cas d'upload de grosses quantités de données vers l'application Silverlight.
Néanmoins, dans la majorité des cas, WebClient suffira.
Donc, c'est à chacun de savoir quelle technique il préfère.
III-D. Remarque sur les requêtes CrossDomain▲
Que l'on soit en Silverlight, en Flash, ou en JavaScript, les requêtes effectuées pour récupérer les données doivent être à destination du même serveur que celui qui héberge l'application Web (Same Origin Policy).
En effet, cela est indispensable afin d'éviter qu'une application Web puisse lancer des attaques par phishing.
D'un autre côté, il arrive qu'il soit nécessaire de permettre à une application Silverlight (ou Flash) d'effectuer des requêtes croisées.
Dans ces cas-là, il est nécessaire de créer un fichier qui décrit les droits à la racine du domaine d'où les requêtes doivent être effectuées (si la ressource à chercher est à http://www.developpez.com/meteo, le fichier devra se situer à http://www.developpez.com).
Ces fichiers peuvent prendre deux syntaxes différentes :
clientaccesspolicy.xml est le standard utilisé pour les applications Flash.
Par compatibilité, les applications Silverlight peuvent utiliser ce fichier. Elles s'exécuteront alors avec les mêmes droits que leurs homologues Flash ;
crossdomain.xml est le standard créé pour les applications Silverlight. Syntaxiquement un peu différent de la version Macromedia, il offre les mêmes fonctionnalités.
Pour la syntaxe contenue dans ces fichiers, je vous redirige vers le lien suivant : MSDN ClientAccessPolicy – CrossDomain.
IV. Exemple : Chargement dynamique d'un assembly▲
IV-A. Introduction▲
Maintenant que vous possédez tout le savoir nécessaire au chargement de données, je vous propose d'aborder un cas particulier : le chargement dynamique d'un assembly.
Mais avant d'aller plus loin, pourquoi pourrait-on avoir besoin de cela ?
À la base, une application Silverlight est contenue dans un fichier XAP (qui n'est qu'un Zip des ressources nécessaires à son exécution). Le problème, c'est que la taille du fichier XAP est proportionnelle à la taille de l'application à afficher (ce qui est logique).
Imaginons le cas d'une entreprise qui fournirait à ses employés un portail comme outil de travail. Ce portail disposerait d'un nombre important de modules.
Nous pourrions rencontrer, par exemple, les outils suivants : gestion des clients, gestion des ressources humaines, gestion des stocks, gestion des livraisons…
Le portail et tous ses modules seraient donc réalisés en Silverlight.
Ajoutons, que notre entreprise dispose de points de vente perdus au fin fond de la campagne européenne (tant qu'à faire un exemple, autant qu'il soit mégalo :) ), et qu'au fond de la campagne européenne, on ne peut assurer l'ADSL… (56ko… si si, cela existe encore, je vous dis.)
Si tous les modules étaient nativement dans le même XAP, cela pourrait engendrer des temps de chargement de l'application absolument …longs… en 56ko.
De plus :
- pourquoi pénaliser un point de vente avec tous les modules alors qu'il n'utilise que certains modules ;
- pourquoi charger tous les modules alors qu'un utilisateur n'a peut-être pas tous les droits d'accès.
Du coup, il est possible de disposer d'un portail minimaliste (XAP), et de ne charger les assembly satellites (qui contiennent les modules) qu'à la demande (lorsque l'utilisateur clique dans le menu).
Pour l'exemple, ce qui nous intéresse, c'est justement le chargement dynamique. Dès lors, nous allons simplement charger dynamiquement une bibliothèque de classe qui contient une image et une classe. Nous afficherons cette image, et instancierons la classe (avec un appel à la méthode ToString de cette classe).
Cet exemple est contenu dans les sources, déclenche le chargement de l'assembly lorsque l'utilisateur ouvre un onglet.
IV-A-1. Implémentation pour récupérer une image▲
Dans ce tuto, nous ne mettrons que les étapes essentielles permettant de réaliser le chargement dynamique d'assembly.
En effet, de nombreux tutos existent déjà sur les bases des projets Silverlight dans Visual Studio.
Pour réaliser cet exemple, créez une solution de type Application Silverlight dans une Application Web (ou une Solution Web).
Dans la solution, ajoutez une Bibliothèque de classes Silverlight, dans cette bibliothèque ajoutez une image JPEG.
Attention, Silverlight supporte entièrement les JPEG et partiellement les PNG.
Pour disposer d'un PNG qui fonctionne dans Silverlight, nous utilisons Paint.NET pour ouvrir l'image (même png) et l'enregistrer/la réenregistrer en png.
Compilez le projet Bibliothèque de classes, et copiez la DLL dans le répertoire ClientBin du projet Web.
Dans l'application silverlight, ajoutez un conteneur, un bouton, et un contrôle Image.
La méthode attachée à l'événement Click du bouton appelle la méthode suivante :
///
<
summary
>
/// Méthode qui charge les données
///
<
/summary
>
private
void
LoadDynamicAssembly
(
)
{
string
uri =
Application.
Current.
Host.
Source.
AbsoluteUri;
int
index =
uri.
IndexOf
(
"/ClientBin"
);
// le nom : votreDll.dll correspond au nom de la dll bibliothèque qui contient l'image
uri =
uri.
Substring
(
0
,
index) +
"/ClientBin/votreDll.dll"
;
WebClient webClient =
new
WebClient
(
);
webClient.
OpenReadCompleted +=
assemblyLoaded_OpenReadCompleted;
// pour les données binaires...
webClient.
OpenReadAsync
(
new
Uri
(
uri));
// Initie le chargement des données binaires
}
Les points importants à noter :
Utilisation de l'événement OpenReadCompleted qui permet de traiter des flux binaires…
Utilisation de la méthode OpenReadAsync pour initier le traitement (car flux binaire)…
Ajoutez la méthode suivante qui sera appelée lors de la fin du traitement de rapatriement de la DLL :
(Méthode attachée à l'événement OpenReadCompleted)
///
<
summary
>
/// Methode qui est responsable de la fin de traitement
///
<
/summary
>
///
<
param
name
=
"sender"
><
/param
>
///
<
param
name
=
"e"
><
/param
>
private
void
assemblyLoaded_OpenReadCompleted
(
object
sender,
OpenReadCompletedEventArgs e)
{
try
{
// Chargement de l'assembly
AssemblyPart ass =
new
AssemblyPart
(
);
ass.
Load
(
e.
Result);
this
.
m_imgLoaded.
Source =
new
System.
Windows.
Media.
Imaging.
BitmapImage
(
new
Uri
(
"BDV.Exemple.Silverlight.DynamicLoadedAssembly;component/tuxCow.png"
,
UriKind.
Relative)
);
if
(
this
.
m_imgLoaded.
Source !=
null
)
this
.
m_assemblyLoaded =
true
;
}
catch
(
Exception)
{
}
}
Les points importants à noter sont :
Utilisation d'un objet AssemblyPart. Cet objet représente un assembly servant à charger dans le XAP actuellement en mémoire.
Dès lors que nous disposons du flux binaire, rien ne nous empêche d'utiliser un AssemblyPart pour charger cet assembly en mémoire dans l'application Silverlight !
Pour récupérer l'image, la syntaxe de l'Uri relative est la suivante :
[assemblyName];component/[RessourcePath]
BDV.Exemple.Silverlight.DynamicLoadedAssembly;component/tuxCow.png
IV-A-2. Implémentation pour l'instanciation d'une classe▲
Pour pouvoir instancier une classe, encore faut-il qu'elle existe dans l'assembly en question !
Donc, ajoutez dans l'assembly Bibliothèque de classes Silverlight une classe, et ajoutez-lui une méthode (ou propriété ou overridez le ToString) qui retourne une chaine de caractères.
Recompilez cet assembly et copiez-le dans le répertoire du projet Web « ClientBin »
(C'est le répertoire de base des applications Silverlight dans les projets Web ASPX).
C'est là qu'il faut être attentif : sinon c'est le drame !
Ajoutez une référence dans le projet d'application Silverlight, qui pointe vers le projet Bibliothèque de classes. (Cela sera nécessaire pour pouvoir utiliser la classe dans l'application).
Cliquez sur la référence, et affichez les propriétés de cette référence…
Pour ce faire, clic droit sur la référence et clic sur Propriétés…
(Raccourcis Visual Studio.NET : sélectionner la référence + F4)
La propertyBox s'affiche…
Modifiez le paramètre « Copie locale » à False.
Et enregistrez le tout (Ctrl+Maj+S).
La copie locale indique juste au compilateur s'il doit inclure, ou non, une copie de l'assembly pointé par la référence dans la sortie par défaut (le fichier XAP, pour une application Silverlight).
En le pointant sur False, nous ne permettons pas à cet assembly de faire partie du package par défaut (XAP).
Si comme moi, vous êtes un « Saint Thomas, je crois ce que je vois », compilez l'application Silverlight, ouvrez le répertoire de génération, utilisez votre outil favori pour dézipper (7zip par exemple) et ouvrez ce fichier… Vous verrez que la bibliothèque de classes n'y est pas !
Enfin, dans votre application Silverlight, ajoutez un bouton et dans le corps de cette méthode, ajoutez (par exemple)
une instanciation, et un appel à la méthode ou propriété ajoutée (celle qui retourne une chaine de caractère) (dans un messagebox par exemple).
BDV.
Exemple.
Silverlight.
DynamicLoadedAssembly.
ClassTest cls =
new
BDV.
Exemple.
Silverlight.
DynamicLoadedAssembly.
ClassTest
(
);
MessageBox.
Show
(
cls.
ToString
(
),
"Instanciation et appel de la méthode ToString()"
,
MessageBoxButton.
OK);
Lancez votre application en debug… et tadaaaa !
Attention, j'attire votre attention sur un dernier point non négligeable.
Lorsque l'on utilise une classe déclarée, mais dont l'assembly n'a pas été chargé. Et que l'on appelle une méthode qui contient une telle situation.
La CLR va générer une exception qui ne sera pas catchable/visible au sein du code qui entraine l'erreur.
Cette exception sera renvoyée directement au gestionnaire d'exceptions de l'application Silverlight.
Pour pouvoir traiter cette exception, il est nécessaire de s'abonner à l'événement Application.UnhandledException (événement claqué pour une telle exception).
L'exception throwée sera de type FileNotFoundException, et le message inclura le nom de l'assembly manquant.
Il est néanmoins possible d'éviter cela, en s'assurant que l'assembly en question a bel et bien été loadé avant l'appel du code concerné.
V. Références▲
MSDN WebRequest
MSDN WebClient
Livre : Pro Silverlight 2 in C# 2008 de Matthew MacDonald aux éditions Apress
Site Web : http://wildermuth.com/2008/09/27/WebClient_vs_WebRequest_in_Silverlight_2
L'icône de la vache utilisée dans les sources provient de http://www.crystalxp.net/ et se nomme : holi cow tux (Licence Creative Commons BY-NC-SA)