384 Stimmen

Einfügen von Inhalten in bestimmte Abschnitte aus einer Teilansicht ASP.NET MVC 3 mit Razor View Engine

Ich habe diesen Abschnitt in meinem _Layout.cshtml

@RenderSection("Scripts", false)

Ich kann sie leicht aus der Vogelperspektive verwenden:

@section Scripts { 
    @*Stuff comes here*@
}

Was ich kämpfe mit ist, wie man einige Inhalte innerhalb dieses Abschnitts von einer partiellen Ansicht injiziert zu bekommen.

Nehmen wir an, dass dies meine Ansichtsseite ist:

@section Scripts { 

    <script>
        //code comes here
    </script>
}

<div>
    poo bar poo
</div>

<div>
  @Html.Partial("_myPartial")
</div>

Ich muss einige Inhalte in die Scripts Abschnitt von _myPartial Teilansicht.

Wie kann ich das tun?

283voto

Darin Dimitrov Punkte 990883

Abschnitte funktionieren nicht in Teilansichten, und das ist so gewollt. Sie können verwenden einige benutzerdefinierte Hilfsprogramme um ein ähnliches Verhalten zu erreichen, aber ehrlich gesagt liegt es in der Verantwortung der Ansicht, die notwendigen Skripte einzubinden, nicht in der Verantwortung der Teilansicht. Ich würde empfehlen, den @scripts-Abschnitt der Hauptansicht zu verwenden, um das zu tun und nicht die partiellen Sorgen über Skripte zu haben.

97voto

dan richardson Punkte 3761

Diese Frage wird häufig gestellt, daher werde ich meine Lösung hier veröffentlichen.

Ich hatte das gleiche Problem, und obwohl es nicht ideal ist, denke ich, dass es eigentlich ganz gut funktioniert und den Teilbereich nicht von der Ansicht abhängig macht.

Mein Szenario war, dass eine Aktion für sich selbst zugänglich war, aber auch in eine Ansicht - eine Google Map - eingebettet werden konnte.

In meinem _layout Ich habe:

@RenderSection("body_scripts", false)

In meinem index Ansicht, die ich habe:

@Html.Partial("Clients")
@section body_scripts
{
    @Html.Partial("Clients_Scripts")
}

In meinem clients Ansicht, die ich habe (die ganze Karte und assoc. html):

@section body_scripts
{
    @Html.Partial("Clients_Scripts")
}

Meine Clients_Scripts view enthält das Javascript, das auf der Seite gerendert werden soll.

Auf diese Weise ist mein Skript isoliert und kann bei Bedarf in die Seite gerendert werden, wobei die body_scripts Tag nur dann gerendert wird, wenn die Razor View Engine es zum ersten Mal findet.

Das ist eine Lösung, die für mich ganz gut funktioniert, andere haben vielleicht Probleme damit, aber sie flickt das "by design"-Loch.

43voto

drzaus Punkte 22843

Von den Lösungen in dieses Thema Ich habe mir die folgende, wahrscheinlich überkomplizierte Lösung ausgedacht, mit der Sie das Rendern von HTML (auch von Skripten) innerhalb eines using-Blocks verzögern können.

ANWENDUNG

Den "Abschnitt" erstellen

  1. Typisches Szenario: In einer Teilansicht darf der Block nur einmal enthalten sein, egal wie oft die Teilansicht auf der Seite wiederholt wird:

    @using (Html.Delayed(isOnlyOne: "some unique name for this section")) {
        <script>
            someInlineScript();
        </script>
    }
  2. In einer Teilansicht muss der Block jedes Mal, wenn die Teilansicht verwendet wird, eingefügt werden:

    @using (Html.Delayed()) {
        <b>show me multiple times, @Model.Whatever</b>
    }
  3. Fügen Sie den Block in einer Teilansicht nur einmal ein, egal wie oft die Teilansicht wiederholt wird, und geben Sie ihn später speziell nach Namen wieder when-i-call-you :

    @using (Html.Delayed("when-i-call-you", isOnlyOne: "different unique name")) {
        <b>show me once by name</b>
        <span>@Model.First().Value</span>
    }

Rendering der "Abschnitte"

(d. h. Anzeige des verzögerten Abschnitts in einer übergeordneten Ansicht)

@Html.RenderDelayed(); // writes unnamed sections (#1 and #2, excluding #3)
@Html.RenderDelayed("when-i-call-you", false); // writes the specified block, and ignore the `isOnlyOne` setting so we can dump it again
@Html.RenderDelayed("when-i-call-you"); // render the specified block by name
@Html.RenderDelayed("when-i-call-you"); // since it was "popped" in the last call, won't render anything due to `isOnlyOne` provided in `Html.Delayed`

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

}

12voto

Serj Sagan Punkte 26507

Wenn Sie ein berechtigtes Bedürfnis haben, einige js von einer partial können Sie folgendermaßen vorgehen, jQuery erforderlich ist:

<script type="text/javascript">        
    function scriptToExecute()
    {
        //The script you want to execute when page is ready.           
    }

    function runWhenReady()
    {
        if (window.$)
            scriptToExecute();                                   
        else
            setTimeout(runWhenReady, 100);
    }
    runWhenReady();
</script>

12voto

alans Punkte 982

Das Ziel des Auftraggebers ist es, Inline-Skripte in seiner Teilansicht zu definieren, wobei ich davon ausgehe, dass dieses Skript nur für diese Teilansicht spezifisch ist, und diesen Block in seinen Skriptabschnitt aufzunehmen.

Ich verstehe, dass er diese Teilansicht haben möchte, um unabhängig zu sein. Die Idee ist ähnlich wie Komponenten bei der Verwendung von Angular.

Mein Weg wäre, die Skripte einfach in der Teilansicht zu belassen, wie sie ist. Das Problem dabei ist, dass beim Aufruf der Teilansicht das Skript dort vor allen anderen Skripten ausgeführt werden kann (was typischerweise am unteren Ende der Layoutseite hinzugefügt wird). In diesem Fall lässt man das Skript der Teilansicht einfach auf die anderen Skripte warten. Es gibt mehrere Möglichkeiten, dies zu tun. Die einfachste, die ich bereits verwendet habe, ist die Verwendung eines Ereignisses auf body .

In meinem Layout würde ich etwas auf der Unterseite wie dieses haben:

// global scripts
<script src="js/jquery.min.js"></script>
// view scripts
@RenderSection("scripts", false)
// then finally trigger partial view scripts
<script>
  (function(){
    document.querySelector('body').dispatchEvent(new Event('scriptsLoaded'));
  })();
</script>

Dann auf meine Teilansicht (ganz unten):

<script>
  (function(){
    document.querySelector('body').addEventListener('scriptsLoaded', function() {

      // .. do your thing here

    });
  })();
</script>

Eine andere Lösung ist die Verwendung eines Stapels, in den alle Skripte eingefügt werden, und der Aufruf jedes einzelnen am Ende. Andere Lösung, wie bereits erwähnt, ist RequireJS/AMD-Muster, die wirklich gut funktioniert auch.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X