Внедрение содержимого в определенные разделы из частичного представления ASP.NET MVC 3 с помощью Razor View Engine - PullRequest
304 голосов
/ 26 сентября 2011

У меня есть этот раздел, определенный в моем _Layout.cshtml

@RenderSection("Scripts", false)

Я могу легко использовать его из вида:

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

То, с чем я борюсь, это как получитьНекоторое содержимое вставлено в этот раздел из частичного представления.

Давайте предположим, что это моя страница просмотра:

@section Scripts { 

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

<div>
    poo bar poo
</div>

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

Мне нужно вставить некоторое содержимое в раздел Scripts из _myPartialчастичный вид.

Как я могу это сделать?

Ответы [ 22 ]

223 голосов
/ 26 сентября 2011

Разделы не работают при частичном просмотре, и это по замыслу. Вы можете использовать некоторых пользовательских помощников для достижения аналогичного поведения, но, честно говоря, ответственность за представление необходимых сценариев лежит на ответственности, а не на частичной. Я бы порекомендовал использовать раздел @scripts на главном экране, чтобы не беспокоиться о скриптах по частям.

80 голосов
/ 12 апреля 2013

Это довольно популярный вопрос, поэтому я опубликую свое решение.
У меня была та же проблема, и хотя она не идеальна, я думаю, что она на самом деле работает довольно хорошо и не делает частичную зависимость от представления.
Мой сценарий состоял в том, что действие было доступно само по себе, но также могло быть встроено в представление - карту Google.

По моему _layout у меня есть:

@RenderSection("body_scripts", false)

В моем index виде у меня есть:

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

В моем clients виде у меня есть (все карты и соответствующие HTML):

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

Мое Clients_Scripts представление содержит JavaScript, который будет отображаться на странице

Таким образом, мой сценарий изолирован и может быть отображен на странице там, где требуется, с тегом body_scripts, отображаемым только при первом появлении, когда механизм просмотра бритвой находит его.

Это позволяет мне разделить все - это решение, которое работает для меня вполне неплохо, у других могут быть проблемы с ним, но оно исправляет дыру "по замыслу".

37 голосов
/ 13 сентября 2013

Из решений в этой теме я придумал следующее, вероятно, слишком сложное решение, которое позволяет задерживать рендеринг любого html (и скриптов) внутри блока using.

USAGE

Создание "секции"

  1. Типичный сценарий: В частичном представлении блок включается только один раз, независимо от того, сколько раз это частичное представлениеповторяется на странице:

    @using (Html.Delayed(isOnlyOne: "some unique name for this section")) {
        <script>
            someInlineScript();
        </script>
    }
    
  2. При частичном просмотре включайте блок при каждом использовании частичного:

    @using (Html.Delayed()) {
        <b>show me multiple times, @Model.Whatever</b>
    }
    
  3. В частичном представлении включайте блок только один раз, независимо от того, сколько раз повторяется частичное, но позже визуализируйте его конкретно по имени 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>
    }
    

Render the "разделы "

(то есть отображение отложенного раздела в родительском представлении)

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

КОД

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


}
17 голосов
/ 02 августа 2014

У меня была эта проблема и я использовал эту технику.

Лучшее решение, которое я нашел, очень гибкое.

Также , пожалуйста, проголосуйте здесь , чтобы добавить поддержку кумулятивной декларации раздела

9 голосов
/ 19 октября 2013

Если у вас есть законная необходимость запустить js из partial, вот как вы могли бы это сделать, jQuery требуется:

<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>
8 голосов
/ 26 сентября 2011

Следуя принципу ненавязчивый , для _myPartial совсем не обязательно вводить контент непосредственно в секцию скриптов. Вы можете добавить эти частичные сценарии просмотра в отдельный файл .js и ссылаться на них в разделе @scripts из родительского представления.

5 голосов
/ 18 июня 2013

В наших представлениях о сети есть фундаментальный недостаток, особенно при использовании MVC.Недостаток в том, что JavaScript как-то отвечает за представление.Представление - это представление, JavaScript (поведенческий или иной) - это JavaScript.В шаблонах Silverlight и WPF MVVM мы сталкиваемся с «видом сначала» или «моделью сначала».В MVC мы всегда должны пытаться рассуждать с точки зрения модели, и JavaScript является частью этой модели во многих отношениях.

Я бы предложил использовать шаблон AMD (мне самому нравится RequireJS ).Разделяйте ваш JavaScript в модулях, определяйте свою функциональность и подключайтесь к html из JavaScript вместо того, чтобы полагаться на представление для загрузки JavaScript.Это очистит ваш код, отделит ваши проблемы и облегчит жизнь одним махом.

2 голосов
/ 25 апреля 2016

Вы можете использовать эти методы расширения : (Сохранить как PartialWithScript.cs)

namespace System.Web.Mvc.Html
{
    public static class PartialWithScript
    {
        public static void RenderPartialWithScript(this HtmlHelper htmlHelper, string partialViewName)
        {
            if (htmlHelper.ViewBag.ScriptPartials == null)
            {
                htmlHelper.ViewBag.ScriptPartials = new List<string>();
            }

            if (!htmlHelper.ViewBag.ScriptPartials.Contains(partialViewName))
            {
                htmlHelper.ViewBag.ScriptPartials.Add(partialViewName);
            }

            htmlHelper.ViewBag.ScriptPartialHtml = true;
            htmlHelper.RenderPartial(partialViewName);
        }

        public static void RenderPartialScripts(this HtmlHelper htmlHelper)
        {
            if (htmlHelper.ViewBag.ScriptPartials != null)
            {
                htmlHelper.ViewBag.ScriptPartialHtml = false;
                foreach (string partial in htmlHelper.ViewBag.ScriptPartials)
                {
                    htmlHelper.RenderPartial(partial);
                }
            }
        }
    }
}

Используйте как это:

Пример частичного: (_MyPartial.cshtml) Поместите HTML в if, а js в else.

@if (ViewBag.ScriptPartialHtml ?? true)
    <p>I has htmls</p>
}
else {
    <script type="text/javascript">
        alert('I has javascripts');
    </script>
}

В вашем _Layout.cshtml или там, где вы хотите, чтобы выполнялись сценарии из партиалов, поместите следующее (один раз): он будет отображать только javascript всех партиалов на текущей странице в этом месте.

@{ Html.RenderPartialScripts(); }

Затем, чтобы использовать вашу частичку, просто сделайте это: она отобразит только html в этом месте.

@{Html.RenderPartialWithScript("~/Views/MyController/_MyPartial.cshtml");}
2 голосов
/ 30 мая 2014

Вам не нужно использовать разделы при частичном просмотре.

Включить в частичное представление. Он выполняет функцию после загрузки jQuery. Вы можете изменить условие условия для вашего кода.

<script type="text/javascript">    
var time = setInterval(function () {
    if (window.jQuery != undefined) {
        window.clearInterval(time);

        //Begin
        $(document).ready(function () {
           //....
        });
        //End
    };
}, 10); </script>

Хулио Спейдер

2 голосов
/ 26 сентября 2011

Первое решение, о котором я могу подумать, это использовать ViewBag для хранения значений, которые должны быть отображены.

Во-первых, я никогда не пробовал, если это работа с частичной точки зрения, но это должно имо.

...