Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Razor sections for Partials. Html.PartialSection() and Html.RenderPartialSection() offer sections accessible to partial pages, with optional de-duplication.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace Helpers
{
/// <summary>Create or Render a <see cref="Helpers.PartialSection"/>s via the current <see cref="HtmlHelper"/></summary>
public static class HtmlPartialSection
{
/// <summary>Create a <see cref="Helpers.PartialSection"/> named <paramref name="sectionName"/>
/// and add <paramref name="content"/> and/or <paramref name="onceOnlyContent"/> and/or <paramref name="scripthrefs"/> to it.
/// <para>Render the partial section in your layout page with <c>Html.RenderPartialSection("<paramref name="sectionName"/>")</c>.</para>
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="sectionName">The unique name for the section. Multiple blocks and scripts for the section will be rendered together.</param>
/// <param name="content">Content to be rendered when <see cref="RenderPartialSection"/> is called on this <paramref name="sectionName"/>
/// Content is rendered in the same order it was added.
/// <paramref name="content"/> is not deduplicated. To add once-only content, use <paramref name="onceOnlyContent"/>
/// </param>
/// <param name="onceOnlyContent">Content to be rendered once and only once when
/// <see cref="RenderPartialSection"/> is called for this <paramref name="sectionName"/>
/// Content is rendered in the same order it was added, but <paramref name="onceOnlyContent"/> is deduplicated: it is rendered when it first appears
/// in the content list.
/// </param>
/// <param name="scripthrefs">zero or more script hrefs. Scripthrefs are de-duplicated before rendering, and then
/// rendered as <c>&lt;script&gt;</c> tags above other section content.</param>
/// <returns>
/// <see cref="MvcHtmlString.Empty"/>.
/// Use <see cref="Helpers.UsePartialSection"/> instead if you want to get a reference to the partial to add further content.
/// </returns>
public static IHtmlString PartialSection(this HtmlHelper htmlHelper, string sectionName,
string content = null, string onceOnlyContent=null, params string[] scripthrefs)
{
var section = EnsurePartialSection(htmlHelper, sectionName);
if (content != null) section.Contents.Add(content);
if (onceOnlyContent != null)
{
section.OnceOnlyContents.Add(onceOnlyContent);
section.Contents.Add(onceOnlyContent);
}
foreach (var href in scripthrefs) section.ScriptHRefs.Add(href);
return MvcHtmlString.Empty;
}
/// <summary>Create a <see cref="Helpers.PartialSection"/> named <paramref name="sectionName"/>.
/// Optionally, add <paramref name="content"/> and/or <paramref name="scripthrefs"/> to it.
/// <para>Render the partial section in your layout page with <c>Html.RenderPartialSection("<paramref name="sectionName"/>")</c>.</para>
/// <para><see cref="Helpers.PartialSection"/> implements <see cref="IDisposable"/> as a convenience for coding styles
/// using <c>@using(var ps=Html.PartialSection()){ ps.Content(); ... }</c>, but does not dispose anything.
/// </para>
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="sectionName">The unique name for the section. Multiple blocks and scripts for the section will be rendered together.</param>
/// <param name="content">Content to be rendered when <see cref="RenderPartialSection"/> is called on this <paramref name="sectionName"/>
/// Content is rendered in the same order it was added.
/// <paramref name="content"/> is not deduplicated. To add once-only content, use <paramref name="onceOnlyContent"/>
/// </param>
/// <param name="onceOnlyContent">Content to be rendered once and only once when
/// <see cref="RenderPartialSection"/> is called on this <paramref name="sectionName"/>
/// Content is rendered in the same order it was added, but <paramref name="onceOnlyContent"/> is deduplicated: it is rendered when it first appears
/// in the content list.
/// </param>
/// <param name="scripthrefs">zero or more script hrefs. Scripthrefs are de-duplicated before rendering, and then
/// rendered as <c>&lt;script&gt;</c> tags above other section content.</param>
/// <returns>
/// The <see cref="Helpers.PartialSection"/>. Use this to add further content.
/// If you do not want to use the return value, use <see cref="PartialSection"/> instead to avoid writing <c>typeof(PartialSection).Name</c> to your markup.
/// </returns>
public static PartialSection UsePartialSection(this HtmlHelper htmlHelper, string sectionName,
string content = null, string onceOnlyContent=null, params string[] scripthrefs)
{
var section = EnsurePartialSection(htmlHelper, sectionName);
if (content != null) section.Contents.Add(content);
if (onceOnlyContent!= null) section.OnceOnlyContents.Add(onceOnlyContent);
foreach (var href in scripthrefs) section.ScriptHRefs.Add(href);
return section;
}
static PartialSection EnsurePartialSection(HtmlHelper htmlHelper, string sectionName)
{
// ReSharper disable once InconsistentlySynchronizedField
var dict =
htmlHelper.ViewContext.HttpContext.Items[KeyPartialSections] as
PartialSectionDictionary;
if (dict == null || !dict.ContainsKey(sectionName))
lock (mutateItemsLocker)
{
dict =
htmlHelper.ViewContext.HttpContext.Items[KeyPartialSections] as
PartialSectionDictionary
?? new PartialSectionDictionary();
htmlHelper.ViewContext.HttpContext.Items[KeyPartialSections] = dict;
if (!dict.ContainsKey(sectionName)) dict[sectionName] = new PartialSection();
}
return dict[sectionName];
}
static readonly object mutateItemsLocker = new object();
static readonly string KeyPartialSections = "ScriptSections" + typeof(PartialSection).GUID;
/// <summary>Renders all content added to the <see cref="Helpers.PartialSection"/> named by <paramref name="sectionName"/>
/// <para>
/// <list type="bullet">
/// <item><description>
/// Script hrefs added by <see cref="ScriptHRef"/> are rendered first, inside <c>&lt;script href="<paramref name="href"/>"&gt;</c> tags.
/// </description></item>
/// <item><description><see cref="PartialSection.Content"/> and <see cref="PartialSection.OnceOnlyContent"/> are rendered next,
/// with <see cref="PartialSection.OnceOnlyContent"/> being de-duplicated as it is rendered.</description></item>
/// </list>
/// </para>
/// </summary>
/// <returns>The rendered content</returns>
public static IHtmlString RenderPartialSection(this HtmlHelper htmlHelper, string sectionName)
{
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext, htmlHelper.RouteCollection);
return EnsurePartialSection(htmlHelper, sectionName).RenderPartialSection(urlHelper);
}
}
/// <summary>
/// A class to provide <c>@section</c> functionality to partials via the <see cref="HtmlHelper"/>.
/// Use <see cref="HtmlPartialSection.PartialSection"/> to create a partial section and add content and script references.
/// Use <see cref="ScriptHRef"/>, <see cref="Content"/> and <see cref="OnceOnlyContent"/> to add further content and script references.
/// Use <see cref="HtmlPartialSection.RenderPartialSection"/> to render the <see cref="PartialSection"/>
/// </summary>
/// <remarks><see cref="PartialSection"/> implements <see cref="IDisposable"/> as a convenience for some coding styles
/// using <c>@using(var ps=Html.PartialSection()){...}</c>, but does not dispose anything.</remarks>
public class PartialSection : IDisposable
{
/// <summary>Renders all content added to this <see cref="PartialSection"/> via
/// <see cref="ScriptHRef"/>, <see cref="Content"/> and <see cref="OnceOnlyContent"/>
/// </summary>
/// <param name="urlHelper">a <see cref="UrlHelper"/> for the current context, used to
/// generate urls for any links added via<see cref="ScriptHRef"/> which start with "~"</param>
/// <returns>The rendered content</returns>
internal IHtmlString RenderPartialSection(UrlHelper urlHelper)
{
if (Contents.Count == 0 && ScriptHRefs.Count == 0) return MvcHtmlString.Empty;
//---
var builder = new StringBuilder()
.AppendLine(string.Join(Environment.NewLine,
ScriptHRefs.Select(
href => "<script src='" + urlHelper.Content(href) + "'></script>")));
lock (renderLocker)
{
var renderedOnce = new HashSet<string>();
foreach (var content in Contents)
{
if (renderedOnce.Contains(content)) continue;
if (OnceOnlyContents.Contains(content)) renderedOnce.Add(content);
builder.AppendLine(content);
}
}
return new MvcHtmlString(builder.ToString());
}
readonly object renderLocker= new object();
/// <summary>Add a scriptHref to the <see cref="PartialSection"/>, to be rendered once-only by
/// <see cref="HtmlHelper"/>.<see cref="HtmlPartialSection.RenderPartialSection"/>, in a
/// <c>&lt;script href="<paramref name="href"/>"&gt;</c> tag.</summary>
/// <param name="href">the href to the script</param>
/// <returns><c>this</c></returns>
public PartialSection ScriptHRef(string href){ ScriptHRefs.Add(href); return this; }
/// <summary>Add content to the <see cref="PartialSection"/>, to be rendered
/// by <see cref="HtmlPartialSection.RenderPartialSection"/></summary>
/// <param name="content"></param>
/// <returns><c>this</c></returns>
public PartialSection Content(string content){ Contents.Add(content); return this;}
/// <summary>Add content to the <see cref="PartialSection"/>, to be rendered once-only by
/// <see cref="HtmlPartialSection.RenderPartialSection"/></summary>
/// <param name="content"></param>
/// <returns><c>this</c></returns>
public PartialSection OnceOnlyContent(string content){ Contents.Add(content); OnceOnlyContents.Add(content); return this;}
internal List<string> Contents { get; } = new List<string>();
internal HashSet<string> OnceOnlyContents { get; } = new HashSet<string>();
internal HashSet<string> ScriptHRefs { get; } = new HashSet<string>();
void IDisposable.Dispose(){}
}
class PartialSectionDictionary : Dictionary<string, PartialSection>{}
}
@chrisfcarroll

