Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

February 2011 - Posts

Dynamically switching the theme in Orchard

(c) Bertrand Le Roy 2010It may sound a little puzzling at first, but in Orchard CMS, more than one theme can be active at any given time. The reason for that is that we have an extensibility point that allows a module (or a theme) to participate in the choice of the theme to use, for each request. The motivation for building the theme engine this way was to enable developers to switch themes based on arbitrary criteria, such as user preferences or the user agent (if you want to serve a mobile theme for phones for example). The choice is made between the active themes, which is why there is a difference between the default theme and the active themes.

In order to have a say in the choice of the theme, all you have to do is implement IThemeSelector. That interface is quite simple as it only has one method, GetTheme, that takes the current RequestContext and returns a ThemeSelectorResult or null if the implementation of the interface does not want to participate in the current request (we'll see an example in a moment). ThemeSelectorResult itself is just a ThemeName string property and an integer Priority. We're using a priority so that an arbitrary number of implementations of IThemeSelector can contribute to the choice of a theme.

If you look for existing implementations of the interface in Orchard, you'll find four:

  • AdminThemeSelector: selects the TheAdmin theme with a very high priority (100) if the current request is for a page that is part of the admin. Otherwise, null is returned, which enables other implementations to choose the theme.
  • PreviewThemeSelector: selects the preview theme if there is one, with a high priority (90), and null otherwise. This enables administrators to view the site under a different theme while everybody else continues to see the current default theme.
  • SiteThemeSelector: this is the implementation that is doing what you expect most of the time, which is to get the current theme from site settings and set it with a priority of –5.
  • SafeModeThemeSelector: this is the fallback implementation, which should almost never win. It sets the theme as the safe mode theme, which has no style and just uses the default templates for everything. The priority is very low (-100).

While this extensibility mechanism is great to have, I wanted to bring that level of choice into the hands of the site administrator rather than just developers. In order to achieve that, I built the Vandelay Theme Picker module.

The module provides administration UI to create rules for theme selection.The theme picker admin UI

It provides its own extensibility point (the IThemeSelectionRule interface) and one implementation of a rule: UserAgentThemeSelectorRule. This rule gets the current user agent from the context and tries to match it with a regular expression that the administrator can configure in the admin UI.

You can for example configure a rule with a regular expression that matches IE6 and serve a different subtheme where the stylesheet has been tweaked for such an antique browser. Another possible configuration is to detect mobile devices from their agent string and serve the mobile theme. All those operations can be done with this module entirely from the admin UI, without writing a line of code.

The module also offers the administrator the opportunity to inject a link into the front-end in a specific zone and with a specific position that enables the user to switch to the default theme if he wishes to. This is especially useful for sites that use a mobile theme but still want to allow users to use the full desktop site.

While the module is nice and flexible, it may be overkill. On my own personal blog, I have only two active themes: the desktop theme and the mobile theme. The desktop and mobile themes side by side

I'm fine with going into code to change the criteria on which to switch the theme, so I'm not using my own Theme Picker module. Instead, I made the mobile theme a theme with code (in other words there is a csproj file in the theme). The project includes a single C# file, my MobileThemeSelector for which the code is the following:

public class MobileThemeSelector : IThemeSelector {
    private static readonly Regex _Msie678  =
new Regex(@"^Mozilla\/4\.0 \(compatible; MSIE [678]" +
@"\.0; Windows NT \d\.\d(.*)\)$",
RegexOptions.IgnoreCase); private ThemeSelectorResult _requestCache; private bool _requestCached; public ThemeSelectorResult GetTheme(RequestContext context) { if (_requestCached) return _requestCache; _requestCached = true; var userAgent = context.HttpContext.Request.UserAgent; if (userAgent.IndexOf("phone",
StringComparison.OrdinalIgnoreCase) != -1 || _Msie678.IsMatch(userAgent) || userAgent.IndexOf("windows live writer",
StringComparison.OrdinalIgnoreCase) != -1) { _requestCache = new ThemeSelectorResult { Priority = 10, ThemeName = "VuLuMobile" }; } return _requestCache; } }

The theme selector selects the current theme for Internet Explorer versions 6 to 8, for phones, and for Windows Live Writer (so that the theme that is used when I write posts is as simple as possible).

What's interesting here is that it's the theme that selects itself here, based on its own criteria.

This should give you a good panorama of what's possible in terms of dynamic theme selection in Orchard. I hope you find some fun uses for it. As usual, I can't wait to see what you're going to come up with…

The best way to learn how to extend Orchard

(c) Bertrand Le Roy 2011We do have tutorials on the Orchard site, but we can't cover all topics, and recently I've found myself more and more responding to forum questions by pointing people to an existing module that was solving a similar problem to the one the question was about.

I really like this way of learning by example and from the expertise of others. This is one of the reasons why we decided that modules would by default come in source code form that we compile dynamically. it makes them easy to understand and easier to modify for your own purposes. Hackability FTW!

But how do you crack open a module and look at what's inside?

You can do it in two different ways.

First, you can just install the module from the gallery, directly from your Orchard instance's admin panel. Once you've done that, you can just look into your Modules directory under the web site. There is now a subfolder with the name of the new module that contains a csproj that you can open in Visual Studio or add to your Orchard solution.

Second, you can simply download the package (it's NuGet) and rename it to a .zip extension. NuGet being based on Zip, this will open just fine in Windows Explorer:What's inside a module's NuGet package

What you want to dig into is the Content/Modules/[NameOfTheModule] folder, which is where the actual code is.

Thanks to Jason Gaylord for the idea for this post.

Storing non-content data in Orchard

Dry earthA CMS like Orchard is, by definition, designed to store content. What differentiates content from other kinds of data is rather subtle. The way I would describe it is by saying that if you would put each instance of a kind of data on its own web page, if it would make sense to add comments to it, or tags, or ratings, then it is content and you can store it in Orchard using all the convenient composition options that it offers. Otherwise, it probably isn't and you can store it using somewhat simpler means that I will now describe.

In one of the modules I wrote, Vandelay.ThemePicker, there is some configuration data for the module. That data is not content by the definition I gave above. Let's look at how this data is stored and queried.

The configuration data in question is a set of records, each of which has a number of properties:

public class SettingsRecord {
    public virtual int Id { get; set;}
    public virtual string RuleType { get; set; }
    public virtual string Name { get; set; }
    public virtual string Criterion { get; set; }
    public virtual string Theme { get; set; }
    public virtual int Priority { get; set; }
    public virtual string Zone { get; set; }
    public virtual string Position { get; set; }
}

Each property has to be virtual for nHibernate to handle it (it creates derived classed that are instrumented in all kinds of ways). We also have an Id property.

The way these records will be stored in the database is described from a migration:

public int Create() {
    SchemaBuilder.CreateTable("SettingsRecord",
        table => table
            .Column<int>("Id", column => column.PrimaryKey().Identity())
            .Column<string>("RuleType", column => column.NotNull().WithDefault(""))
            .Column<string>("Name", column => column.NotNull().WithDefault(""))
            .Column<string>("Criterion", column => column.NotNull().WithDefault(""))
            .Column<string>("Theme", column => column.NotNull().WithDefault(""))
            .Column<int>("Priority", column => column.NotNull().WithDefault(10))
            .Column<string>("Zone", column => column.NotNull().WithDefault(""))
            .Column<string>("Position", column => column.NotNull().WithDefault(""))
        );
    return 1;
}

When we enable the feature, the migration will run, which will create the table in the database.

Once we've done that, all we have to do in order to use the data is inject an IRepository<SettingsRecord>, which is what I'm doing from the set of helpers I put under the SettingsService class:

private readonly IRepository<SettingsRecord> _repository;
private readonly ISignals _signals;
private readonly ICacheManager _cacheManager;

public SettingsService(
    IRepository<SettingsRecord> repository,
    ISignals signals,
    ICacheManager cacheManager) {
    _repository = repository;
    _signals = signals;
    _cacheManager = cacheManager;
}

The repository has a Table property, which implements IQueryable<SettingsRecord> (enabling all kind of Linq queries) as well as methods such as Delete and Create.

Here's for example how I'm getting all the records in the table:

_repository.Table.ToList()

And here's how I'm deleting a record:

_repository.Delete(_repository.Get(r => r.Id == id));

And here's how I'm creating one:

_repository.Create(new SettingsRecord {
    Name = name,
    RuleType = ruleType,
    Criterion = criterion,
    Theme = theme,
    Priority = priority,
    Zone = zone,
    Position = position
});

In summary, you create a record class, a migration, and you're in business and can just manipulate the data through the repository that the framework is exposing. You even get ambient transactions from the work context.

Caching items in Orchard

(c) Bertrand Le Roy 2010Orchard has its own caching API that while built on top of ASP.NET's caching feature adds a couple of interesting twists.

In addition to its usual work, the Orchard cache API must transparently separate the cache entries by tenant but beyond that, it does offer a more modern API.

Here's for example how I'm using the API in the new version of my Favicon module:

_cacheManager.Get(
    "Vandelay.Favicon.Url",
    ctx => {
        ctx.Monitor(_signals.When("Vandelay.Favicon.Changed"));
        var faviconSettings = ...;
        return faviconSettings.FaviconUrl;
    });

There is no need for any code to test for the existence of the cache entry or to later fill that entry. Seriously, how many times have you written code like this:

var faviconUrl = (string)cache["Vandelay.Favicon.Url"];
if (faviconUrl == null) {
    faviconUrl = ...;
    cache.Add("Vandelay.Favicon.Url", faviconUrl, ...);
}

Orchard's cache API takes that control flow and internalizes it into the API so that you never have to write it again. Notice how even casting the object from the cache is no longer necessary as the type can be inferred from the return type of the Lambda.

The Lambda itself is of course only hit when the cache entry is not found. In addition to fetching the object we're looking for, it also sets up the dependencies to monitor.

You can monitor anything that implements IVolatileToken. Here, we are monitoring a specific signal ("Vandelay.Favicon.Changed") that can be triggered by other parts of the application like so:

_signals.Trigger("Vandelay.Favicon.Changed");

In other words, you don't explicitly expire the cache entry. Instead, something happens that triggers the expiration. Other implementations of IVolatileToken include absolute expiration or monitoring of the files under a virtual path, but you can also come up with your own.

More Posts