如何在View中多次使用Razor Section;部分视图(合并)而不覆盖它?

时间:2011-08-16 15:45:50

标签: asp.net-mvc-3 razor

在_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 。我想知道的是,是否有办法将每个调用合并到该部分。

如果我们不能这样做,你有什么建议?

谢谢你的帮助!

更新

我找到了一些解决问题的代码。查看我的answer below

3 个答案:

答案 0 :(得分:7)

这是该问题的解决方案。它来自这个博客: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()

答案 1 :(得分:2)

无法在视图和部分视图之间共享部分。

如果没有类似ScriptManager的解决方案,您可以拥有一组脚本文件(在视图中初始化并存储在HttpContext.ItemsViewData中),部分视图将附加到脚本文件它需要的名字。然后在视图结束时,您将声明一个获取该集合并发出正确脚本标记的部分。

答案 2 :(得分:2)

接受答案的问题是它破坏了输出缓存。解决这个问题的技巧是用你自己的实现覆盖OutputCache属性。遗憾的是,我们无法扩展原始属性,因为它有许多我们需要访问的内部方法。

我实际上使用Donut Output Caching来覆盖OutputCache属性本身。还有其他库也使用自己的OutputCache属性,因此我将解释为使其工作而采取的步骤,以便您可以将它应用于您正在使用的任何一个。

首先,您需要复制现有的OutputCache属性并将其放在应用程序中。您可以通过查看源代码来获取现有属性。

现在将以下属性添加到类中。这是我们存储脚本块的地方,因此我们可以在从缓存中检索时呈现正确的。

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()”。

现在返回到您的自定义OutputCache属性,并仅在从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输出缓存库,使IExtendedOutputCacheManager和OutputCacheManager构造函数公开。

请注意,这是从我的申请中提取的,可能需要一些小的调整。您还应该在页面底部放置WriteScriptBlocks,以便在触发所有子操作后才会调用它。

希望这有帮助。