[Silverlight] File Downloader
Comme chacun sait, Microsoft a mis un point d'honneur à la sécurité dans Silverlight, isolant chaque application dans un univers confiné, "sandbox", coupé du monde extérieur. Ainsi à l'heure actuelle, il n'est pas possible pour une application Silverlight d'accéder aux périphériques communément utilisés (webcam, etc.) ou au système de fichier de la machine sur laquelle elle s'execute si ce n'est une petite partie lui étant réservée appelée "Isolated Storage".
Dans ces conditions, il est assez aisé de deviner les problématiques de sécurité tournant autour du téléchargement de fichiers et de constater ainsi l'absence d'un contrôle offrant cette fonctionnalité (contrôle qui devrait exister dans une futur version de Silverlight). Je me suis donc penché sur le problème et je vous expose ici ma solution.
Puisque nous n'avons pas accès au système de fichier, la seule solution s'offrant à nous, pour permettre à un utilisateur de télécharger un fichier où bon lui semble dans son espace de travail, est d'utiliser les fonctionnalités du navigateur. En effet chaque navigateur propose un "SaveFileDialog" pour permettre d'enregistrer du contenu sur un système de fichier. C'est d'ailleur le comportement par défaut lorsque nous fournissons une url correspondant à du contenu non représentable par le navigateur (archive, certains formats d'images, etc.), une boite de dialogue apparaît pour permettre de sélectionner une destination d'enregistrement. C'est sur ce point que nous allons pouvoir jouer : mettre en place une architecture client-serveur permettant de générer une url correspondant à l'emplacement exacte du fichier à télécharger et l'envoyer au navigateur pour faire apparaître sa boite de dialoque de sauvegarde de fichier.
Tout d'abord, prévoyons deux cas. En effet Il est possible, selon le design de l'application, d'avoir accès directement, côté client, à l'url du fichier à télécharger ; auquel cas il suffit d'envoyer directement cette url au navigateur et le tour est joué. Mais il est également possible du côté client, comme c'est souvent le cas, d'avoir accès simplement à une id car cela suffit pour effectuer toutes les tâches relatives au fichier. Le développeur pourrait en effet modifier son code pour stocker l'url du fichier côté client mais rendons notre architecture plus générique afin que l'id du fichier soit suffisante côté client pour pouvoir lancer un téléchargement. Dans ce dernier cas, il faudra envoyer cette id au serveur et résoudre le chemin du fichier en conséquence.
Côté serveur
Dans le cas où l'id du fichier est la seule information existante côté client, la technique pour lancer le téléchargement serait d'ouvrir une nouvelle fenêtre en lui donnant l'url d'un fichier aspx et en fournissant à ce dernier l'id du fichier en paramètre. Ensuite nous pouvons utiliser une couche d'accès aux données ("DataAccessLayer") pour récupérer l'url du fichier associée à l'id récupérée en paramètre de la page. Cette couche utilisera un "DataAccessObject" qui effectuera réellement la résolution du chemin du fichier. Nous allons créer une interface simple qui sera utilisée par notre DataAccessLayer et qui permettra de pouvoir utiliser différents DataAccesObjects, chaque DataAccessObject ayant une technique de résolution propre à la source de donnée (un DataAccesObject pour la résolution à partir d'une base de donnée, un autre pour la résolution à partir d'un fichier XML,etc.).
Voici le code de l'interface qui sera utilisée :
1: public interface IDataAccessObject
2: {
3: string GetFilePath(int fileId);
4: }
Et voici un exemple de DataAccessObject qui ira récupérer le chemin d'un fichier à partir d'une base de donnée :
1: public class BddDataAccessObject : IDataAccessObject
2: {
3: public string GetFilePath(int fileId)
4: {
5: using (TestDataContext dataContext =
6: new TestDataContext())
7: {
8: var query = from p in dataContext.Files
9: where p.Id == fileId
10: select String.Format("{0}/{1}",
11: p.Path, p.Name);
12: return (query.FirstOrDefault());
13: }
14: }
15: }
Notre DataAccessLayer aura au final pour seule fonction de permettre la création de différents types de DataAccessObject en ayant préalablement vérifié par réflection que le type du DataAccessObject voulu implémente bien l'interface précédemment décrite :
1: public class DataAccessLayer
2: {
3: private static DataAccessLayer s_Instance;
4: private static object s_InstanceIdentifier = new object();
5:
6: public static DataAccessLayer Instance
7: {
8: get
9: {
10: lock (s_InstanceIdentifier)
11: {
12: if (s_Instance == null)
13: {
14: s_Instance = new DataAccessLayer();
15: }
16:
17: return (s_Instance);
18: }
19: }
20: }
21:
22: private DataAccessLayer() { }
23:
24: public IDataAccessObject CreateDataAccessObjectInstance(Type dataAccessObject)
25: {
26: if (dataAccessObject.GetInterfaces().FirstOrDefault(type => type == typeof(IDataAccessObject)) != null)
27: return (Activator.CreateInstance(dataAccessObject)
28: as IDataAccessObject);
29: return (null);
30: }
31: }
Et voici le contenu de notre fichier aspx qui va utiliser notre DataAccessLayer pour créer une instance d'un DataAccessObject spécifique et ensuite utiliser ce dernier pour aller récupérer le chemin du fichier à télécharger et faire une simple redirection vers ce chemin :
1: public partial class Gate : System.Web.UI.Page
2: {
3: private const string PARAM_NAME = "FileID";
4:
5: protected void Page_Load(object sender, EventArgs e)
6: {
7: IDataAccessObject dataAccessObject =
8: DataAccessLayer.Instance.CreateDataAccessObjectInstance(typeof(BddDataAccessObject));
9:
10: if (Request.Params[PARAM_NAME] != null && dataAccessObject != null)
11: {
12: int fileId;
13: if (int.TryParse(Request.Params[PARAM_NAME], out fileId))
14: {
15: string filePath = dataAccessObject.GetFilePath(fileId);
16: if (!String.IsNullOrEmpty(filePath))
17: Response.Redirect(filePath);
18: }
19: }
20: }
21: }
Le contrôle
Silverlight fournit un contrôle jouant le rôle de lien hypertexte, le fameux "HyperlinkButton". Il peut être intéressant d'hériter de ce contrôle, en effet une fois l'url du fichier récupéré et enregistré, un clic sur le bouton suffira à ouvrir la boite de dialogue :
1: public class FileDownloader : HyperlinkButton
2: {
3: private const string DEFAULT_GATE_NAME = "Gate.aspx";
4: private const string DEFAULT_GATE_PARAM_NAME = "FileID";
5:
6: private int m_FileID;
7:
8: public string GateName { get; set; }
9:
10: public string GateParamName { get; set; }
11:
12: public int FileId
13: {
14: get { return (this.m_FileID); }
15: set
16: {
17: this.m_FileID = value;
18: this.NavigateUri =
19: new UriBuilder(HtmlPage.Document.DocumentUri.Scheme,
20: HtmlPage.Document.DocumentUri.Host,
21: HtmlPage.Document.DocumentUri.Port,
22: this.GateName,
23: String.Format("?{0}={1}",
24: this.GateParamName,
25: value.ToString())).Uri;
26: }
27: }
28:
29: public string FileUrl
30: {
31: get { return(this.NavigateUri.AbsoluteUri); }
32: set { this.NavigateUri = new Uri(value); }
33: }
34:
35: public FileDownloader()
36: {
37: this.TargetName = "_blank";
38: this.GateName = DEFAULT_GATE_NAME;
39: this.GateParamName = DEFAULT_GATE_PARAM_NAME;
40: }
41: }
Voilà je pense que vous avez compris l'idée. Dans un prochain post, j'étenderais cette architecture pour pouvoir gérer la sélection multiple (création d'une archive côté serveur etc...).
Enjoy !
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :