I liked the solution posted by @john-w-harding, so I combined it with the answer by @darin-dimitrov to make the following probably overcomplicated solution that lets you delay rendering any html (scripts too) within a using block.
USAGE
In a repeated partial view, only include the block one time:
@using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
<script>
someInlineScript();
</script>
}
In a (repeated?) partial view, include the block for every time the partial is used:
@using (Html.Delayed()) {
<b>show me multiple times, @Model.Whatever</b>
}
In a (repeated?) partial view, include the block once, and later render it specifically by name one-time
:
@using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
<b>show me once by name</b>
<span>@Model.First().Value</span>
}
To render:
@Html.RenderDelayed();
@Html.RenderDelayed("one-time", false);
@Html.RenderDelayed("one-time");
@Html.RenderDelayed("one-time");
CODE
public static class HtmlRenderExtensions {
private class DelayedInjectionBlock : IDisposable {
private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309";
private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;
private const string EMPTY_IDENTIFIER = "";
public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
}
private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
var storage = GetStorage(helper);
return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
}
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;
public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
this.helper = helper;
((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());
this.identifier = identifier ?? EMPTY_IDENTIFIER;
this.isOnlyOne = isOnlyOne;
}
public void Dispose() {
var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();
var queue = GetQueue(this.helper, this.identifier);
var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);
if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
queue.Enqueue(renderedContent);
if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count;
}
}
}
public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
}
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
);
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));
}
}