Charger des scripts dans une vue partielle

Standard

En ASP.NET MVC,  il arrive que l’on veuille écrire ou référencer des scripts depuis une vue partielle, afin de déléguer l’entière responsabilité de certains scripts et de leur fonctionnement à ce bout de vue uniquement. Par exemple, une vue partielle destinée à de la conversation instantanée, à du « chat », présentera tous les éléments UI nécessaires à l’envoi et à la réception de messages instantanés si la personne est authentifiée et s’occupera également de s’assurer que les bons scripts, tels que SignalR par exemple, ont été chargés et exécutés en vue du bon fonctionnement du chat.

Ces scripts nécessitent souvent que d’autres scripts aient été chargés au préalable, comme JQuery par exemple. Malheureusement, une vue partielle se situe souvent en milieu de document et les scripts de base, eux, souvent juste avant la balise de fermeture du body, afin de ne pas bloquer le rendu de la page et de conserver de bonnes performances. Sachant qu’ils se chargent et s’exécutent dans l’ordre de leur référencement, les scripts situés dans la vue partielle ne fonctionneront pas car les dépendances n’auront pas encore été chargées.

Ce problème se résout facilement dans une vue basique car il est possible de redéfinir une section en ASP.NET MVC : le développeur peut redéfinir la section « Scripts » qui se situerait après les scripts de base de fin de body. Mais dans une vue partielle, il est impossible de redéfinir une section. Ainsi, un script présent dans une vue partielle nécessitant JQuery risque de planter en disant que $ est « undefined ».

En effet, la structure de la page se posera de manière problématique ainsi :

 schema page

Par exemple, il aura un _Layout.cshtml ainsi :


<!DOCTYPE html>
<html lang="en">
<head>
    @Styles.Render("~/Content/css")
</head>
<body>
    <div id="body">
        @RenderSection("featured", required: false)
        <section class="content-wrapper main-content clear-fix">
            @RenderBody()
        </section>
    </div>
    @Scripts.Render("~/bundles/jquery")
    @RenderSection("scripts", required: false)
</body>
</html>

Les vues non partielles utilisant pour layout la vue ci-dessus apparaîtront naturellement au niveau de l’instruction @RenderBody et elles pourront tout à fait définir la section « scripts » et ajouter leurs scripts à la fin du document html. Ainsi des scripts nécessitant jQuery fonctionneront. Par contre, si une vue Index a pour layout _Layout.cshtml et contient une vue partielle, « _Chat », ancrée de la sorte :

@{
    ViewBag.Title = "Home Page";
}
@section featured {
    <section class="featured">
        <div class="content-wrapper">
            <p>
                To learn more about ASP.NET MVC visit
                <a href="http://asp.net/mvc" title="ASP.NET MVC Website">http://asp.net/mvc</a>.
            </p>
        </div>
    </section>
}
@Html.Partial("_Chat")
<h3>We suggest the following:</h3>
(…)

Alors la vue partielle, “_Chat”, ne peut pas contenir de script qui nécessite que les scripts de fin de document, comme jquery, soient chargés. En effet, ne pouvant redéfinir de section, elle sera écrite au milieu du document, dans le flux de la page mère, elle-même écrite au niveau du @RenderBody de « _Layout.cshtml », et donc bien avant le chargement des scripts jquery. Le fameux petit dollar sera inconnu au moment où il est appelé par la vue partielle.

Comment résoudre ce besoin ? Je veux que ma vue partielle contienne des scripts qui n’ont de sens que dans sa portée, par exemple, des scripts contenant les fonctionnalités de discussion instantanée, des scripts un peu lourds, dont elle seule a la responsabilité du chargement.

Il suffit alors de mettre en place un petit mécanisme de callbacks qui se chargera d’enregistrer des méthodes au moment du chargement de nos vues partielles et de programmer leur exécution. On pourrait alors référencer dans le head de nos pages le script suivant :

(function () {
    var finalCallbacks = [];
    
    window.scriptManager.pushEndCallback = function (callback) {
        finalCallbacks.push(callback);
    };

    window.scriptManager.executeEndCallbacks = function () {
        $(finalCallbacks).each(function() {
             this();
        });
        finalCallbacks.length = 0;
    };
})();


Les vues partielles nécessitant l’exécution de scripts dépendant des scripts de fin de page pourraient alors procéder ainsi :

<script type="text/javascript">
    scriptManager.pushEndCallback(function () {
              //do my stuff
    });
</script>

Enfin, juste avant la balise fermante de body, toutes les méthodes enregistrées seraient exécutées, après que les scripts nécessaires aient été chargés :

<body>
(…)
@RenderSection("scripts", required: false)
    <script type="text/javascript">
        scriptManager.executeEndCallbacks();
    </script>
</body>

Ainsi, il serait possible de contourner le problème de ces vues partielles qui ne peuvent redéfinir de section et qui voudraient charger et exécuter des scripts possédant des dépendances sur les scripts référencés en fin de page, comme JQuery par exemple.

De plus, grâce à la méthode JQuery $.getScript il devient possible de charger certains scripts en maîtrisant exactement le moment de leur chargement. Ainsi, dans le cas d’une vue partielle de chat :

<script type="text/javascript">
    scriptManager.pushEndCallback(function () {
        $.getScript("/Scripts/jquery.signalR-2.1.1.js", function (a, e, o) {
            (…)
});
       
    });    
</script>

Mais alors, me direz-vous, comme ça, on perd l’avantage des bundles apparus en ASP.NET MVC 4 ? Eh bien malheureusement oui… sauf si… vous utilisez l’ingénieuse solution de M. Léonard Labat qui consiste à appeler en Ajax une action de type web api qui servira simplement le contenu d’un bundle. Elle consiste tout d’abord en un html helper qui permet d’obtenir les urls des différents scripts qui composent le bundle. Ce helper passe par la méthode Scripts.RenderFormat qui permet d’obtenir les urls, en remplaçant le tag par défaut “<script src=\”{0}\”></script>” par un simple {0} afin d’obtenir uniquement l’url :

public static class BundleExtensions
    {
        public static MvcHtmlString BundlePaths(this HtmlHelper htmlHelper, params string[] bundles)
        {
            var paths = Scripts.RenderFormat("{0}", bundles);

            var javascriptSerializer = new JavaScriptSerializer();

            var bundlePaths = paths.ToString()
                                   .Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries)
                                   .ToArray();

            return new MvcHtmlString(javascriptSerializer.Serialize(bundlePaths));
        }
    }

Il sérialise par la suite les différents urls obtenus en JSON, afin que la collection d’urls puisse être bien interprétée du côté client :

<script type="text/javascript">
    scriptManager.pushEndCallback(function () {
        var bundlePaths = JSON.parse('@Html.BundlePaths("~/bundles/signalR")');
 //exemple, une array et en index 0: "/Scripts/jquery.signalR-2.1.1.js"

//Le script construit ensuite un ensemble de promises 
//qui vont charger les urls des scripts reçus. 
        var promises = $.map(bundlePaths, function (url) {
            return $.getScript(url);
        });       

//Une fois toutes exécutées (when.apply), pourra s’exécuter 
//une callback (then) qui chargera /signalr/hubs...
 $.when.apply($, promises).then(function () {
            $.ajax({
                url: "/signalr/hubs",
                dataType: "script",
(…)
    });    
</script>


Ainsi, grâce à cette méthode, il est possible de charger des scripts dans une vue partielle qui ont des dépendances sur des scripts chargés en fin de page, tout en profitant du système des bundles d’ASP.NET MVC.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s