When writing web applications it is always a good idea to minify and combine your scripts and stylesheets to limit the quantity and size of requests to the server from the browser. There are some occasions where you may want to add a particular script file or stylesheet when a given Sitecore rendering is on a page. A good example of why you might want to do this is if you have a client heavy rendering that has a large amount of JSON data that is only used by that rendering. This is probably something you wouldn't want to include on every page. When doing this with a webforms application, I've use an approach much like the one laid out in this post. The basic idea is that you create a custom attribute, apply it to the code-behind of your user control, and add the script and stylesheets from the attributes in the insertRenderings pipeline. This is a great approach and works really well in a webforms app.

While this approach works well in a webforms application, it doesn't work very well in an MVC application. There are a few problems that prevent this from working in an MVC application:

  • MVC applications don't have code behind and therefore don't have a class you can attach an attribute to include your client reference to.
  • MVC requests don't go through the insertRenderings pipeline. When Sitecore recognizes that a request is going to an MVC layout, it routes it through a separate pipeline for processing.

Since MVC requests are handled differently we need to make some adjustments to this solution in order to make this work.

The Solution

This solution was developed in Sitecore version 7.5, but this should most likely work in any version of Sitecore that supports MVC. The other important piece of information to note is that this solution only works with Controller Renderings. This will not work with View Renderings. The approach is to decorate your controller attributes with a custom attribute that defines the client file (script or stylesheet) to insert into the rendering and a custom pipeline processor will do the actual insertion.

The custom attribute will look like this:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ClientReferenceAttribute : Attribute
{
	public string Path { get; private set; }
	public ClientReferenceType Type { get; private set; }
	public string Placeholder { get; set; }
	public int SortOrder { get; set; }
 
	public ClientReferenceAttribute(string path, ClientReferenceType type)
	{
		Path = path;
		Type = type;
		Placeholder = Type == ClientReferenceType.Stylesheet ? "header-styles" : "body-scripts";
		SortOrder = 100;
	}
}
 
public enum ClientReferenceType
{
	Script, Stylesheet
}

You'll notice that this attribute can be applied to methods only. ClientReferenceType is used to determine whether or not the reference is a script or a stylesheet. You'll notice that the Placeholder property is set based upon what type of reference it is or you can pass it in when setting the attribute. You will actually have to add those placeholder's to your layout in order for this to work. With an MVC application this can be accomplished like this:

@Html.Sitecore().Placeholder("header-styles")

And:

@Html.Sitecore().Placeholder("body-scripts")

To use the attribute you would have a controller that looks something like this:

public class SampleController : SitecoreController
{
	[ClientReference("~/bundles/custom-script", ClientReferenceType.Script]
	public ActionResult SampleMethod()
	{
		return View();
	}
}

In this example, I'm using the System.Web.Optimization assembly to do the combining and minification of client reference scripts.

After adding your custom attributes, the following processor will find them and insert the scripts into the Renderings collection.

public class AddClientReferenceProcessor : BuildPageDefinitionProcessor
{
	public override void Process(BuildPageDefinitionArgs args)
	{
		if (args.Result == null)
			return;
 
		AddClientReference(args);
	}
 
