Accueil > Asp.Net > MasterPage comme ressource embarquée: custom VirtualPathProvider

MasterPage comme ressource embarquée: custom VirtualPathProvider

Dans une discussion sur le forum MSDN, un utilisateur a la problématique suivante: il voudrait pouvoir embarquer des masterpages dans une dll pour pouvoir les partager facilement sans en copier les sources dans les différents projets web de son entreprise. Ce post s’adresse à lui et à tous ceux qui ont une problématique similaire. On se focalise ici sur les masterpage mais tout type de fichier peut subir le même traitement (médias, ascx, aspx…)

Le namespace System.Web.Hosting met à notre disposition une classe VirtualPathProvider. Cette classe permet de définir la manière dont sont mappés des url avec des fichiers physiques ou non. Elle permet en fait d’associer une url à un objet dérivant des classes VirtualFile ou VirtualDirectory le cas échéant. En dérivant les classes VirtualPathProvider et VirtualFile, on peut associer une url à un fichier embarqué comme ressource dans une dll. C’est ce mécanisme que nous allons utiliser pour les masterpages.

Tout d’abord, il vous faudra créer une dll contenant une masterpage et définir son action de génération sur « Ressource incorporée ».
Nous allons ensuite définir une classe MasterVirtualFile comme ceci:

    public class MasterVirtualFile:VirtualFile
    {
        public string Path
        { get; set; }
 
 
        public MasterVirtualFile(string virtualPath)
            : base(virtualPath)
        {
            this.Path = virtualPath;
        }
 
        public override System.IO.Stream Open()
        {
            string resourceFileName = VirtualPathUtility.GetFileName(Path);
            Assembly assembly = Assembly.GetExecutingAssembly();
            return assembly.GetManifestResourceStream(NamespacePrefix + resourceFileName);
        }
 
        public const string NamespacePrefix = "MasterPage.";
    }

Cette classe définit une propriété Path et une constante NamespacePrefix qui nous seront utiles pour définir le chemin vers la ressource dans la méthode surchargée Open().
Cette méthode très simple retourne un flux vers le fichier demandé en allant le chercher dans les ressources embarquées dans l’assembly en cours.

Rentrons maintenant dans le vif du sujet avec la classe MasterPathProvider:

    public class MasterPathProvider:VirtualPathProvider
    {
        public override bool FileExists(string virtualPath)
        {
            if (IsPathVirtual(virtualPath))
            {
                MasterVirtualFile file = (MasterVirtualFile)GetFile(virtualPath);
                return (file == null) ? false : true;
            }
            else
            {
                return Previous.FileExists(virtualPath);
            }
        }
 
        public override VirtualFile GetFile(string virtualPath)
        {
            if (IsPathVirtual(virtualPath))
            {
                return new MasterVirtualFile(virtualPath);
            }
            else
            {
                return Previous.GetFile(virtualPath);
            }
        }
 
        private static bool IsPathVirtual(string virtualPath)
        {
            String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
            return checkPath.StartsWith(MasterPathPrefix, StringComparison.InvariantCultureIgnoreCase);
        }
 
        public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
        {
            if (IsPathVirtual(virtualPath))
            {
                return null;
            }
            else
            {
                System.Collections.Specialized.StringCollection dependencies=new System.Collections.Specialized.StringCollection();
                foreach (string path in virtualPathDependencies)
                {
                    if (!IsPathVirtual(path))
                    {
                        dependencies.Add(path);
                    }
                }
                return Previous.GetCacheDependency(virtualPath, dependencies, utcStart);
            }
 
        }
 
        public const string MasterPathPrefix = "~/VirtualMaster";
    }

Cette classe hérite de VirtualFileProvider et en surcharge 3 méthodes:

  • FileExists: Définit si le fichier demandé existe
  • GetFile: Retourne le fichier demandé
  • GetCacheDependency: Retourne une dépendance de cache sur l’objet demandé

