Как использовать Razor Section несколько раз в View & PartialView (слияние), не переопределяя его?

В файле _Layout.cshtml у меня есть раздел внизу тела, который называется "ScriptsContent", объявленный так:

@RenderSection("ScriptsContent", required: false)

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

Посмотреть

@section ScriptsContent
{
    <script type="text/javascript">
        alert(1);
    </script>
}

@Html.Partial("PartialView")

PartialView

@section ScriptsContent
{
    <script type="text/javascript">
        alert(2);
    </script>
}

Результат

Отрисовывается только первый скрипт. Второй скрипт не существует в исходном коде веб-страницы.

Кажется, что Razor выводит только первый @section ScriptsContent, который он видит. То, что я хотел бы знать, есть ли способ объединить каждый вызов в раздел.

Если мы не можем сделать это, что вы предлагаете?

Спасибо тебе за помощь!

ОБНОВИТЬ

Я нашел код, который решает проблему. Смотрите мой ответ ниже.

3 ответа

Решение

Вот решение этой проблемы. Это из этого блога: http://blog.logrythmik.com/post/A-Script-Block-Templated-Delegate-for-Inline-Scripts-in-Razor-Partials.aspx

public static class ViewPageExtensions
{        
    private const string SCRIPTBLOCK_BUILDER = "ScriptBlockBuilder";

    public static MvcHtmlString ScriptBlock(this WebViewPage webPage, Func<dynamic, HelperResult> template)
    {
        if (!webPage.IsAjax)
        {
            var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();

            scriptBuilder.Append(template(null).ToHtmlString());
            webPage.Context.Items[SCRIPTBLOCK_BUILDER] = scriptBuilder;

            return new MvcHtmlString(string.Empty);
        }

        return new MvcHtmlString(template(null).ToHtmlString());
    }

    public static MvcHtmlString WriteScriptBlocks(this WebViewPage webPage)
    {
        var scriptBuilder = webPage.Context.Items[SCRIPTBLOCK_BUILDER] as StringBuilder ?? new StringBuilder();

        return new MvcHtmlString(scriptBuilder.ToString());
    }
}

поэтому в любом месте вашего View или PartialView вы можете использовать это:

@this.ScriptBlock(
    @<script type='text/javascript'>
        alert(1);
    </script>
)

и в вашем _Layout или MasterView используйте это:

@this.WriteScriptBlocks()

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

Я фактически использую Кэширование пончиков, которое перезаписывает сам атрибут Output Cache. Существуют альтернативные библиотеки, которые также используют свой собственный атрибут Output Cache, поэтому я объясню шаги, которые я сделал, чтобы заставить его работать, чтобы вы могли применить его к любому, который вы используете.

Сначала вам нужно скопировать существующий атрибут Output Cache и поместить его в ваше приложение. Вы можете получить существующий атрибут, посмотрев на исходный код.

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

public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();

Теперь внутри метода OnActionExecuting вам нужно сохранить ключ кэша (уникальный идентификатор для кэша вывода) внутри текущей коллекции запросов. Например:

filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;

Теперь измените класс ViewPageExtensions, добавив следующее (заменив CustomOutputCacheAttribute именем вашего атрибута):

var outputCacheKey = webPage.Context.Items["OutputCacheKey"] as string;

if (outputCacheKey != null)
    CustomOutputCacheAttribute.ScriptBlocks.AddOrUpdate(outputCacheKey, new StringBuilder(template(null).ToHtmlString()), (k, sb) => {
        sb.Append(template(null).ToHtmlString());

        return sb;
    });

до:

return new MvcHtmlString(string.Empty);

Примечание. Для небольшого повышения производительности вы также должны убедиться, что вызываете "template(null).ToHtmlString()" только один раз.

Теперь вернитесь к своему пользовательскому атрибуту Output Cache и добавляйте следующее только при извлечении из кэша внутри метода OnActionExecuting:

if (ScriptBlocks.ContainsKey(cacheKey)) {
    var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();

    scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());

    filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
}

Вот окончательный код моего атрибута:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;
using DevTrends.MvcDonutCaching;

public class CustomOutputCacheAttribute : ActionFilterAttribute, IExceptionFilter {
    private readonly IKeyGenerator _keyGenerator;
    private readonly IDonutHoleFiller _donutHoleFiller;
    private readonly IExtendedOutputCacheManager _outputCacheManager;
    private readonly ICacheSettingsManager _cacheSettingsManager;
    private readonly ICacheHeadersHelper _cacheHeadersHelper;

    private bool? _noStore;
    private CacheSettings _cacheSettings;

    public int Duration { get; set; }
    public string VaryByParam { get; set; }
    public string VaryByCustom { get; set; }
    public string CacheProfile { get; set; }
    public OutputCacheLocation Location { get; set; }

    public bool NoStore {
        get { return _noStore ?? false; }
        set { _noStore = value; }
    }

    public static ConcurrentDictionary<string, StringBuilder> ScriptBlocks = new ConcurrentDictionary<string, StringBuilder>();

