Utilizzo delle sezioni in Editor / modelli di visualizzazione


104

Voglio mantenere tutto il mio codice JavaScript in una sezione; appena prima del bodytag di chiusura nella mia pagina di layout principale e mi chiedevo solo il meglio per farlo, in stile MVC.

Ad esempio, se creo un DisplayTemplate\DateTime.cshtmlfile che utilizza DateTime Picker dell'interfaccia utente di jQuery, incorporerei JavaScript direttamente in quel modello, ma verrà visualizzato a metà pagina.

Nelle mie visualizzazioni normali posso semplicemente usare @section JavaScript { //js here }e poi @RenderSection("JavaScript", false)nel mio layout principale ma questo non sembra funzionare nei modelli di visualizzazione / editor: qualche idea?


4
per chiunque venga a questo più tardi - c'è un pacchetto nuget per gestirlo
Russ Cam

Risposte:


189

Potresti procedere con una congiunzione di due helper:

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;
    }
}

e poi nel tuo _Layout.cshtml:

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

e da qualche parte in qualche modello:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)

3
Dato che un dizionario non è ordinato, come dovrei fare first in first out? L'ordine di uscita è casuale (presumibilmente a causa del Guid) ..
eth0

Forse potresti impostare un campo intero statico e utilizzare Interlocked.Increment () al posto del GUID per ottenere l'ordinamento, ma anche in questo caso penso che un dizionario non garantisca mai l'ordinamento. Ripensandoci, forse un campo statico è pericoloso in quanto potrebbe essere mantenuto nelle visualizzazioni di pagina. Invece potresti aggiungere un numero intero al dizionario degli elementi, ma dovresti mettere un lucchetto attorno ad esso.
Mark Adamson

Ho iniziato a utilizzare questa soluzione di recente, ma non riesco a inserire due script in una singola riga @ Html.Script (), perché non sono sicuro di come funzioni HelperResult. Non è possibile eseguire 2 blocchi di script in 1 chiamata Html.Script?
Langdon

2
@TimMeers, cosa intendi? Per me tutto questo è sempre stato obsoleto. Non userei affatto quegli aiutanti. Non ho mai avuto la necessità di includere alcuno script nelle mie visualizzazioni parziali. Mi limiterò semplicemente al Razor standard sections. In MVC4 Bundling potrebbe effettivamente essere utilizzato così come aiuta a ridurre la dimensione degli script.
Darin Dimitrov,

4
Questo approccio non funziona se vuoi inserire i tuoi script o stili nel headtag invece che alla fine del bodytag, perché @Html.RenderScripts()verrà eseguito prima della tua visualizzazione parziale e quindi prima @Html.Script().
Maksim Vi.

41

Versione modificata della risposta di Darin per garantire l'ordinazione. Funziona anche con 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);
}

Puoi aggiungere risorse JS e CSS in questo modo:

@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")

E renderizza le risorse JS e CSS in questo modo:

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

Potresti fare un controllo delle stringhe per vedere se inizia con script / link in modo da non dover definire esplicitamente cosa sia ciascuna risorsa.


Grazie eth0. Ho raggiunto un compromesso su questo problema, ma dovrò verificarlo.
one.beat.consumer

Lo so quasi 2 anni fa, ma c'è un modo per verificare se il file css / js esiste già e non renderlo? Grazie
CodingSlayer

1
ok. Non sono sicuro di quanto sia efficiente, ma attualmente lo sto facendo: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Type] as List <Func <object, HelperResult >>; var prevItem = from q in httpTemplates dove q (null) .ToString () == Template (null) .ToString () select q; if (! prevItem.Any ()) {// Add Template}
CodingSlayer

@imAbhi grazie, proprio quello di cui avevo bisogno, sembra un ciclo for-1 di bundle con item.ToString quindi penserei che dovrebbe essere abbastanza veloce
Kunukn

35

Ho affrontato lo stesso problema, ma le soluzioni qui proposte funzionano bene solo per l'aggiunta di riferimenti alla risorsa e non sono molto adatte per il codice JS inline. Ho trovato un articolo molto utile e ho inserito tutto il mio JS in linea (e anche i tag di script) in

@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>
}