Nous définissons de plus la méthode IsPathVirtual qui définit si le chemin demandé correspond ou non à un des chemins traités par ce PathProvider. On remarque que dans les méthodes FileExists et GetFile, si le chemin n’est pas virtuel, on redonne la main au provider standard grâce à la propriété Previous. En effet, les différents PathProviders sont ajoutés à l’application Web séquentiellement et chacun ne traite que ce qu’il est destiné à traiter ou rend la main au précédent dans le cas contraire.
Ce provider obéit à une règle simple: tous les chemins commençant par MasterPathPrefix sont considérés comme virtuels et les autres non.
La méthode GetFile instancie un objet MasterVirtualFile correspondant au chemin demandé.
La méthode GetCacheDependency est importante car même si on ne définit pas de dépendance de cache ici, il faut gérer ce cas en retournant un null dans le cas d’un chemin virtuel et supprimer les dépendances induites dans les fichiers non virtuels: en clair, une page aspx a une dépendance sur sa master donc même si sa master est virtuelle, elle apparaît dans la liste des dépendances virtualPathDependencies passée comme 2ème argument de la méthode GetCacheDependency. Si on se contente pour les chemins non virtuels de rendre la main au Provider par défaut, il cherchera sur le disque la master pour lui attacher une dépendance de cache… S’en suit une belle erreur vous disant que le fichier xxx.master n’existe pas! Pour remédier à ce problème, il faut gérer les dépendances virtuelles qui apparaissent dans la liste des dépendances passée en paramètre différemment. Comme nous ne définissons pas ici de dépendance de cache, nous nous contentons de retirer les paths virtuels de la liste des dépendances avant de rendre la main au provider par défaut.

Votre DLL est maintenant prête, il est temps de tester la master dans une page. On ajoute donc à la solution un projet web avec une page aspx. On ajoute la DLL précédente aux références du projet Web.
Mais maintenant comment associer le MasterPageProvider à votre projet? Le plus simple est d’enregistrer le provider manuellement dans le global.asax avec l’instruction suivante dans l’évènement Application_Start:

HostingEnvironment.RegisterVirtualPathProvider(new MasterPathProvider());

Le but étant de partager ces ressources, devoir ajouter du code n’est pas très satisfaisant donc pour ma part je me suis tourné vers un HttpModule très simple qui lui est dédié:

    public class MasterPathProviderHttpModule:IHttpModule
    {
        public MasterPage.MasterPathProvider MasterPathProvider
        { get; set; }
 
        public void Dispose()
        {
        }
 
        public void Init(HttpApplication context)
        {
            MasterPathProvider = new MasterPage.MasterPathProvider();
            HostingEnvironment.RegisterVirtualPathProvider(MasterPathProvider);
        }
    }

Il ajoute le provider à la liste à l’initialisation du module tout simplement.
Grâce à ce module, côté site web il ne reste plus qu’à ajouter le module dans le web.config:

<httpModules>
        <add name="EmbeddedMasterModule" type="MasterPage.MasterPathProviderHttpModule, MasterPage" />
</httpModules>

et pour utiliser la master embarquée il faut définir dans la page aspx: MasterPageFile= »~/VirtualMaster/Shared.Master »

Cerise sur le gâteau, non seulement le fichier .master est embarqué mais aussi son code behind. Ce mécanisme peut donc s’appliquer même si de la logique est appliquée côté code dans votre MasterPage.

Le code d’une petite solution de démo est disponible ICI

Enjoy…

