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 (Pepi) is a Product Builder focused on building the future of Sitefinity, relying on the newest technologies such as .NET 6 (and up), React and Angular. His previous experience as a Developer Advocate and Manager of Engineering helps him to understand the customers’ and market needs of Sitefinity. He also is passionate about being active and healthy.