Solving Caching Issues in the Sitefinity MVC Navigation Widget

Solving Caching Issues in the Sitefinity MVC Navigation Widget

September 18, 2017 0 Comments

When pages get cached all the widgets on them are cached too. The problem here is that this caching can cause user confusion. When a widget is cached its nodes are cached and some of them could still be visible even if you log out, or some of the menu nodes could be hidden even if you are authenticated. This case should sound familiar to you if you have read the documentation in detail or used the Navigation controls in some of the described scenarios.

This issue is solved elegantly in the WebForms Navigation by exposing OutputCache settings – VaryByAuthenticationStatus and VaryByUserRoles. When one of those two settings is set, the Navigation's cache is invalidated when a user logs in or there is a change in the user's role. Unfortunately, the MVC Navigation does not offer those settings, so for MVC this case requires different solutions.

How to solve the caching problem of the Sitefinity MVC Navigation widget?

There are three immediate solutions:

  • The first solution is obvious, although I would like to mention it: use the hybrid templates and use the WebForms Navigation widget instead of the MVC implementation. The styling of the widget will be a little bit different, but you will benefit quickly from the OutputCache settings.
  • The second solution is to stop caching the pages that do not require authentication. This way you will be sure that when the user logs out, they will see only the public part of the web site in the navigation.
  • The third solution is to implement a custom MVC Navigation widget and extend it with OutputCache settings.

The third solution will provide you with the most flexibility. It might seem like the hardest, but let’s take a look at how we can put that together with just a few lines of code.

Implementing a custom MVC Navigation with OutputCache settings

We don't have to come up with a complex implementation. To start, just look at the code of the WebForms Navigation control and see how the output cache mechanism works there. Use JustDecompile and open Telerik.Sitefinity.dll assembly and look for the LightNavigationControl control and for the CreateChildControl method. Those four lines of code are doing the magic:

if (this.OutputCache.VaryByAuthenticationStatus || this.OutputCache.VaryByUserRoles)
{
    this.outputCacheVariation = new NavigationOutputCacheVariation(this.OutputCache);
    PageRouteHandler.RegisterCustomOutputCacheVariation(this.outputCacheVariation);
}

 

Here is how the MVC Navigation controller looks if we add the full implementation of those four lines of code and implement the dependencies:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Web.Mvc;
using ServiceStack.Text;
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers;
using Telerik.Sitefinity.Frontend.Mvc.Infrastructure.Controllers.Attributes;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.Models;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.StringResources;
using Telerik.Sitefinity.Modules.Pages.Configuration;
using Telerik.Sitefinity.Mvc;
using Telerik.Sitefinity.Services;
using Telerik.Sitefinity.Web;
using Telerik.Sitefinity.Web.UI;
using Telerik.Sitefinity.Web.UI.ContentUI.Contracts;
using Telerik.Sitefinity.Web.UI.NavigationControls.Cache;
using Telerik.Sitefinity.Frontend.Navigation.Mvc.Controllers;
using Telerik.Sitefinity.Security.Claims;
using System.Linq;
 
namespace SitefinityWebApp.Mvc.Controllers
{
    [ControllerToolboxItem(Name = "MyCustomNavigation", Title = "My Custom Navigation", SectionName = "Custom MV CWidgets")]
    [Localization(typeof(NavigationResources))]
    [IndexRenderMode(IndexRenderModes.NoOutput)]
    public class MyNavigationController : NavigationController
    {
 
        protected override ViewResult View(IView view, object model)
        {
 
            if (this.OutputCache.VaryByAuthenticationStatus || this.OutputCache.VaryByUserRoles)
            {
                MyPageRouteHandler.RegisterCustomOutputCacheVariation(new NavigationOutputCacheVariation(this.OutputCache));
            }
 
            return base.View(view, model);
        }
 
        private NavigationOutputCacheVariationSettings outputCacheSettings;
        //private string templateNamePrefix = "NavigationView.";
 
        /// <summary>
        /// Provides UI for setting navigation output cache variations
        /// </summary>
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public NavigationOutputCacheVariationSettings OutputCache
        {
            get
            {
                if (this.outputCacheSettings == null)
                {
                    this.outputCacheSettings = new NavigationOutputCacheVariationSettings();
                }
 
                return this.outputCacheSettings;
            }
 
            set
            {
                this.outputCacheSettings = value;
            }
        }
    }
 
 
    public class MyPageRouteHandler : PageRouteHandler
    {
        internal static void RegisterCustomOutputCacheVariation(ICustomOutputCacheVariation cacheVariation)
        {
            if (string.IsNullOrEmpty(cacheVariation.Key))
                throw new InvalidOperationException("The Key property of the cache variation cannot be empty.");
            var context = SystemManager.CurrentHttpContext;
 
            var cacheVariationsRegistry = context.Items[PageRouteHandler.RegisteredCacheVariations] as CustomOutputCacheVariationsRegistry;
            if (cacheVariationsRegistry == null)
            {
                cacheVariationsRegistry = new CustomOutputCacheVariationsRegistry();
                context.Items[PageRouteHandler.RegisteredCacheVariations] = cacheVariationsRegistry;
            }
            cacheVariationsRegistry.AddVariation(cacheVariation);
            cacheVariationsRegistry.Validated = true;
        }
    }
 