E nella vista _Layout posizionata @Html.PageScripts()appena prima di chiudere il tag "body". Funziona come un fascino per me.


Gli aiutanti stessi:

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())));
    }
}

3
questa è la risposta migliore; ti consente anche di iniettare praticamente qualsiasi cosa e di ritardarlo fino alla fine
drzaus

1
Dovresti copiare e incollare il codice dall'articolo nel caso in cui vada giù! Questa è un'ottima risposta!
Shaamaan

Come possiamo farlo in asp.net core
ramanmittal

13

Mi è piaciuta la soluzione pubblicata da @ john-w-harding, quindi l'ho combinata con la risposta di @ darin-dimitrov per creare la seguente soluzione probabilmente troppo complicata che ti consente di ritardare il rendering di qualsiasi html (anche script) all'interno di un blocco using.

USO

In una visualizzazione parziale ripetuta, includere il blocco solo una volta:

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

In una vista parziale (ripetuta?), Includere il blocco ogni volta che viene utilizzato il parziale:

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

In una vista parziale (ripetuta?), Includere il blocco una volta e successivamente renderlo specificatamente per nome one-time:

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

Per eseguire il rendering:

@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

CODICE

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));
    }


}

Strano. Non ricordo di aver copiato la risposta a questo altro thread , ma ho scritto un articolo leggermente migliore lì ...
drzaus

12

Installa il pacchetto nuget Forloop.HtmlHelpers: aggiunge alcuni helper per la gestione degli script nelle viste parziali e nei modelli di editor.

Da qualche parte nel tuo layout, devi chiamare

@Html.RenderScripts()

Questo sarà il punto in cui tutti i file di script e i blocchi di script verranno emessi nella pagina, quindi consiglierei di metterlo dopo gli script principali nel layout e dopo una sezione degli script (se ne hai uno).

Se stai usando The Web Optimization Framework con bundling, puoi usare overload

@Html.RenderScripts(Scripts.Render)

in modo che questo metodo venga utilizzato per scrivere file di script.

Ora, ogni volta che desideri aggiungere file di script o blocchi in una vista, una vista parziale o un modello, usa semplicemente

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

Gli helper assicurano che venga eseguito il rendering di un solo riferimento al file di script se aggiunto più volte e assicurano inoltre che i file di script siano visualizzati nell'ordine previsto, ad es.

  1. disposizione
  2. Parziali e modelli (nell'ordine in cui appaiono nella vista, dall'alto verso il basso)

5

Questo post mi ha davvero aiutato, quindi ho pensato di pubblicare la mia implementazione dell'idea di base. Ho introdotto una funzione di supporto che può restituire tag di script da utilizzare nella funzione @ Html.Resource.

Ho anche aggiunto una semplice classe statica in modo da poter utilizzare variabili digitate per identificare una risorsa JS o 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));
    }
}

E in uso

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

Grazie a @Darin Dimitrov che ha fornito la risposta alla mia domanda qui .


2

La risposta fornita in Populate a Razor Section From a Partial using the RequireScriptHtmlHelper segue lo stesso modello. Ha anche il vantaggio di controllare e sopprimere i riferimenti duplicati allo stesso URL Javascript e ha un priorityparametro esplicito che può essere utilizzato per controllare l'ordinamento.

Ho esteso questa soluzione aggiungendo metodi per:

// 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) { ... }

Tuttavia, mi piacciono le soluzioni di Darin e eth0 poiché utilizzano il HelperResultmodello, che consente blocchi di script e CSS, non solo collegamenti a file Javascript e CSS.


1

Risposte @Darin Dimitrov e @ eth0 da utilizzare con l'utilizzo dell'estensione del bundle:

@Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.