Add Styles & Scripts Dynamically to head tag in ASP .NET MVC
Hey,
I wanted to be able to create partial views in asp .net mvc in a way that every partial will declare its own styles and scripts.
So the naive solution is to define a section in the layout, and in the partial view just put everything inside.
The problem is, it's working only for one level deep. What happens if your partial view uses another partial views ?
What happens if your layout is dynamic, and uses partial views as well?
So I created the following solution:
Put this line in your _layout file, right above the </head>
Every style & script will appear in the head section.
How does it work ?
Every view registers in the request http context it's own scripts & styles. The section declaration puts in a unique string as a placeholder in the <head>. After the controller action has executed it filters the actions with ViewResult because the following code isn't relevant to all the other types.
So when a ViewResult is found the Filter stream is replaced, so we can control the actual html that returns to the browser.
Then, we scan the result buffer to look for the </head> and the placeholder.
The search is limited until the </head> is found, because the placeholder must be in the <head>.
When the placeholder is found it is replaced with all the styles & scripts defined.
Good luck!
I wanted to be able to create partial views in asp .net mvc in a way that every partial will declare its own styles and scripts.
So the naive solution is to define a section in the layout, and in the partial view just put everything inside.
The problem is, it's working only for one level deep. What happens if your partial view uses another partial views ?
What happens if your layout is dynamic, and uses partial views as well?
So I created the following solution:
public class DynamicHeaderHandler : ActionFilterAttribute { private static readonly byte[] _headClosingTagBuffer = Encoding.UTF8.GetBytes("</head>"); private static readonly byte[] _placeholderBuffer = Encoding.UTF8.GetBytes(DynamicHeader.SectionPlaceholder); public override void OnActionExecuted(ActionExecutedContext filterContext) { if (filterContext.Result is ViewResult) { filterContext.HttpContext.Response.Filter = new ResponseFilter(filterContext.HttpContext.Response.Filter); } base.OnActionExecuted(filterContext); } private class ResponseFilter : MemoryStream { private readonly Stream _outputStream; public ResponseFilter(Stream outputStream) { _outputStream = outputStream; } private void WriteToOutputStream() { var buffer = this.ToArray(); var index = IndexOfSubArray(buffer, _placeholderBuffer, _headClosingTagBuffer); if (index > -1) { var html = DynamicHeader.GetHtml(); var injectBytes = Encoding.UTF8.GetBytes(html); buffer = buffer.Take(index).Concat(injectBytes).Concat(buffer.Skip(index + _placeholderBuffer.Length)).ToArray(); } _outputStream.Write(buffer, 0, buffer.Length); _outputStream.Flush(); } public override void Close() { WriteToOutputStream(); _outputStream.Close(); base.Close(); } private static int IndexOfSubArray(byte[] aBuffer, byte[] aPlaceHolder, byte[] aHeadTag) { for (var outerIndex = 0; outerIndex < aBuffer.Length; ++outerIndex) { var subInnerIndex = 0; var shouldCheckPlaceHolder = true; var shouldCheckHeadTag = true; for (var innerIndex = outerIndex; innerIndex < aBuffer.Length && (shouldCheckHeadTag || shouldCheckPlaceHolder); ++subInnerIndex, ++innerIndex) { if (shouldCheckPlaceHolder && subInnerIndex < aPlaceHolder.Length) { shouldCheckPlaceHolder = aBuffer[innerIndex] == aPlaceHolder[subInnerIndex]; } if (shouldCheckHeadTag && subInnerIndex < aHeadTag.Length) { shouldCheckHeadTag = aBuffer[innerIndex] == aHeadTag[subInnerIndex]; } if (shouldCheckPlaceHolder && subInnerIndex == aPlaceHolder.Length - 1) { return outerIndex; } if (shouldCheckHeadTag && subInnerIndex == aHeadTag.Length - 1) { return -1; } } } return -1; } } }Register this action filter in the GlobalFilterCollection in your Global.asax file like this:
filters.Add(new DynamicHeaderHandler());Now you're missing the component that registers the styles & scripts:
public enum ResourceType { Layout = 0, Infrastructure = 1, Regular = 2 } public class DynamicHeader { public const string SectionPlaceholder = "_HeaderPlaceholder_"; private const string mSessionInstanceKey = "DynamicHeaderInstance"; private Dictionary<string, ResourceType> mScripts = new Dictionary<string, ResourceType>(); private Dictionary<string, ResourceType> mStyleSheets = new Dictionary<string, ResourceType>(); private static DynamicHeader GetInstance() { var contextItems = HttpContext.Current.Items; var instance = contextItems[mSessionInstanceKey] as DynamicHeader; if (instance == null) { contextItems[mSessionInstanceKey] = instance = new DynamicHeader(); } return instance; } public static HtmlString HeaderSection() { return new HtmlString(SectionPlaceholder); } public static void AddScript(string aScriptPath, ResourceType aResourcePriority = ResourceType.Regular) { DynamicHeader.GetInstance().mScripts[aScriptPath] = aResourcePriority; } public static void AddStyleSheet(string aStyleSheetPath, ResourceType aResourcePriority = ResourceType.Regular) { DynamicHeader.GetInstance().mStyleSheets[aStyleSheetPath] = aResourcePriority; } public static string GetHtml() { var instance = GetInstance(); var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext); var html = new StringBuilder(); foreach (var item in instance.mStyleSheets.OrderBy(x => x.Value).Select(x => x.Key)) { html.AppendFormat( @"<link rel=""stylesheet"" type=""text/css"" href=""{0}""/>", urlHelper.StaticContent(item)); } foreach (var item in instance.mScripts.OrderBy(x => x.Value).Select(x => x.Key)) { html.AppendFormat( @"<script type=""text/javascript"" src=""{0}""></script>", urlHelper.StaticContent(item)); } return html.ToString(); } }How do you use it ? pretty simple:
Put this line in your _layout file, right above the </head>
@DynamicHeader.HeaderSection()And in every view you want to declare a style or a script just add:
@{ DynamicHeader.AddStyleSheet("/Content/Css/footer.css", ResourceType.Layout); DynamicHeader.AddStyleSheet("/Content/Css/controls.css", ResourceType.Infrastructure); DynamicHeader.AddScript("/Content/Js/Controls.js", ResourceType.Infrastructure); DynamicHeader.AddStyleSheet("/Content/Css/homepage.css"); }And that's it!
Every style & script will appear in the head section.
How does it work ?
Every view registers in the request http context it's own scripts & styles. The section declaration puts in a unique string as a placeholder in the <head>. After the controller action has executed it filters the actions with ViewResult because the following code isn't relevant to all the other types.
So when a ViewResult is found the Filter stream is replaced, so we can control the actual html that returns to the browser.
Then, we scan the result buffer to look for the </head> and the placeholder.
The search is limited until the </head> is found, because the placeholder must be in the <head>.
When the placeholder is found it is replaced with all the styles & scripts defined.
Good luck!
Way to go, Speedy!
ReplyDeleteGreat stuff!
Rad, solution. Thank you. Somebody on Stackoverflow just told me this is not possible. http://stackoverflow.com/questions/16409496/asp-net-mvc-set-variable-in-layout-template-from-partial-or-body
ReplyDeleteWhere would you save DynamicHeaderHandler.cs and DynamicHeader.cs?
Cheers,
AA
Thanks man!
DeleteUsually I keep a class library between my client project and the actual BL project.
I call it ClientFramework, and there I put extension methods, and other classes that expands the client framework.
In our case I would have put it there along with HtmlHelperExtensions, FilterAttributes etc.
if _Header in one byte[], and Placeholder_ in the next byte[], this code may not work
ReplyDeleteTrue, but the probability for that to happen is extremely low.
DeleteIf it does happen, it's very easy to bypass or fix it.
Thanks
My mistake, you are injecting to the whole bytes, it should work(but maybe too many LOH objects?). And this link http://dynamicheader.codeplex.com/SourceControl/latest#trunk/Lib/DynamicHeaderFilter.cs will have the "chunked" issue.
DeleteThe following line in WriteTooutputStream gets flagged for a potential XSS attack:
ReplyDelete_outputStream.Write(buffer, 0, buffer.Length);
I can't figure out how to get this to 'pass', any suggestions? Thanks for the detailed article.