	private void AddClientReference(BuildPageDefinitionArgs args)
	{
		var renderings = new ListRendering>();
		var styleSheets = new ListTupleClientReferenceAttribute, Guid, Guid>>();
		var scripts = new ListTupleClientReferenceAttribute, Guid, Guid>>();
		foreach (var rendering in args.Result.Renderings.Where(r => r.Renderer is ControllerRenderer))
		{
			var renderer = rendering.Renderer as ControllerRenderer;
			var controllerType = Type.GetType(renderer.ControllerName);
			var deviceId = rendering.DeviceId;
			var layoutId = rendering.LayoutId;
			if (controllerType != null)
			{
				var method = controllerType.GetMethod(renderer.ActionName, BindingFlags.Public | BindingFlags.Instance);
				if (method != null)
				{
					var attributes = method.GetCustomAttributesClientReferenceAttribute>();
					foreach (var attribute in attributes)
					{
						switch (attribute.Type)
						{
							case ClientReferenceType.Script:
								scripts.Add(new TupleClientReferenceAttribute, Guid, Guid>(attribute, deviceId, layoutId));
								break;
							case ClientReferenceType.Stylesheet:
								styleSheets.Add(new TupleClientReferenceAttribute, Guid, Guid>(attribute, deviceId, layoutId));
								break;
						}
					}
 
				}
			}
		}
		foreach (var script in SortAndFilter(scripts))
		{
			renderings.Add(CreateRendering(CreateScriptReference(script.Item1.Path),
				script.Item1.Placeholder, script.Item2, script.Item2));
		}
 
		foreach (var style in SortAndFilter(styleSheets))
		{
			renderings.Add(CreateRendering(CreateStyleSheetReference(style.Item1.Path),
				style.Item1.Placeholder, style.Item2, style.Item3));
		}
		args.Result.Renderings.AddRange(renderings);
	}
 
	private static Rendering CreateRendering(string content, string placeholder, Guid deviceId, Guid layoutId)
	{
		return new ContentRendering
		{
			Content = content,
			Placeholder = placeholder,
			DeviceId = deviceId,
			LayoutId = layoutId
		};
	}
 
	private static string CreateScriptReference(string path)
	{
		var output = new StringWriter();
 
		using (var writer = new HtmlTextWriter(output))
		{
			writer.AddAttribute(HtmlTextWriterAttribute.Src, System.Web.Optimization.Scripts.Url(path).ToHtmlString());
			writer.RenderBeginTag(HtmlTextWriterTag.Script);
			writer.RenderEndTag();
		}
		return output.ToString();
	}
 
	private static string CreateStyleSheetReference(string path)
	{
		var output = new StringWriter();
 
		using (var writer = new HtmlTextWriter(output))
		{
			writer.AddAttribute(HtmlTextWriterAttribute.Rel, "stylesheet");
			writer.AddAttribute(HtmlTextWriterAttribute.Href, System.Web.Optimization.Styles.Url(path).ToHtmlString());
			writer.RenderBeginTag(HtmlTextWriterTag.Link);
			writer.RenderEndTag();
		}
 
		return output.ToString();
	}
 
	private static IEnumerableTupleClientReferenceAttribute, Guid, Guid>> SortAndFilter(IEnumerableTupleClientReferenceAttribute, Guid, Guid>> attributes)
	{
		return attributes.OrderByDescending(a => a.Item1.SortOrder).GroupBy(a => a.Item1.Path).Select(a => a.First());
	}
}

This processor finds all of the Renderings from the Renderings collection where the Renderer is a ControllerRenderer. It then looks for all Renderers with the custom ClientReferenceAttribute and creates a list of all of the stylesheets and scripts to be inserted. The processor will then sort and filter out duplicate references so that the same scripts and stylesheets aren't added to a page more than once. The processor then inserts ContentRenderings into the Renderings collection in the specified placeholder. You'll notice that the content is the stylesheet link or the script file and they use the System.Web.Optimization assembly. One thing to note is that the ContentRendering must have the DeviceId and LayoutId set from each rendering. Otherwise, your client references will not be output.

The last thing that needs to be done to make this work is to include this processor in the mvc.BuildPageDefinition pipeline. This can be accomplished with the following xml:

 mvc.buildPageDefinition>
	processor patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.BuildPageDefinition.ProcessXmlBasedLayoutDefinition, Sitecore.Mvc']"
			 type="Sample.AddClientReferenceProcessor, Sample"/>
 mvc.buildPageDefinition>

This adds the processor to the pipeline after the ProcessXmlBasedLayoutDefinition and it's important that it is put here since that is the processor that inserts all of the renderings for a given page and they need to be there to determine which references to add.

With this processor in place, you can now add script and stylesheet references to your Sitecore site only on the pages that need them. This will allow for a much more dynamic Sitecore site while including only the script and stylesheets that are needed for a given rendering.