Sitecore Support for MVC Areas

Multitenancy with MVC and Areas

When developing for Sitecore, you must always be conscious of those “other” people … that is, other tenants.

As mentioned countless times before, Sitecore offers support for multiple tenants from a single running instance, but it does not provide process or filesystem isolation for assets developed for each tenant website.

If you’re fortunate enough to be using MVC as your presentation technology of choice, MVC areas are an obvious choice for organizing and separating assets belonging to each tenant, or for breaking up a large single tenant project into multiple modules.  But MVC Areas actually do far more than just organize  your files into separate directories.  Runtime resolution of assets based on  directory convention is also great, and support for this by Visual Studio IntelliSense is even better.

Two examples of this are resolving views from a controller action, and the use of DisplayTemplates and EditTemplates.

Controller Actions

When using controllers and action methods within MVC, your return value can take many forms.  Specifically, when returning a View or PartialView from the controller action, you can use the syntax:

return PartialView();

When a controller is called using one of these return values, MVC will use a convention to find the related view or partial view to render the result of the action.  If the solution is not using areas, the default directory where MVC will look for the related view files includes:

~/Views/ControllerName/ActionName.cshtml
~/Views/Shared/ActionName.cshtml

And, as mentioned, IntelliSense will inspect the code and make sure that the view requested does in fact exist at one of the specific locations.

However, the same syntax will find the view within the area if the controller and route to the controller are contained within the area.  When the view and controller actions methods are contained within an area, the MVC engine will first look within the area to resolve the views.

~/Areas/AreaName/Views/ControllerName/ActionName.cshtml
~/Areas/AreaName/Views/Shared/ActionName.cshtml

Yes – there is also a version of the PartialView(“~/Views/SomeViewName.cshtml”) methods that will allow you to specify WHERE to find the view file, but we don’t like that since it introduces magic strings into the code. However, this workaround does not work when using DisplayTemplates and EditTemplates.

DisplayTemplates and EditTemplates

Display and Edit Templates give you a way to create small partial views that are designed to render only a specific model type.  These are very useful if you have some small data objects that need to be rendered in a consistent way.  To use a Display Template, you can use some simple syntax within your view

@Html.DisplayFor( x => x.PropertyName)

Razor will “inspect” the type of the property and pass the item as model to a rendering that matches the type name.  The same syntax works for collections – and will call the display template for each item in the collection when referenced by the collection.

The problem with display templates is that it only looks in very specific areas for the template, and there isn’t a way to override the location.  If there is no current area, the display templates are expected to be in a folder

~/Views/Shared/DisplayTemplates/templatename.cshtml

Although you can use a subfolder

@Html.DisplayFor( x => x.PropertyName, "Some/Other/Folder/templatename")

you cannot unroot the path from the DisplayTemplates folder.

If you are within an area – the area is searched for the display templates folder as well –

~/Areas/AreaName/Views/Shared/DisplayTemplates/templatename.cshtml

How MVC “Normally” Determines the Area

When executing a controller action, the route can be used to map a controller and action to an area.  The area name is registered during the area registration process – such as

public class MyAreaRegistration : AreaRegistration
{
   public override string AreaName {  get { return "MyArea"; } }

   public override void RegisterArea(AreaRegistrationContext context)
   {
      context.MapRoute( "MyArea_default", "MyArea/{controller}/{action}/{id}",
                        new { action = "Index", id = UrlParameter.Optional });
   }
}

Calling One Area from Another

In addition, when using the Html helpers to execute a controller to a partial view – you can call controller actions from one area to another using the syntax

@Html.RenderAction("ActionName", "ControllerName", new { area = "AreaName"} )

Why Doesn’t this Work in Sitecore?

In Sitecore, we don’t always use routes to get to a controller action – and we also do not use the @Html.Partial() or @Html.RenderAction() helper functions to execute views.  Rather, the Sitecore ItemResolver will resolve the item we are trying to display, and then that item will be rendered – which means that it’s layout will be rendered through Sitecore’s normal pipelined process.

Of the renderings that are within the layout field of that item, there might be several components bound to the page that are view renderings or controller renderings.  These renderings are rendered by Sitecore through the RenderRenderings pipeline, (rrrrrrr….), and Sitecore is not using the area of each item when executing each rendering context.

So What Can We Do?

NOTE: the solution described here is posted to GitHub at https://github.com/brainjocks/SitecoreMvcAreas

What we need to do is two things – first, tell view and controller renderings in Sitecore that they are contained within an area.  Then, tell MVC to set the area name while each rendering is being executed.

For the first part, we will add a processor within the mvc.renderRendering pipeline:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   <sitecore>
      <pipelines>
         <mvc.renderRendering>
            <processor patch:before="*[1]" type="Score.Custom.Pipelines.Mvc.AddRenderingArea, Score.Custom" />
         </mvc.renderRendering>
      </pipelines>
   </sitecore>
</configuration>

Since there is no built-in, obvious way to get the area for all rendering types (controller renderings, view renderings, layouts, etc.), we choose to use this new pipeline processor in mvc.renderingRendering to create and execute a “micro-pipeline” (as Alex Shyba would say)…

public virtual string GetAreaName(Sitecore.Mvc.Presentation.Rendering rendering)
{
   return PipelineService.Get().RunPipeline("score.mvc.getArea", new GetAreaArgs(rendering), arg => arg.AreaName);
}

Within the new pipeline we have created, we will execute 2 new processors to fetch the area name using 2 different techniques.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <score.mvc.getArea>
        <processor type="Score.Custom.Pipelines.Mvc.GetAreaByRenderingPath, Score.Custom" />
        <processor type="Score.Custom.Pipelines.Mvc.GetAreaByRenderingFolder, Score.Custom" />
      </score.mvc.getArea> 
    </pipelines>
  </sitecore>
</configuration>

Finding the Area Name for View Renderings and Layouts

The first technique we will use will employ a simple process – if the rendering has a view path, attempt to extract the area name from the path by convention.

public virtual string GetAreaName(string renderingPath)
{
   Match m = Regex.Match(renderingPath,
                @"/areas/(?<areaname>[^s/]*)/views/(/[ws-+]+/)*(.*.(cshtml|ascx)$)",
                RegexOptions.IgnoreCase);

   return m.Success ? m.Groups["areaname"].Value : null;
}

Finding the Area Name for Controller Renderings

Since controller renderings do not have a rendering path, this method will not work. For a controller rendering, we chose another approach. First, we create a template called Rendering Folder with Area Name that is derived from Rendering Folder and adds a single property – Area Name – as a text field.

Screen Shot 2014-12-08 at 10.30.52 AM

To use this method, replace a rendering folder in your Sitecore path under /sitecore/Layout/Renderings with your new folder type, and set the area name field in the folder content tab.


Screen Shot 2014-12-08 at 10.30.21 AM

The code within the second processor, GetAreaByRenderingFolder, will find the immediate parent of a controller rendering and read the property

public virtual void FindAreaByFolder(GetAreaArgs args)
{           
   RenderingItem renderingItem = args.Rendering.RenderingItem;
   Item current = renderingItem.InnerItem;

   Item folder = current.FindParentDerivedFrom(new ID(ScoreConst.TemplateIds.MvcAreaNameBase));

   if (folder == null)
   {
      return;
   }

   string areaName = folder["Area Name"];

   if (!String.IsNullOrWhiteSpace(areaName))
   { 
      args.AreaName = areaName;
   }
}

Now that we have the area name, what do we do with it?

Some interesting code within the AddRenderingArea processor is the use of IDisposable to temporarily set the area name, and reset it once the rendering has completed it’s processing.

The area name in MVC is stored within a data token within the RequestContext –

/// <summary>
///     A disposable that will set the MVC Area for the current RequestContext on 
///     creation and get it back to the state it was in on dispose
/// </summary>
   public class RenderingAreaContext : IDisposable
   {
      private readonly DisposeHelper _disposer = new DisposeHelper(true);

      private RequestContext RequestContext { get; set; }

      private string PreviousArea { get; set; }

      private bool PreviousAreaWasNull { get; set; }

      public RenderingAreaContext(PageContext pageContext, string newAreaName)
      {
         RequestContext = pageContext.RequestContext;
         // not sure if this can occur, but just in case for now
         if (!RequestContext.RouteData.DataTokens.ContainsKey("area"))
         {
            PreviousAreaWasNull = true;
            RequestContext.RouteData.DataTokens.Add("area", newAreaName);
         }
         else
         {
            PreviousArea = (string) RequestContext.RouteData.DataTokens["area"];
            RequestContext.RouteData.DataTokens["area"] = newAreaName;
         }
      }

      public void Dispose()
      {
         if (_disposer.Disposed)
         {
            return;
         }
         if (PreviousAreaWasNull)
         {
            RequestContext.RouteData.DataTokens.Remove("area");
         }
         else
         {
            RequestContext.RouteData.DataTokens["area"] = PreviousArea;
         }
      }
   }

— and that’s it. I have a packaged release of this added to the Sitecore Marketplace awaiting publishing … and the code is available to download and inspect on the BrainJocks GitHub page at https://github.com/brainjocks/SitecoreMvcAreas … Enjoy!

Brian Beckham

I am the President and CEO of BrainJocks. As a Sitecore MVP, I spend most of my time consulting and architecting software solutions for enterprise-level Sitecore projects. Learn more about Brian.

4 comments on Sitecore Support for MVC Areas

scKarimDecember 24, 2014 - Reply

Just dropping a ‘thank-you-for-this-awesome-blog’ comment. 🙂

MaulikApril 29, 2016 - Reply

Hi Brian,
Thanks for the nice blog.

We have downloaded the package from Shared Modules.
We are facing issue when we try to get the view.
It is not searching in the Areas folder.

We are using Sitecore 7.2.

Brian BeckhamMay 12, 2016 - Reply

Maulik – I assume you are talking about a view referenced by a controller rendering?

If you installed the shared source module, did you also create a folder to organize the renderings and specify the MVC area for the view and controller renderings?

Multisite WFFM Form Markup using MVC Areas | jammykamOctober 3, 2017 - Reply

[…] does the same thing for earlier versions of Sitecore 8. They went a different route and used an MVC Areas module by Brainjocks (since there was no OOTB Area support in Sitecore at the time remember). I highly […]

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back