Share

LinkedIn

Adding Scripts and Stylesheets from Cached Sitecore Renderings

While it is generally best to minify and combine scripts and stylesheets that are used site-wide, it is occasionally necessary to add these references for a smaller section of a site.

In a basic ASP.NET site this would normally be done from the code behind a user control in the Page_Load event handler. This can work in Sitecore as well as long as your sublayout is not cached. However if you decide to turn on caching for your sublayout, you will find that the references are added on the first request but not on subsequent requests. This is because the Page_Load event is not fired if the sublayout is rendered from the cache. Below I will show one way to work around this problem.

The basic idea for this solution is to decorate the sublayout class with custom attributes that specify which scripts and resources it depends on and then add those references to the page via a pipeline processor.

Here’s the custom attribute:

namespace Aws.BlogPosts.ClientReferences
{
    [AttributeUsage(AttributeTargets.Class, 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)
        {
            this.Path = path;
            this.Type = type;
            this.Placeholder = "HeaderScripts";
            this.SortOrder = 100;
        }
    }
  
    public enum ClientReferenceType
    {
        Script, Stylesheet
    }
}

The attribute requires parameters for the path of the file and the reference type (script or stylesheet). It allows named parameters to be used to specify a placeholder name and a sort order. Here is how you would use it on the code behind of a sublayout:

namespace Aws.BlogPosts.ClientReferences
{
    [ClientReference("/js/attributeScript.js", ClientReferenceType.Script)]
    [ClientReference("~/css/FancyStyles.css", ClientReferenceType.Stylesheet, Placeholder = "stylesheets", SortOrder = 1000)]
    public partial class ClientReferencesSublayout : UserControl
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
    }
}

These attributes are then used by a processor for the InsertRenderings pipeline:

namespace Aws.BlogPosts.ClientReferences
{
  
    public class ProcessClientReferences : InsertRenderingsProcessor
    {
        public override void Process(InsertRenderingsArgs args)
        {
            try
            {
                var stylesheets = new List<ClientReferenceAttribute>();
                var scripts = new List<ClientReferenceAttribute>();
  
                foreach (var renderingReference in args.Renderings)
                {
                    var control = renderingReference.GetControl();
                    var sublayout = control as Sublayout;
                    if (sublayout != null)
                    {
                        control = sublayout.GetUserControl();
                    }
  
                    if (control == null)
                    {
                        continue;
                    }
  
                    var attributes =
                        Attribute.GetCustomAttributes(control.GetType(), typeof(ClientReferenceAttribute)) as
                        ClientReferenceAttribute[];
  
                    if (attributes != null)
                    {
                        foreach (var attribute in attributes)
                        {
                            switch (attribute.Type)
                            {
                                case ClientReferenceType.Stylesheet:
                                    stylesheets.Add(attribute);
                                    break;
                                case ClientReferenceType.Script:
                                    scripts.Add(attribute);
                                    break;
                            }
                        }
                    }
                }
  
                foreach (var stylesheet in SortAndFilter(stylesheets))
                {
                    PlaceControl(args, CreateStylesheetReference(stylesheet.Path), stylesheet.Placeholder);
                }
  
                foreach (var script in SortAndFilter(scripts))
                {
                    PlaceControl(args, CreateScriptReference(script.Path), script.Placeholder);
                }
            }
            catch (Exception ex)
            {
                Log.Error("Error processing client reference attributes.", ex, this);
            }
        }
  
        private static Control CreateScriptReference(string scriptPath)
        {
            var script = new HtmlGenericControl("script");
            script.Attributes.Add("type", "text/javascript");
            script.Attributes.Add("src", WebUtil.CurrentPage.ResolveUrl(scriptPath));
            return script;
        }
  
        private static Control CreateStylesheetReference(string stylesheetPath)
        {
            var link = new HtmlLink { Href = WebUtil.CurrentPage.ResolveUrl(stylesheetPath) };
            link.Attributes.Add("rel", "stylesheet");
            link.Attributes.Add("type", "text/css");
            return link;
        }
  
        private static void PlaceControl(InsertRenderingsArgs args, Control control, string placeholder)
        {
            args.Renderings.Add(new RenderingReference(control) { Placeholder = placeholder });
        }
  
        private static IEnumerable<ClientReferenceAttribute> SortAndFilter(IEnumerable<ClientReferenceAttribute> attributes)
        {
            return attributes.OrderByDescending(a => a.SortOrder).GroupBy(a => a.Path).Select(g => g.First());
        }
  
    }
}

This processor checks the classes associated with each of the renderings on the page for our attributes.  This will work with Web control renderings as well as sublayouts.  Script and stylesheet attributes are added to separate lists which are then sorted and filtered for duplicates. New renderings are created from the rendered script and link tags and are added to the specified placeholders.

The final step is to add the processor to the InsertRenderings pipeline in a config include file:

<pipelines>
    <insertRenderings>
        <processor type="Aws.BlogPosts.ClientReferences.ProcessClientReferences, Aws.BlogPosts.ClientReferences"/>
    </insertRenderings>
</pipelines>

This adds the processor to the end of the pipeline. It is important that it be at the end of the pipeline since it operates on the renderings added by the other processors.

With this processor in place adding scripts and stylesheets for a sublayout is much more convenient. However care should be taken with this processor since it runs on every request. If you are only using it for a couple of pages, you might consider adding some conditions to make the processor abort for areas of the site that don’t utilize it.

Sitecore development, Sitecore custom code

Comments

Add a Comment

*
*

Please confirm you are human by typing the text you see in this image:

Nick Cipollina said: 6/19/2014 at 9:23 AM

I noticed you are using a placeholder value of "HeaderScripts" to put your links and script references in. Is this a placeholder that sitecore provides automatically, or do you have this on your layout file?

Ben Golden said: 6/19/2014 at 9:43 AM

You will need to put any placeholder you want to use on your layout or one of your sublayouts. Also, HeaderScripts is just the default value. You can specify a different placeholder in the attribute.