    internal class CustomOutputCacheVariationsRegistry
    {
        public CustomOutputCacheVariationsRegistry()
        {
            this.variations = new List<ICustomOutputCacheVariation>();
            this.changed = true;
        }
 
        public CustomOutputCacheVariationsRegistry(IList<ICustomOutputCacheVariation> variations)
        {
            this.variations = variations;
            this.Validated = false;
        }
 
        public bool Validated
        {
            get;
            set;
        }
 
        public bool Changed
        {
            get
            {
                return this.changed;
            }
        }
 
 
        public IList<ICustomOutputCacheVariation> Variations
        {
            get
            {
                return this.variations;
            }
            //set
            //{
            //    this.variations = value;
            //}
        }
 
        public void AddVariation(ICustomOutputCacheVariation variation)
        {
            var existingVariation = this.variations.FirstOrDefault(v => v.Key == variation.Key);
            if (existingVariation != null)
            {
                if (existingVariation.Equals(variation))
                    return;
 
                this.variations.Remove(existingVariation);
            }
 
            this.variations.Add(variation);
            this.changed = true;
            ignoreCache = null;
        }
 
        public bool IgnoreCache()
        {
            if (!ignoreCache.HasValue)
                ignoreCache = variations.Any(cv => cv.NoCache);
            return this.ignoreCache.Value;
        }
 
        private readonly IList<ICustomOutputCacheVariation> variations;
        private bool? ignoreCache;
        private bool changed;
    }
 
    internal class NavigationOutputCacheVariation : CustomOutputCacheVariationBase
    {
        private NavigationOutputCacheVariationSettings Settings { get; set; }
 
        public NavigationOutputCacheVariation(NavigationOutputCacheVariationSettings settings)
        {
            this.Settings = settings;
        }
 
        /// <inheritdoc />
        public override string Key
        {
            get
            {
                return "sf-navigation-view";
            }
        }
 
        /// <inheritdoc />
        public override bool NoCache
        {
            get
            {
                if (this.Settings.VaryByAuthenticationStatus)
                    return ClaimsManager.GetCurrentIdentity().IsAuthenticated;
                else
                    return false;
            }
        }
 
        /// <inheritdoc />
        public override string GetValue()
        {
            if (this.Settings.VaryByUserRoles)
            {
                var identity = ClaimsManager.GetCurrentIdentity();
                var orderedRoles = identity.Roles.OrderBy(r => r.Id).Select(r => r.Id);
                var value = string.Join(string.Empty, orderedRoles);
                var hashedValue = value.GetHashCode().ToString();
                return hashedValue;
            }
            else
                return string.Empty;
        }
    }
}

 

The last step will be to create the views and the widget designers – but this is effortless since you can simply copy and paste those files from the default implementation of the widget. Keep in mind that the above solution is tested with the 10.1 version. If you want to use a different version, you must copy and paste the views and designers corresponding to the version that you are using.

Will OutputCache settings be implemented in the MVC Navigation widget itself?

With Sitefinity 10.2 the above implementation will come out of the box and you can remove the custom code. Although the solution is coming to the latest version soon, for those of you using an older version this solution will continue to work. Happy coding!

Peter Filipov

Peter Filipov

Peter Filipov (Pepi) is a Developer Advocate focused on Sitefinity. He is passionate about web development and sports. Prior joining the DevRel team, Pepi was one of the team leaders in the Telerik Web Components division.

Comments
Comments are disabled in preview mode.
Topics
 
 
Latest Stories in
Your Inbox
Subscribe
More From Progress
New_Mobile_Dev_Ebook_Progress_Website_Thumbail
The New Mobile Development Landscape
Download Whitepaper
 
IDC Spotlight Sitefinity Thumbnail
Choosing the Right Digital Experience Platform to Improve Business Outcomes
Download Whitepaper
 
TheFastestWayToBuildMobileAppsArtboard-2
The Fastest Way to Build Mobile Apps With Cloud Data
Watch Webinar