I have this section defined in my _Layout.cshtml
@RenderSection(\"Scripts\", false)
I can easily use it from a view:
From the solutions in this thread, I came up with the following probably overcomplicated solution that lets you delay rendering any html (scripts too) within a using block.
Typical scenario: In a partial view, only include the block one time no matter how many times the partial view is repeated in the page:
@using (Html.Delayed(isOnlyOne: "some unique name for this section")) {
}
In a partial view, include the block for every time the partial is used:
@using (Html.Delayed()) {
show me multiple times, @Model.Whatever
}
In a partial view, only include the block once no matter how many times the partial is repeated, but later render it specifically by name when-i-call-you
:
@using (Html.Delayed("when-i-call-you", isOnlyOne: "different unique name")) {
show me once by name
@Model.First().Value
}
(i.e. display the delayed section in a parent view)
@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 {
///
/// Delegate script/resource/etc injection until the end of the page
/// @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/
///
private class DelayedInjectionBlock : IDisposable {
///
/// Unique internal storage key
///
private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";
///
/// Internal storage identifier for remembering unique/isOnlyOne items
///
private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;
///
/// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
///
private const string EMPTY_IDENTIFIER = "";
///
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
///
/// the helper from which we use the context
/// optional unique sub-identifier for a given injection block
/// list of delayed-execution callbacks to render internal content
public static Queue GetQueue(HtmlHelper helper, string identifier = null) {
return _GetOrSet(helper, new Queue(), identifier ?? EMPTY_IDENTIFIER);
}
///
/// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
///
/// the helper from which we use the context
/// 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
/// optional unique sub-identifier for a given injection block
/// list of delayed-execution callbacks to render internal content
private static T _GetOrSet(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));
}
///
/// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
///
///
///
public static Dictionary GetStorage(HtmlHelper helper) {
var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary;
if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary());
return storage;
}
private readonly HtmlHelper helper;
private readonly string identifier;
private readonly string isOnlyOne;
///
/// Create a new using block from the given helper (used for trapping appropriate context)
///
/// the helper from which we use the context
/// optional unique identifier to specify one or many injection blocks
/// 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)
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;
}
///
/// Append the internal content to the context's cached list of output delegates
///
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(), 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)
}
}
}
///
/// Start a delayed-execution block of output -- this will be rendered/printed on the next call to .
///
///
/// Print once in "default block" (usually rendered at end via @Html.RenderDelayed()
). Code:
///
/// @using (Html.Delayed()) {
/// show at later
/// @Model.Name
/// etc
/// }
///
///
///
///
///
/// Print once (i.e. if within a looped partial), using identified block via @Html.RenderDelayed("one-time")
. Code:
///
/// @using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
/// show me once
/// @Model.First().Value
/// }
///
///
///
///
/// the helper from which we use the context
/// optional unique identifier to specify one or many injection blocks
/// 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)
/// using block to wrap delayed output
public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
}
///
/// Render all queued output blocks injected via .
///
///
/// Print all delayed blocks using default identifier (i.e. not provided)
///
/// @using (Html.Delayed()) {
/// show me later
/// @Model.Name
/// etc
/// }
///
/// -- then later --
///
/// @using (Html.Delayed()) {
/// more for later
/// etc
/// }
///
/// -- then later --
///
/// @Html.RenderDelayed() // will print both delayed blocks
///
///
///
///
///
/// Allow multiple repetitions of rendered blocks, using same @Html.Delayed()...
as before. Code:
///
/// @Html.RenderDelayed(removeAfterRendering: false); /* will print */
/// @Html.RenderDelayed() /* will print again because not removed before */
///
///
///
///
/// the helper from which we use the context
/// optional unique identifier to specify one or many injection blocks
/// only render this once
/// rendered output content
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("", 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("", injectionBlockId) +
#endif
string.Join(Environment.NewLine, stack));
}
}