Utilisation des sections dans les modèles Editor / Display

104

Je souhaite conserver tout mon code JavaScript dans une seule section; juste avant la fermeturebody balise de dans ma page de mise en page principale et je me demande simplement ce qu'il y a de mieux, style MVC.

Par exemple, si je crée un DisplayTemplate\DateTime.cshtmlfichier qui utilise le sélecteur DateTime de jQuery UI, j'incorporerais le JavaScript directement dans ce modèle, mais il rendra le milieu de la page.

Dans mes vues normales, je peux simplement utiliser @section JavaScript { //js here }puis @RenderSection("JavaScript", false)dans ma mise en page principale, mais cela ne semble pas fonctionner dans les modèles d'affichage / éditeur - des idées?

eth0
la source
4
pour tous ceux qui viendront à cela plus tard - il existe un paquet nuget pour gérer cela: nuget.org/packages/Forloop.HtmlHelpers
Russ Cam

Réponses:

189

Vous pouvez procéder à une conjonction de deux assistants:

public static class HtmlExtensions
{
    public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
    {
        htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
        {
            if (key.ToString().StartsWith("_script_"))
            {
                var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
                if (template != null)
                {
                    htmlHelper.ViewContext.Writer.Write(template(null));
                }
            }
        }
        return MvcHtmlString.Empty;
    }
}

puis dans votre _Layout.cshtml:

<body>
...
@Html.RenderScripts()
</body>

et quelque part dans un modèle:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
la source
3
Comme un dictionnaire n'est pas ordonné, comment puis-je faire le premier entré, premier sorti? L'ordre qu'il produit est aléatoire (probablement à cause du Guid) ..
eth0
Vous pourriez peut-être configurer un champ entier statique et utiliser Interlocked.Increment () à la place du GUID pour obtenir la commande, mais même dans ce cas, je pense qu'un dictionnaire ne garantit jamais la commande. À la réflexion, un champ statique est peut-être douteux car il pourrait être conservé à travers les affichages de page. Au lieu de cela, vous pourriez ajouter un entier au dictionnaire d'éléments, mais vous devrez mettre un verrou autour de celui-ci.
Mark Adamson
J'ai commencé à utiliser cette solution récemment, mais je n'arrive pas à remplir deux scripts dans une seule ligne @ Html.Script (), car je ne suis pas sûr du fonctionnement de HelperResult. N'est-il pas possible de faire 2 blocs de script en 1 appel Html.Script?
Langdon
2
@TimMeers, que voulez-vous dire? Pour moi, tout cela a toujours été obsolète. Je n'utiliserais pas du tout ces aides. Je n'ai jamais eu besoin d'inclure des scripts dans mes vues partielles. Je m'en tiendrai simplement au rasoir standard sections. Dans MVC4, le bundling pourrait en effet être utilisé et il permet de réduire la taille des scripts.
Darin Dimitrov du
4
Cette approche ne fonctionne pas si vous souhaitez placer vos scripts ou styles dans une headbalise plutôt qu'à la fin de la bodybalise, car @Html.RenderScripts()elle sera exécutée avant votre vue partielle et donc avant @Html.Script().
Maksim Vi.
41

Version modifiée de la réponse de Darin pour assurer la commande. Fonctionne également avec CSS:

public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
    else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };

    return new HtmlString(String.Empty);
}

public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
    {
        List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];

        foreach (var Resource in Resources)
        {
            if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
        }
    }

    return new HtmlString(String.Empty);
}

Vous pouvez ajouter des ressources JS et CSS comme ceci:

@Html.Resource(@<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
@Html.Resource(@<link rel="stylesheet" href="@Url.Content("~/CSS/style.css")" />, "css")

Et affichez les ressources JS et CSS comme ceci:

@Html.RenderResources("js")
@Html.RenderResources("css")

Vous pouvez faire une vérification de chaîne pour voir si elle commence par un script / lien afin de ne pas avoir à définir explicitement ce qu'est chaque ressource.

eth0
la source
Merci eth0. J'ai compromis sur ce problème, mais je vais devoir vérifier cela.
one.beat.consumer
Je le sais il y a presque 2 ans, mais y a-t-il un moyen de vérifier si le fichier css / js existe déjà et de ne pas le rendre? Merci
CodingSlayer
1
D'accord. Je ne sais pas à quel point il est efficace, mais actuellement je fais ceci: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = from q dans httpTemplates où q (null) .ToString () == Template (null) .ToString () select q; if (! prevItem.Any ()) {// Ajouter un modèle}
CodingSlayer
@imAbhi merci, juste ce dont j'avais besoin, ressemble à une boucle 1 for de bundles avec item.ToString donc je pense que ça devrait être assez rapide
Kunukn
35

J'ai rencontré le même problème, mais les solutions proposées ici ne fonctionnent bien que pour ajouter une référence à la ressource et ne sont pas très adaptées au code JS en ligne. J'ai trouvé un article très utile et enveloppé tous mes JS en ligne (ainsi que les balises de script) dans

@using (Html.BeginScripts())
{
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
    <script>
    // my inline scripts here
    <\script>
}

Et dans la vue _Layout placée @Html.PageScripts()juste avant la fermeture de la balise 'body'. Fonctionne comme un charme pour moi.


Les aides eux-mêmes:

public static class HtmlHelpers
{
    private class ScriptBlock : IDisposable
    {
        private const string scriptsKey = "scripts";
        public static List<string> pageScripts
        {
            get
            {
                if (HttpContext.Current.Items[scriptsKey] == null)
                    HttpContext.Current.Items[scriptsKey] = new List<string>();
                return (List<string>)HttpContext.Current.Items[scriptsKey];
            }
        }

        WebViewPage webPageBase;

        public ScriptBlock(WebViewPage webPageBase)
        {
            this.webPageBase = webPageBase;
            this.webPageBase.OutputStack.Push(new StringWriter());
        }

        public void Dispose()
        {
            pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
        }
    }

    public static IDisposable BeginScripts(this HtmlHelper helper)
    {
        return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
    }

    public static MvcHtmlString PageScripts(this HtmlHelper helper)
    {
        return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
    }
}
John.W.Harding
la source
3
C'est la meilleure réponse; il vous permet également d'injecter à peu près n'importe quoi et de le retarder jusqu'à la fin
drzaus
1
Vous devez copier-coller le code de l'article au cas où il tomberait jamais! C'est une excellente réponse!
Shaamaan
Comment pouvons-nous faire cela dans le noyau asp.net
ramanmittal
13

J'ai aimé la solution publiée par @ john-w-harding, alors je l'ai combinée avec la réponse de @ darin-dimitrov pour créer la solution probablement trop compliquée suivante qui vous permet de retarder le rendu de tout html (scripts également) dans un bloc d'utilisation.

USAGE

Dans une vue partielle répétée, n'incluez le bloc qu'une seule fois:

@using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
    <script>
        someInlineScript();
    </script>
}

Dans une vue partielle (répétée?), Incluez le bloc à chaque fois que le partiel est utilisé:

@using (Html.Delayed()) {
    <b>show me multiple times, @Model.Whatever</b>
}

Dans une vue partielle (répétée?), Incluez le bloc une seule fois, puis rendez-le spécifiquement par son nom one-time:

@using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    <b>show me once by name</b>
    <span>@Model.First().Value</span>
}

Rendre:

@Html.RenderDelayed(); // the "default" unidentified blocks
@Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
@Html.RenderDelayed("one-time"); // render the specified block by name
@Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything

CODE

public static class HtmlRenderExtensions {

    /// <summary>
    /// Delegate script/resource/etc injection until the end of the page
    /// <para>@via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
    /// </summary>
    private class DelayedInjectionBlock : IDisposable {
        /// <summary>
        /// Unique internal storage key
        /// </summary>
        private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";

        /// <summary>
        /// Internal storage identifier for remembering unique/isOnlyOne items
        /// </summary>
        private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;

        /// <summary>
        /// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
        /// </summary>
        private const string EMPTY_IDENTIFIER = "";

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
            return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
        }

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
            var storage = GetStorage(helper);

            // return the stored item, or set it if it does not exist
            return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
        }

        /// <summary>
        /// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
        /// </summary>
        /// <param name="helper"></param>
        /// <returns></returns>
        public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
            var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
            if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
            return storage;
        }


        private readonly HtmlHelper helper;
        private readonly string identifier;
        private readonly string isOnlyOne;

        /// <summary>
        /// Create a new using block from the given helper (used for trapping appropriate context)
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
        /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
        public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
            this.helper = helper;

            // start a new writing context
            ((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());

            this.identifier = identifier ?? EMPTY_IDENTIFIER;
            this.isOnlyOne = isOnlyOne;
        }

        /// <summary>
        /// Append the internal content to the context's cached list of output delegates
        /// </summary>
        public void Dispose() {
            // render the internal content of the injection block helper
            // make sure to pop from the stack rather than just render from the Writer
            // so it will remove it from regular rendering
            var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
            var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();

            // if we only want one, remove the existing
            var queue = GetQueue(this.helper, this.identifier);

            // get the index of the existing item from the alternate storage
            var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);

            // only save the result if this isn't meant to be unique, or
            // if it's supposed to be unique and we haven't encountered this identifier before
            if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
                // remove the new writing context we created for this block
                // and save the output to the queue for later
                queue.Enqueue(renderedContent);

                // only remember this if supposed to
                if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
            }
        }
    }


    /// <summary>
    /// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
    /// <para>
    /// <example>
    /// Print once in "default block" (usually rendered at end via <code>@Html.RenderDelayed()</code>).  Code:
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show at later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Print once (i.e. if within a looped partial), using identified block via <code>@Html.RenderDelayed("one-time")</code>.  Code:
    /// <code>
    /// @using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    ///     <b>show me once</b>
    ///     <span>@Model.First().Value</span>
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
    /// <returns>using block to wrap delayed output</returns>
    public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
        return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
    }

    /// <summary>
    /// Render all queued output blocks injected via <see cref="Delayed"/>.
    /// <para>
    /// <example>
    /// Print all delayed blocks using default identifier (i.e. not provided)
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show me later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>more for later</b>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @Html.RenderDelayed() // will print both delayed blocks
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Allow multiple repetitions of rendered blocks, using same <code>@Html.Delayed()...</code> as before.  Code:
    /// <code>
    /// @Html.RenderDelayed(removeAfterRendering: false); /* will print */
    /// @Html.RenderDelayed() /* will print again because not removed before */
    /// </code>
    /// </example>
    /// </para>

    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="removeAfterRendering">only render this once</param>
    /// <returns>rendered output content</returns>
    public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
        var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);

        if( removeAfterRendering ) {
            var sb = new StringBuilder(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
                );
            // .count faster than .any
            while (stack.Count > 0) {
                sb.AppendLine(stack.Dequeue());
            }
            return MvcHtmlString.Create(sb.ToString());
        } 

        return MvcHtmlString.Create(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId) + 
