More in this section
Categories
Bloggers
Blogs RSS feed

Solving Caching Issues in the Sitefinity MVC Navigation Widget

by Peter Filipov

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!

5 comments

Leave a comment
  1. Derrick Koon Sep 18, 2017
    Thanks Peter, I was running into this the other day and you just took a bug off my plate :) 
  2. Peter Sep 18, 2017

    Hi Derrick,

    I know that it is an annoying issue and I wanted to share a fix for it. Good to hear that I helped you just in time.

    Peter

  3. Razziyah Nov 12, 2017

    Navigation is the basically part of maps which helps us nowadays. The peoples can also use this allassignmenthelp service to download this type of helping applications. It helps the travelers more and its pretty easy to use and find our selected location. 

  4. vex 3 Nov 18, 2017
    Thanks for your post! Through your pen I found the problem up interesting! I believe there are many other people who are interested in them just like me! Thanks your shared!... I hope you will continue to have similar posts to share with everyone! I believe a lot of people will be surprised to read this article!vex 3 
  5. Jeremy Nov 30, 2017
    Thank you so much for saving my my lots of time. :)

    Leave a comment