This comment has been minimized.

Copy link
Owner Author

commented Nov 6, 2018

@Html.PartialSection( "ScriptFooter", @"
    <script id=""exit-form-validation"">
        $.validate({
            form: this.exitForm,
            validateHiddenInputs: false,
            validateOnBlur: true,
            modules: 'security'
        });
    </script>")

and

@using(var ps = Html.PartialSection("ScriptFooter"))
{
    ps.ScriptHRef("~/scripts/script1.js");
    ps.ScriptHRef("~/scripts/script2.js");
    ps.Content("This contents appears once for every time it is added.");
    ps.Content("This contents appears once for every time it is added.");
    ps.OnceOnlyContent("This will be rendered only once, even if the partial is used multiple times.");
    ps.OnceOnlyContent("This will be rendered only once, even if the partial is used multiple times.");
}

Will together result in:

@Html.RenderPartialSection("ScriptFooter")

rendering:

<script src='/scripts/script1.js'></script>
<script src='/scripts/script2.js'></script>
<script id="exit-form-validation">
    $.validate({
        form: this.exitForm,
        validateHiddenInputs: false,
        validateOnBlur: true,
        modules: 'security'
    });
</script>
This contents appears once for every time it is added.
This will be rendered only once, even if the partial is used multiple times.
This contents appears once for every time it is added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.