Verwenden von Abschnitten in Editor- / Anzeigevorlagen

104

Ich möchte meinen gesamten JavaScript-Code in einem Abschnitt behalten. kurz vor dem schließenden bodyTag auf meiner Master-Layout-Seite und ich frage mich nur, was ich am besten tun soll, im MVC-Stil.

Wenn ich beispielsweise eine DisplayTemplate\DateTime.cshtmlDatei erstelle , die die DateTime-Auswahl von jQuery UI verwendet, würde ich das JavaScript direkt in diese Vorlage einbetten, aber dann wird es in der Mitte der Seite gerendert.

In meinen normalen Ansichten kann ich nur @section JavaScript { //js here }und dann @RenderSection("JavaScript", false)in meinem Master-Layout verwenden, aber dies scheint in Anzeige- / Editor-Vorlagen nicht zu funktionieren - irgendwelche Ideen?

eth0
quelle
4
Für alle, die später dazu kommen - es gibt ein Nuget-Paket, um dies zu handhaben
Russ Cam

Antworten:

189

Sie können mit einer Verbindung von zwei Helfern fortfahren:

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

und dann in Ihrem _Layout.cshtml:

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

und irgendwo in einer Vorlage:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
quelle
3
Da ein Wörterbuch nicht geordnet ist, wie würde ich es zuerst tun? Die Reihenfolge, die es ausgibt, ist zufällig (vermutlich wegen der Guid).
eth0
Vielleicht könnten Sie ein statisches Ganzzahlfeld einrichten und Interlocked.Increment () anstelle der GUID verwenden, um die Bestellung zu erhalten, aber selbst dann denke ich, dass ein Wörterbuch niemals die Bestellung garantiert. Bei zweiten Überlegungen ist ein statisches Feld möglicherweise zweifelhaft, da es möglicherweise über Seitenanzeigen hinweg beibehalten wird. Stattdessen könnte dem Items-Wörterbuch eine Ganzzahl hinzugefügt werden, aber Sie müssten eine Sperre dafür setzen.
Mark Adamson
Ich habe kürzlich angefangen, diese Lösung zu verwenden, aber ich kann anscheinend nicht zwei Skripte in eine einzelne @ Html.Script () -Zeile füllen, da ich nicht sicher bin, wie HelperResult funktioniert. Ist es nicht möglich, 2 Skriptblöcke in einem Html.Script-Aufruf auszuführen?
Langdon
2
@ TimMeers, was meinst du? Für mich war das alles immer veraltet. Ich würde diese Helfer überhaupt nicht benutzen. Ich hatte nie die Notwendigkeit, Skripte in meine Teilansichten aufzunehmen. Ich würde mich einfach an den Standard-Rasierer halten sections. In MVC4 könnte Bundling tatsächlich verwendet werden und hilft, die Größe von Skripten zu reduzieren.
Darin Dimitrov
4
Dieser Ansatz funktioniert nicht, wenn Sie Ihre Skripte oder Stile im headTag anstatt am Ende des bodyTags platzieren möchten , da er @Html.RenderScripts()vor Ihrer Teilansicht und daher vorher ausgeführt wird @Html.Script().
Maksim Vi.
41

Geänderte Version von Darins Antwort, um die Bestellung sicherzustellen. Funktioniert auch mit 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);
}

Sie können JS- und CSS-Ressourcen wie folgt hinzufügen:

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

Und rendern Sie JS- und CSS-Ressourcen wie folgt:

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

Sie können eine Zeichenfolgenprüfung durchführen, um festzustellen, ob sie mit einem Skript / Link beginnt, damit Sie nicht explizit definieren müssen, um welche Ressource es sich handelt.

