In my previous post I’ve described a solution that allows you to inject scripts and styles in specific placeholders in a layout. Scripts are registered in views, but they will be rendered by global filters only when all output of a page is ready. However, when it comes to caching Sitecore will be able to record HTML generated by your component just after it is rendered. So none of the actions that will be required in other parts of the site will be triggered with cache enabled.

Approach

We need to simulate similar behavior that Sitecore has in the pipeline, but instead of HTML output, we need to save and restore the required actions. We will need 3 processors:

  • Enter Context
  • Save Actions
  • Restore From Cache

Context & Cache Key

When we are talking about MVC components they all have their execution context, as you would need to provide different rendering items to each component. Sitecore will create it and push context to a stack when you enter it, and remove it from the stack and return to parent context, when a lifetime of args object for the current pipeline will come to an end. This would ensure that during the whole execution of the pipeline you will be able to get current rendering info.

Also if Sitecore finds out that the current component configured to be cached it would generate CacheKey. The Key will be used to identify the HTML output of a component in a site cache.

In the current solution, context will store information about the current caching scope and the key will be used to identify scripts in the cache.

public class EnterScriptCachingContext : RenderRenderingProcessor
{
    public override void Process(RenderRenderingArgs args)
    {
        Assert.ArgumentNotNull(args, "args");
        if (args.Rendered || string.IsNullOrEmpty(args.CacheKey) 
            || RequireHelper.GetInstance().IsScriptCachingContextEntered) // prevent saving in caching context of nested controls
        {
            return;
        }
        this.EnterContext(args);
    }

    protected virtual void EnterContext(RenderRenderingArgs args)
    {
        var disposable = RequireHelper.GetInstance().EnterScriptCachingContext(args.CacheKey);
        args.Disposables.Add(disposable);
    }
}

In Line 7, we checking that if the caching context was entered already. We should be able to get all scripts for current and child controls when current control will be retrieved from the cache.

Line 16, create a context with current CacheKey (It is important to add it to an args.Disposables to exit context properly). Context creation will use Sitecore ContextService to push the ScriptCachingService object there.

public IDisposable EnterScriptCachingContext(string cacheKey)
{
    return ContextService.Get().Push(new ScriptCachingContext(cacheKey));
}

Writing to Cache

Writing to cache would be similar to renderings cache implementation, we only need to generate peace of content that we would like to save.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
protected virtual void UpdateCache(string cacheKey, RenderRenderingArgs args)
{
    var helper = RequireHelper.GetInstance();

    foreach (var reg in helper.Registrars)
    {
        // Get all script elements from cache

        var scripts = reg.FindByCacheKey(cacheKey);
        var index = 0;
        foreach (var script in scripts)
        {
            this.AddHtmlToCache(reg.UniqueId + cacheKey + index++, script, args);
        }
    }
}

In the case of scripts registration code will iterate over registrars and find scripts snippets marked with current **CacheKey **with method FindByCacheKey, which will return rendered script section. Before saving key would be enriched with the registrar id and index of this script so that we will be able to put scripts in the correct place in layout and remove duplication.

Restore Scripts

The last piece of this puzzle is to restore elements from the cache. If a component has cache options enabled and the site already has something in the cache then the processor will look for an element with the current key, registrar id, and index. When script snippets are retrieved they are registered similar to registration from view described in the initial post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
protected virtual void Render(string cacheKey, RenderRenderingArgs args)
{
    var htmlCache = Context.Site.ValueOrDefault(CacheManager.GetHtmlCache);
    if (htmlCache == null)
    {
        return;
    }

    var helper = RequireHelper.GetInstance();
    foreach (var itemRegistrar in helper.Registrars)
    {
        var index = 0;
        var html = htmlCache.GetHtml(itemRegistrar.UniqueId + cacheKey + index++);
        while (html != null)
        {
            itemRegistrar.Add(html);
            html = htmlCache.GetHtml(itemRegistrar.UniqueId + cacheKey + index++);
        }
    }
}

Usage

Finally, we need to register our pipelines in mvc.renderRendering pipeline in the order you find below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<mvc.renderRendering>
    <processor patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderFromCache, Sitecore.Mvc']"
        type="Example.Pipelines.Require.RequireScriptsFromCache, Example" />

    <processor patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.EnterRenderingContext, Sitecore.Mvc']"
        type="Example.Pipelines.Require.EnterRequireScriptsContext, Example" />

    <processor patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.RenderRendering.AddRecordedHtmlToCache, Sitecore.Mvc']"
        type="Example.Pipelines.Require.AddRecordedScriptsToCache, Example" />
</mvc.renderRendering>

That’s it. We could cache components that inject scripts and styles via global filters.


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