According to HTML best practices and guidelines, we need to put styles in the head (so that they would be applied to HTML immediately after rendering) and scripts at the end of body (so that they will not block your browser from downloading other content). On the other hand for Sitecore, you need to create a component, which could require some styles on specific scripts, and put them in a placeholder in the middle of the page.

This results in two common solutions: small scripts and style sections spread all over the page, added in each component, or huge styles and scripts files containing all required content and added according to guidelines.

There should be a better way, let’s see how it looks.

Approach

In each component, you register the required script or style-sheet in a HttpRequest and update page generated output by replacing placeholders with resource insertion snippets.

Extension for HtmlHelper

First of all, we need to create an extension for HtmlHelper that will allow you to register scripts.

public static class RequireHtmlHelperExtension
{
    public static RequireHelper Require(this HtmlHelper htmlHelper)
    {
        return RequireHelper.GetInstance();
    }
}

public class RequireHelper
{
    public RequireHelper()
    {
        this.Styles = new ItemRegistrar(AssetFormatters.StyleFormat);
        this.Script = new ItemRegistrar(AssetFormatters.ScriptFormat);
        ...

        this.Registrars = new List<ItemRegistrar>
                            {
                                this.Styles,
                                this.Script,
                                ...
                            };
    }

    public List<ItemRegistrar> Registrars { get; private set; }

    public ItemRegistrar Script { get; private set; }

    public ItemRegistrar Styles { get; private set; }

    ...
    
    public static RequireHelper GetInstance()
    {
        const string InstanceKey = "some key here";

        var context = HttpContext.Current;
        if (context == null)
        {
            return null;
        }

        var assetsHelper = (RequireHelper)context.Items[InstanceKey];

        if (assetsHelper == null)
        {
            context.Items.Add(InstanceKey, assetsHelper = new RequireHelper());
        }

        return assetsHelper;
    }
}

In the extension shown in the first code snippet, we get a singleton instance of Helper object from HttpRequest.Context.Items or create a new one. This object has registrar classes as properties, which are responsible for resources on specific types (line 13-14). In addition to that registrar elements are grouped into a collection for convenience (will be shown later).

Registrar Class

ItemRegistrar class will be responsible for adding elements on specific types, check uniqueness, pre-rendering, and rendering HTML snippets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ItemRegistrar
{
    private readonly string format;

    private readonly string uid;

    private readonly IList<string> items;

    public ItemRegistrar(string format)
    {
        this.uid = string.Format("<i id='{0}'/>", Guid.NewGuid());
        this.format = format;
        this.items = new List<string>();
    }

    public ItemRegistrar Add(string url)
    {
        if (!this.items.Contains(url))
        {
            this.items.Add(url);
        }

        return this;
    }

    public string UniqueId
    {
        get
        {
            return uid;
        }
    }

    public IHtmlString PreRender()
    {
        return new HtmlString(uid);
    }

    public IHtmlString Render()
    {
        var sb = new StringBuilder();

        foreach (var item in this.items)
        {
            var fmt = string.Format(this.format, item);
            sb.AppendLine(fmt);
        }

        return new HtmlString(sb.ToString());
    }
}

You probably noticed that the class above has two methods: PreRender and Render. We will need them since MVC generates output consequentially processing elements as they appear. HTML in the head will be added to the output writer by the time we register resources somewhere in the body. So we use method PreRender to add a placeholder with a unique key, which would be replaced afterward.

MVC Filters

The last element in our puzzle is the replacement of placeholders defined by PreRender method. Here MVC filters come into play. We need to register the global filter in global.asax file that will perform replacements.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class RequireFilterAttribute : ActionFilterAttribute
{
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        base.OnResultExecuted(filterContext);

        var response = filterContext.HttpContext.Response;

        if (response.Filter == null) return;

        response.Filter = new InsertRequiredTagsFilter(response.Filter, response.ContentEncoding);
    }

    public class InsertRequiredTagsFilter : MemoryStream
    {
        private readonly Encoding contentEncoding;

        private readonly Stream response;

        public InsertRequiredTagsFilter(Stream response, Encoding contentEncoding)
        {
            this.response = response;
            this.contentEncoding = contentEncoding;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            var html = new StringBuilder(this.contentEncoding.GetString(buffer));
            html = this.IncertRequiredTags(html);
            buffer = this.contentEncoding.GetBytes(html.ToString());
            this.response.Write(buffer, offset, buffer.Length);
        }

        private StringBuilder IncertRequiredTags(StringBuilder html)
        {
            var require = RequireHelper.GetInstance().Registrars
                .Select(x => new { Key = x.UniqueId, Value = x.Render() })
                .ToList();

            foreach (var pair in require)
            {
                html = html.Replace(pair.Key, pair.Value.ToString());
            }

            return html;
        }
    }
}

As you see we read the content of response.Filter stream and replace placeholders that we left earlier with tags.

Usage

Now we just need to put required PreRender placeholders in layout once and use Html.Require() in views where we need to reference scripts.

1
2
3
4
5
// Layout.cshtml file
Html.Require().Styles.PreRender()

// Component.cshtml file
Html.Require().Script.Add(BundleTable.Bundles.ResolveBundleUrl("~/bundles/js"));

As usual: Share if you like the post. Follow me on Twitter @true_shoorik