eth0
quelle
Danke eth0. Ich habe in dieser Angelegenheit Kompromisse geschlossen, aber ich muss das überprüfen.
one.beat.consumer
Ich weiß das vor fast 2 Jahren, aber gibt es eine Möglichkeit zu überprüfen, ob die CSS / JS-Datei bereits vorhanden ist und sie nicht zu rendern? Danke
CodingSlayer
1
OK. Ich bin mir nicht sicher, wie effizient es ist, aber derzeit mache ich Folgendes: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items [Typ] als Liste <Func <Objekt, HelperResult >>; var prevItem = from q in httpTemplates wobei q (null) .ToString () == Template (null) .ToString () q auswählen; if (! prevItem.Any ()) {// Vorlage
hinzufügen
@imAbhi danke, genau das, was ich brauchte, sieht aus wie eine 1-for-Schleife von Bundles mit item.ToString, also würde ich denken, es sollte schnell genug sein
Kunukn
35

Ich hatte das gleiche Problem, aber die hier vorgeschlagenen Lösungen eignen sich nur zum Hinzufügen von Verweisen auf die Ressource und sind für Inline-JS-Code nicht sehr geeignet. Ich fand einen sehr hilfreichen Artikel und wickelte alle meine Inline-JS (und auch Skript-Tags) ein

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

Und in der _Layout-Ansicht, die @Html.PageScripts()kurz vor dem Schließen des Tags 'body' platziert wurde. Funktioniert wie ein Zauber für mich.


Die Helfer selbst:

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
quelle
3
das ist die beste Antwort; Sie können auch so ziemlich alles injizieren und bis zum Ende verzögern
drzaus
1
Sie sollten den Code aus dem Artikel kopieren und einfügen, falls er jemals ausfällt! Dies ist eine ausgezeichnete Antwort!
Shaamaan
Wie können wir dies in asp.net core
ramanmittal
13

Ich mochte die von @ john-w-harding gepostete Lösung , also kombinierte ich sie mit der Antwort von @ darin-dimitrov, um die folgende wahrscheinlich überkomplizierte Lösung zu erstellen, mit der Sie das Rendern von HTML (auch Skripten) innerhalb eines using-Blocks verzögern können.

VERWENDUNG

Schließen Sie in einer wiederholten Teilansicht den Block nur einmal ein:

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

Fügen Sie in einer (wiederholten?) Teilansicht den Block für jedes Mal ein, wenn der Teil verwendet wird:

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

Fügen Sie in einer (wiederholten?) Teilansicht den Block einmal ein und rendern Sie ihn später spezifisch nach Namen one-time:

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

So rendern Sie:

@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
quelle
Seltsam. Ich erinnere mich nicht, die Antwort auf diesen anderen Thread kopiert zu haben , aber ich habe dort etwas besser geschrieben ...
drzaus
12

Installieren Sie das Nuget- Paket Forloop.HtmlHelpers. Es enthält einige Hilfsprogramme zum Verwalten von Skripten in Teilansichten und Editorvorlagen.

Irgendwo in Ihrem Layout müssen Sie anrufen

@Html.RenderScripts()

Hier werden alle Skriptdateien und Skriptblöcke auf der Seite ausgegeben. Ich würde daher empfehlen, sie nach Ihren Hauptskripten im Layout und nach einem Skriptabschnitt (falls vorhanden) einzufügen.

Wenn Sie das Web Optimization Framework mit Bündelung verwenden, können Sie die Überladung verwenden

@Html.RenderScripts(Scripts.Render)

Damit wird diese Methode zum Schreiben von Skriptdateien verwendet.

Wenn Sie jetzt Skriptdateien oder -blöcke zu einer Ansicht, Teilansicht oder Vorlage hinzufügen möchten, verwenden Sie diese einfach

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

Die Helfer stellen sicher, dass nur eine Skriptdateireferenz gerendert wird, wenn sie mehrmals hinzugefügt wird, und sie stellen auch sicher, dass Skriptdateien in der erwarteten Reihenfolge gerendert werden, d. H.

  1. Layout
  2. Partials und Vorlagen (in der Reihenfolge, in der sie in der Ansicht von oben nach unten angezeigt werden)
Russ Cam
quelle
5

Dieser Beitrag hat mir wirklich geholfen und ich dachte, ich würde meine Umsetzung der Grundidee veröffentlichen. Ich habe eine Hilfsfunktion eingeführt, die Skript-Tags zur Verwendung in der Funktion @ Html.Resource zurückgeben kann.

Ich habe auch eine einfache statische Klasse hinzugefügt, damit ich typisierte Variablen verwenden kann, um eine JS- oder CSS-Ressource zu identifizieren.

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

Und im Einsatz

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

Vielen Dank an @Darin Dimitrov, der die Antwort in meiner Frage hier geliefert hat .

Chris
quelle
2

Die Antwort in „ Rasiermesserabschnitt aus einem Teil mit dem RequireScriptHtmlHelper füllen“ folgt demselben Muster. Es hat auch den Vorteil, dass doppelte Verweise auf dieselbe Javascript-URL gesucht und unterdrückt werden, und es verfügt über einen expliziten priorityParameter, mit dem die Reihenfolge gesteuert werden kann.

Ich habe diese Lösung um Methoden erweitert für:

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

Ich mag die Lösungen von Darin & eth0, da sie die HelperResultVorlage verwenden, die Skript- und CSS-Blöcke ermöglicht, nicht nur Links zu Javascript- und CSS-Dateien.

Martin_W
quelle
1

Antworten von @Darin Dimitrov und @ eth0 zur Verwendung bei der Verwendung der Bundle-Erweiterung:

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