Switching the layout in Orchard CMS

(c) Bertrand Le Roy 2010The 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

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).The contents of our custom theme

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.

30 Comments

  • Nice!!

    Could you maybe do something like an API overview of some of the core modules (Like Orchard.Blog or Shapes)

    Its a little hard to traverse the source and figure things out.

  • This is great! Thank you!
    It should be built in to the next version.

  • btw, do you know why this hasn't been built in?
    Does it have any negative side-effects?

  • @David: you should check out the designer tools module and its url alternate feature.

  • Thanks, that's very easy!
    I started reading the documentation just before the release of version 1.1, must have just overlooked this part.

  • I created a Folder and added this code but the lines starting with: workContext.Layout.Metadata...
    do not compile.

    Error says:
    One or more types required to compile a dynamic expression cannot be found. Are you missing references to Microsoft.CSharp.dll and System.Core.dll? D:\Visual Studio 2010\Projects\Orchard.Source.1.1.30\src\Orchard.Web\Themes\MyTheme\Filters\LayoutFilter.cs 23 4 Themes (Themes\Themes)

  • I needed to add the following using statement to get this to compile.

    using Microsoft.CSharp;

  • After finally compiling this code I cannot seem to have it hit a breakpoint to be sure it is working. Are there other dependencies needed to get this thing to work?

  • @Chiliyago: do you have debug=true in your web.config?

  • I'm using the Url Alternates feature of the Designer Tools module to achieve the same thing, which is great.

    However, I would really like to use the Razor Layout feature so that I could have a Master.cshtml which contains the primary layout markup, and then a Layout-url-home.cshtml template for the homepage and Layout.cshtml for all other content pages.

    Both Layout-url-home.cshtml and Layout.cshtml should use Master.cshtml as their Layout (in terms of the standard Razor view engine).
    Orchard uses its own view engine implementation which prohibits setting the Layout property.

    Right now I have duplicated HTML. What should I do?

  • @Skywalker: it seems like the layout is *not* the shape you want to vary on those pages. I'd need to know more about what you are trying to do but I'd guess that with a few deeper shapes and wrappers, you should be able to do exactly what you want. Feel free to e-mail me if you want to discuss that more specifically.

  • I'm basically trying to do exactly the same as you are doing in this article, but if you look at both your Layout-HomePage.cshtml and Layout-Orchard_Blogs.cshtml templates, they look very similar: for example, both templates define the Header zone.

    Although this works just fine, I would prefer to put the site markup structure (e.g. Head zone, Footer zone) in its own "master" template. Then Layout-HomePage.cshtml would only have "This is the home page!" and Layout-Orchard_Blogs.cshtml would only have "This is the blog!". The surrounding HTML would come from the "master" template.

    In ASP.NET MVC Razor views, you would achieve this by simply creating a _Layout.cshtml view, a Home.cshtml view and a Blogs.cshtml view. The latter two would set the Layout property to "_Layout.cshtml".

    This doesn't work in Orchard (for understandable reasons, Orchard works with the Shape system and is quite different I assume)

    If I'm still not making sense I will be happy to elaborate in a message demonstrating the issue and what I'm trying to accomplish.

    Thanks!

  • Well, if that's the extent of duplication, I wouldn't be that worried: a layout is just a few structural tags and top-level zone declarations; and maybe a small number of custom, single use shapes. If you have anything more than that you are probably doing something wrong. In that case, the Orchard layout is doing pretty much exactly what Razor layouts are doing. So the remark that you would "prefer to put the site markup structure in its own master template" makes little sense to me: that is exactly what the layout is supposed to be. That is how we designed it and the intent we had. As a side note, the PM in charge of Razor layouts was on the Orchard team when we designed this.
    This different title that you have there, and what your layout seems to be varying on currently, does *not* belong in layout. It should be a widget or a custom shape for which you have url alternates.

  • I had used your example here to alternate my homepage layout. This worked until I translated the page. With multiple languages, it seems that Layout-HomePage no longer applies to the default or translated pages.

    Can you think of what I may be doing to cause this behavior? I would like for all translations of the homepage to share Layout-HomePage.cshtml

  • @Matthew: I'm guessing you're not using Orchard 1.3 because that problem was fixed in the latest release.

  • I had already upgraded via source enlistment. The dashboard says I'm running version 1.3.9 but only one of the translations is marked as "HomePage" (RoutableHomePageProvider;50). I'm not sure how multiple translated pages should appear in the "Settings_SiteSettingsPartRecord" table.

    I'll see if I can track down the old issue on codeplex and check if my problem is the same.

  • Thanks. Please file a bug if necessary.

  • You're probably not looking at the right shape. The alternate should be layout-url-aboutpage.cshtml if you're using URL alternates.

  • I tried that too...it didn't work. Where can I see the list of all the alternate layouts that I can use for a page? Which shape do I need to check in shape tracing?

  • For the page you won't see it in shape tracing. But I certify you that layout-url-whatever.cshtml works if you've enabled the URL Alternates feature. I've done it dozens of time, and I'm staring at it right now.

  • Does this post still work in v1.4? With Autoroute and Alias there isn't really a hard specifier for the homepage; just the convention of using an empty custom pattern.

    What would the alternate layout be for a homepage at "~/"?
    I would think that "layout-url-.cshtml" would hit the mark, but it doesn't work.

  • Ignore my previous comment. I get it layout-url-homepage.cshtml works in place of Layout-HomePage.cshtml. It's a little confusing because in every other case we're talking about slugs except in the case of the homepage, but I understand we can't have something like Layout-url-/.cshtml

  • this works on 1.4 too

    just go to modules, gallery, and install "Designer Tools" and then enable the URL Alternates features

  • Brilliant. I was looking to customize the ListByArchives view of the Blog module. And since it returns a regular MVC view instead of an Orchard shape, I can't override the view template in my custom theme to include a sidebar. But providing an alternate layout for that exact controller action fixes it.

  • Oh, never mind my comment on not being able to override views that are returned from regular action methods. I can override regular views perfectly after all. I assumed I had to create a subfolder named after the action that returns the view, but I just needed to create a view with the same name directly in the Views folder of my theme.

  • @Eric: you no longer need this for the homepage: enable the url alternates feature (comes with Orchard now) and name your template layour-url-homepage.cshtml.

  • How to integrate theme to specific custom module in orchard

  • @pintu: I don't understand what that means. Please ask your question on the discussions forum on CodePlex.

  • Sorry im just trying to work out.. how do you acutally USE this.. i.e. how do map a given url or shape to one of these alternate layouts?

    I have a page where i'd like to list a bunch of elements and remove the header.. and then the homepage which has the header etc.. i just don't quite get how it's then hooked up..
    Cheers,
    Tom

  • @Tom: I'm suspecting you're looking at it backwards but you're not giving enough details for me to be able to tell. Please post on the CodePlex discussion forums with more details about what exactly you are trying to do.

Comments have been disabled for this content.