    public DonutOutputCacheAttribute() {
        var keyBuilder = new KeyBuilder();

        _keyGenerator = new KeyGenerator(keyBuilder);
        _donutHoleFiller = new DonutHoleFiller(new EncryptingActionSettingsSerialiser(new ActionSettingsSerialiser(), new Encryptor()));
        _outputCacheManager = new OutputCacheManager(OutputCache.Instance, keyBuilder);
        _cacheSettingsManager = new CacheSettingsManager();
        _cacheHeadersHelper = new CacheHeadersHelper();

        Duration = -1;
        Location = (OutputCacheLocation)(-1);
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        _cacheSettings = BuildCacheSettings();

        var cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);

        if (_cacheSettings.IsServerCachingEnabled) {
            var cachedItem = _outputCacheManager.GetItem(cacheKey);

            if (cachedItem != null) {
                filterContext.Result = new ContentResult {
                    Content = _donutHoleFiller.ReplaceDonutHoleContent(cachedItem.Content, filterContext),
                    ContentType = cachedItem.ContentType
                };

                if (ScriptBlocks.ContainsKey(cacheKey)) {
                    var scriptBuilder = filterContext.HttpContext.Items["ScriptBlockBuilder"] as StringBuilder ?? new StringBuilder();

                    scriptBuilder.Append(ScriptBlocks[cacheKey].ToString());

                    filterContext.HttpContext.Items["ScriptBlockBuilder"] = scriptBuilder;
                }
            }
        }

        if (filterContext.Result == null) {
            filterContext.HttpContext.Items["OutputCacheKey"] = cacheKey;

            var cachingWriter = new StringWriter(CultureInfo.InvariantCulture);

            var originalWriter = filterContext.HttpContext.Response.Output;

            filterContext.HttpContext.Response.Output = cachingWriter;

            filterContext.HttpContext.Items[cacheKey] = new Action<bool>(hasErrors => {
                filterContext.HttpContext.Items.Remove(cacheKey);

                filterContext.HttpContext.Response.Output = originalWriter;

                if (!hasErrors) {
                    var cacheItem = new CacheItem {
                        Content = cachingWriter.ToString(),
                        ContentType = filterContext.HttpContext.Response.ContentType
                    };

                    filterContext.HttpContext.Response.Write(_donutHoleFiller.RemoveDonutHoleWrappers(cacheItem.Content, filterContext));

                    if (_cacheSettings.IsServerCachingEnabled && filterContext.HttpContext.Response.StatusCode == 200)
                        _outputCacheManager.AddItem(cacheKey, cacheItem, DateTime.UtcNow.AddSeconds(_cacheSettings.Duration));
                }
            });
        }
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext) {
        ExecuteCallback(filterContext, false);

        if (!filterContext.IsChildAction)
            _cacheHeadersHelper.SetCacheHeaders(filterContext.HttpContext.Response, _cacheSettings);
    }

    public void OnException(ExceptionContext filterContext) {
        if (_cacheSettings != null)
            ExecuteCallback(filterContext, true);
    }

    private void ExecuteCallback(ControllerContext context, bool hasErrors) {
        var cacheKey = _keyGenerator.GenerateKey(context, _cacheSettings);

        var callback = context.HttpContext.Items[cacheKey] as Action<bool>;

        if (callback != null)
            callback.Invoke(hasErrors);
    }

    private CacheSettings BuildCacheSettings() {
        CacheSettings cacheSettings;

        if (string.IsNullOrEmpty(CacheProfile)) {
            cacheSettings = new CacheSettings {
                IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally,
                Duration = Duration,
                VaryByCustom = VaryByCustom,
                VaryByParam = VaryByParam,
                Location = (int)Location == -1 ? OutputCacheLocation.Server : Location,
                NoStore = NoStore
            };
        } else {
            var cacheProfile = _cacheSettingsManager.RetrieveOutputCacheProfile(CacheProfile);

            cacheSettings = new CacheSettings {
                IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally && cacheProfile.Enabled,
                Duration = Duration == -1 ? cacheProfile.Duration : Duration,
                VaryByCustom = VaryByCustom ?? cacheProfile.VaryByCustom,
                VaryByParam = VaryByParam ?? cacheProfile.VaryByParam,
                Location = (int)Location == -1 ? ((int)cacheProfile.Location == -1 ? OutputCacheLocation.Server : cacheProfile.Location) : Location,
                NoStore = _noStore.HasValue ? _noStore.Value : cacheProfile.NoStore
            };
        }

        if (cacheSettings.Duration == -1) 
            throw new HttpException("The directive or the configuration settings profile must specify the 'duration' attribute.");

        if (cacheSettings.Duration < 0)
            throw new HttpException("The 'duration' attribute must have a value that is greater than or equal to zero.");

        return cacheSettings;
    }
}

Мне также пришлось изменить библиотеку Donut Output Cache, чтобы сделать IExtendedOutputCacheManager и конструктор Output CacheManager общедоступными.

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

Надеюсь это поможет.

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

При отсутствии решения, подобного ScriptManager, у вас может быть коллекция файлов сценариев (инициализированных на ваш взгляд и сохраненных либо в HttpContext.Items или в ViewData) к которому частичное представление добавит имена файлов скриптов, которые ему требуются. Затем в конце просмотра вы объявите раздел, который извлекает эту коллекцию и выдает правильные теги сценария.

Другие вопросы по тегам