Étiquettes : ,
  1. D4rkTiger
    20/01/2011 à 9:18

    Bonjour,

    Je vous remercie de m’aider dans mes recherches. D’après votre tutoriel, c’est exactement ce qu’il me faut pour mes MasterPages et autres UserControl que je souhaite embarquer.

    J’ai voulu reproduire le tutoriel mais je ne sais pas exactement quel type de projet créer : une application Web (site web asp.net) ou bien une bibliothèque de classe ?

  2. 20/01/2011 à 8:26

    Bonjour,
    Il vous faudra les 2 en fait. Vous ne pouvez pas créer d’ascx ou de masterpage dans une dll. Il faut donc créer un site web asp.net et une dll puis créer la master dans le site web et ensuite copier le fichier dans le projet de la dll.

  3. D4rkTiger
    21/01/2011 à 8:32

    Il faut donc que je créé un site web qui va contenir mes pages que je souhaites embarquées. Il faut également que je crée un projet pour la dll du VirtualProvider, laquelle j’inclus dans le premier projet de site web.

    Une fois que tout cela est fait, il faut que je prennes la dll du site web que j’inclus ensuite dans chaque site web que je souhaite qu’il utilise les pages communes. c’est ça ?

  4. 21/01/2011 à 8:44

    Non pas exactement. Le site Web qu’il faut créer ne sert qu’à pouvoir créer le .master. Ensuite vous déplacez physiquement les fichier pour les inclure dans la dll et vous pouvez supprimer le site web il ne sert plus à rien. On est obligés d’en passer par là juste parce que visual studio ne permet pas de créer des .master ou .ascx dans une dll. Ce site web est purement temporaire.
    Pour utiliser les pages embarquées, il suffit d’ajouter une référence à la dll ainsi créée. Regardez dans la solution exemple fournie avec l’article: Vous avez une dll et un site web qui l’utilise mais le fichier master n’apparaît que dans la dll.

  5. D4rkTiger
    21/01/2011 à 11:12

    C’est cool je viens de tester avec mon MasterPage et ça marche nickel. J’ai dû faire quelques modifs pour la première intégration mais là ça marche donc bon, je vais continuer à regarder ça😉

    Sinon j’aurai une question toujours par rapport à la virtualisation. Si j’ai une dépendance dans mon MasterPage à un UserControl que je souhaite embarqué, je dois indiqué une url virtuelle ? comment procéder sinon ?

  6. 21/01/2011 à 8:12

    Je n’ai pas essayé dans ce cas mais je ne vois pas pourquoi ça ne fonctionnerait pas avec des chemins virtuels. De toute manière le VirtualPathProvider correspondant prendra la main. Si ça ne marche pas dis le moi j’y jetterai un œil.

  7. Yoann Blossier (D4rkTiger)
    21/01/2011 à 10:30

    Bonsoir,

    Je viens d’essayer en reprenant votre tutoriel et en l’adaptant pour mon UserControl. J’ai finalement réussi à arriver au résultat que je souhaitais, c’est à dire l’intégration de mon UserControl virtualisé dans le MasterPage virtualisé.

    En effet, il s’agit bien de procéder de la même manière que pour l’intégration du MasterPage dans un site web, c’est à dire simplement en utilisant une URL virtuelle qui va ensuite être interprétée pour le PathProvider associé.

    J’ai cependant d’autres problèmes annexes, tels que :
    – l’accès à des variables dans le fichier Global.asax que je n’arrive pas à retrouver. Ceci surement à cause du fait que le fichier est spécifique à chaque site web.

    Il s’agit de mon plus gros soucis, les autres sont mineurs et peuvent être résolus en réadaptant mon architecture de fichiers.

    Si vous avez une idée de comment accéder au fichier Global.asax, je suis preneur. J’ai pensé à de la rétrospection mais je ne sais pas par où chercher.

    Voilà sinon je vous remercie infiniment de votre aide, je commençais à désespérer de ne pas voir de solution viable dans mon projet. Je me suis enrichi d’une notion vraiment intéressante grâce à vous, et je tiens à vous en remercier grandement.

  8. 21/01/2011 à 10:58

    Quel genre de variables avez-vous dans le global.asax et à quoi servent-elles?
    La plupart des traitements du global.asax peuvent être externalisées dans des modules http ça résoudrait pas mal de problèmes pour vous je pense.

    Merci infiniment pour vos compliments ça prouve que je ne perds pas mon temps en rédigeant ce blog!

  9. Yoann Blossier (D4rkTiger)
    21/01/2011 à 11:55

    Il s’agit essentiellement de DataTable qui stockent les données les moins changeantes comme par exemple la liste des utilisateurs qui n’évoluent que très peu souvent dans l’entreprise.

    Cela permet essentiellement de réduire les accès à la base de données qui fournit ces données.

    Concernant la rédaction de ce blog vous avez raison, vous ne perdez pas votre temps, vous êtes un exemple à suivre😉. J’essayes de tenir un blog aussi mais le temps me manque, je suis débutant dans mon métier et je ne voudrais pas que cela perdure, alors je suis à la recherche de blog comme le vôtre qui me permettent d’avancer, d’apprendre et d’évoluer chaque jour un peu plus pour ne pas rester débutant ^^. Mais je vais arrêter de spammer les commentaires de ce post ^^.

    Pour revenir aux variables, elles sont en effet externalisables, ce que j’ai fait pour la plupart, mais concernant les DataTables, je ne vois pas où je pourrais les mettre.

    Une idée ?

  10. 23/01/2011 à 4:20

    Vous pourriez monter un cache avec ces données soit en utilisant directement le cache d’application soit en utilisant une solution dédiée au caching. J’ai l’habitude pour ma part de travailler avec Enterprise Library 5.0 et le caching application block pour ce genre de scénarii. Vous pouvez même définir une dépendance de cache SQL pour gérer le cas où ces données changeraient. En tout cas il est clair que ces données n’ont rien à faire dans le global.asax.

  11. D4rkTiger
    24/01/2011 à 5:20

    J’ai réussi à déporter mes objets pour libérer le Global.asax de ces contraintes. Par contre, j’essaye de virtualisé également le dossier App_Themes mais là je bloque. Il faut passer par VirtualDirectory ?

    As-tu une idée ?

  12. 25/01/2011 à 7:48

    Désolé je n’ai pas beaucoup de temps en ce moment je passe mes nuits à coder. Dès que j’aurai un peu de temps libre je regarderai ton problème.

  1. No trackbacks yet.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :