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?
[su_note note_color=”#fafafa”]NOTE: the solution described here is posted to GitHub at https://github.com/brainjocks/SitecoreMvcAreas[/su_note]
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/(?&amp;lt;areaname&amp;gt;[^s/]<em>)/views/(/[ws-+]+/)</em>(.*.(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.
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.
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 –
/// &lt;summary&gt; /// 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 /// &lt;/summary&gt; 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!
Just dropping a ‘thank-you-for-this-awesome-blog’ comment. 🙂
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.
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?
Pingback: Multisite WFFM Form Markup using MVC Areas | jammykam