#endif
            string.Join(Environment.NewLine, stack));
    }


}
drzaus
la source
Bizarre. Je ne me souviens pas avoir copié la réponse sur cet autre fil de discussion , mais j'ai fait une rédaction légèrement meilleure là-bas ...
drzaus
12

Installez le package nuget Forloop.HtmlHelpers - il ajoute des aides pour la gestion des scripts dans les vues partielles et les modèles d'éditeur.

Quelque part dans votre mise en page, vous devez appeler

@Html.RenderScripts()

Ce sera là où tous les fichiers de script et blocs de script seront affichés dans la page, je vous recommande donc de le placer après vos principaux scripts dans la mise en page et après une section de scripts (si vous en avez une).

Si vous utilisez The Web Optimization Framework avec bundling, vous pouvez utiliser la surcharge

@Html.RenderScripts(Scripts.Render)

afin que cette méthode soit utilisée pour écrire des fichiers de script.

Désormais, à chaque fois que vous souhaitez ajouter des fichiers de script ou des blocs dans une vue, une vue partielle ou un modèle, utilisez simplement

@using (Html.BeginScriptContext())
{
  Html.AddScriptFile("~/Scripts/jquery.validate.js");
  Html.AddScriptBlock(
    @<script type="text/javascript">
       $(function() { $('#someField').datepicker(); });
     </script>
  );
}

Les assistants veillent à ce qu'une seule référence de fichier de script soit rendue si elle est ajoutée plusieurs fois et garantit également que les fichiers de script sont rendus dans un ordre attendu, c'est-à-dire

  1. Disposition
  2. Partiels et modèles (dans l'ordre dans lequel ils apparaissent dans la vue, de haut en bas)
Russ Cam
la source
5

Cet article m'a vraiment aidé, alors j'ai pensé publier ma mise en œuvre de l'idée de base. J'ai introduit une fonction d'assistance qui peut renvoyer des balises de script à utiliser dans la fonction @ Html.Resource.

J'ai également ajouté une classe statique simple afin que je puisse utiliser des variables typées pour identifier une ressource JS ou CSS.

public static class ResourceType
{
    public const string Css = "css";
    public const string Js = "js";
}

public static class HtmlExtensions
{
    public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
        else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };

        return new HtmlString(String.Empty);
    }

    public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
        {
            List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];

            foreach (var resource in resources)
            {
                if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
            }
        }

        return new HtmlString(String.Empty);
    }

    public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
    {
        var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
        var script = new TagBuilder("script");
        script.Attributes["type"] = "text/javascript";
        script.Attributes["src"] = urlHelper.Content("~/" + url);
        return x => new HtmlString(script.ToString(TagRenderMode.Normal));
    }
}

Et en cours d'utilisation

@Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)

Merci à @Darin Dimitrov qui a fourni la réponse en ma question ici .

Chris
la source
2

La réponse donnée dans Remplir une section de rasoir à partir d'un partiel à l'aide de RequireScriptHtmlHelper suit le même modèle. Il a également l'avantage de vérifier et de supprimer les références en double à la même URL Javascript, et il a un explicitepriority paramètre qui peut être utilisé pour contrôler l'ordre.

J'ai étendu cette solution en ajoutant des méthodes pour:

// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }

// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }

J'aime les solutions de Darin & eth0 car elles utilisent le HelperResultmodèle, qui permet des scripts et des blocs CSS, pas seulement des liens vers des fichiers Javascript et CSS.

Martin_W
la source
1

@Darin Dimitrov et @ eth0 réponses à utiliser avec l'utilisation de l'extension de bundle:

@Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")
Erkan
la source