Switching the layout in Orchard CMS
- by Bertrand Le Roy
The UI composition in Orchard is extremely flexible, thanks in no small part to the usage of dynamic Clay shapes. Every notable UI construct in Orchard is built as a shape that other parts of the system can then party on and modify any way they want. Case in point today: modifying the layout (which is a shape) on the fly to provide custom page structures for different parts of the site. This might actually end up being built-in Orchard 1.0 but for the moment it’s not in there. Plus, it’s quite interesting to see how it’s done. We are going to build a little extension that allows for specialized layouts in addition to the default layout.cshtml that Orchard understands out of the box. The extension will add the possibility to add the module name (or, in MVC terms, area name) to the template name, or module and controller names, or module, controller and action names. For example, the home page is served by the HomePage module, so with this extension you’ll be able to add an optional layout-homepage.cshtml file to your theme to specialize the look of the home page while leaving all other pages using the regular layout.cshtml. I decided to implement this sample as a theme with code. This way, the new overrides are only enabled as the theme is activated, which makes a lot of sense as this is going to be where you’ll be creating those additional layouts. The first thing I did was to create my own theme, derived from the default TheThemeMachine with this command: codegen theme CustomLayoutMachine /CreateProject:true /IncludeInSolution:true /BasedOn:TheThemeMachine
.csharpcode, .csharpcode pre
{
font-size: 12px;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
Once that was done, I worked around a known bug and moved the new project from the Modules solution folder into Themes (the code was already physically in the right place, this is just about Visual Studio editing).
The CreateProject flag in the command-line created a project file for us in the theme’s folder. This is only necessary if you want to run code outside of views from that theme.
The code that we want to add is the following LayoutFilter.cs:
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard;
using Orchard.Mvc.Filters;
namespace CustomLayoutMachine.Filters {
public class LayoutFilter : FilterProvider, IResultFilter {
private readonly IWorkContextAccessor _wca;
public LayoutFilter(IWorkContextAccessor wca) {
_wca = wca;
}
public void OnResultExecuting(ResultExecutingContext filterContext) {
var workContext = _wca.GetContext();
var routeValues = filterContext.RouteData.Values;
workContext.Layout.Metadata.Alternates.Add( BuildShapeName(routeValues, "area"));
workContext.Layout.Metadata.Alternates.Add( BuildShapeName(routeValues, "area", "controller"));
workContext.Layout.Metadata.Alternates.Add( BuildShapeName(routeValues, "area", "controller", "action"));
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
}
private static string BuildShapeName( RouteValueDictionary values, params string[] names) {
return "Layout__" +
string.Join("__",
names.Select(s => ((string)values[s] ?? "").Replace(".", "_")));
}
}
}
This filter is intercepting ResultExecuting, which is going to provide a context object out of which we can extract the route data. We are also injecting an IWorkContextAccessor dependency that will give us access to the current Layout object, so that we can add alternate shape names to its metadata.
We are adding three possible shape names to the default, with different combinations of area, controller and action names. For example, a request to a blog post is going to be routed to the “Orchard.Blogs” module’s “BlogPost” controller’s “Item” action. Our filters will then add the following shape names to the default “Layout”:
Layout__Orchard_Blogs
Layout__Orchard_Blogs__BlogPost
Layout__Orchard_Blogs__BlogPost__Item
Those template names get mapped into the following file names by the system (assuming the Razor view engine):
Layout-Orchard_Blogs.cshtml
Layout-Orchard_Blogs-BlogPost.cshtml
Layout-Orchard_Blogs-BlogPost-Item.cshtml
This works for any module/controller/action of course, but in the sample I created Layout-HomePage.cshtml (a specific layout for the home page), Layout-Orchard_Blogs.cshtml (a layout for all the blog views) and Layout-Orchard_Blogs-BlogPost-Item.cshtml (a layout that is specific to blog posts).
Of course, this is just an example, and this kind of dynamic extension of shapes that you didn’t even create in the first place is highly encouraged in Orchard. You don’t have to do it from a filter, we only did it this way because that was a good place where we could get the context that we needed. And of course, you can base your alternate shape names on something completely different from route values if you want.
For example, you might want to create your own part that modifies the layout for a specific content item, or you might want to do it based on the raw URL (like it’s done in widget rules) or who knows what crazy custom rule.
The point of all this is to show that extending or modifying shapes is easy, and the layout just happens to be a shape. In other words, you can do whatever you want. Ain’t that nice?
The custom theme can be found here:
Orchard.Theme.CustomLayoutMachine.1.0.nupkg
Many thanks to Louis, who showed me how to do this.