Update: Please read the update at the end of this post as this code will no longer work with ASP.NET MVC 2.0 RC +.
In the last two weeks I have seen this technique spoken about twice! It is a hot topic at the moment…. how do you have multiple themes for an ASP.NET MVC web application and then set the desired theme dynamically? There are multiple ways to implement this and in this post I will explain my preferred method. I like to have multiple master pages with each master page linking to its own set of assets and styles, then dynamically changing the master page used based on a route parameter.
routes.MapRoute(
"Default",
"{theme}/{controller}/{action}/{id}",
new { theme ="Default", controller = "Home", action = "Index", id = "" }
);
Firstly we need to setup our route to accept a theme parameter in the RegisterRoutes method located in the global.asax. Once this is implemented we will be able to browse to URL’s such as http://www.site.com/MyCoolTheme/Home/Index.
Now you will need to set the master page on every request based on the value set for theme route parameter which is inferred from the url. One way to do this is to override how the default view engine resolves which master page to use. Below we create a class that inherits from the WebFormViewEngine which is the default view engine used by the MVC framework. Now you have the ability to override how certain features of the ViewEngine behave.
When the CreateView method is overridden you then have access to the master page path set on each request.
I decided to setup a folder called theme at the root of my application which in turn has subfolders. Each subfolders name is the name of the theme which contains a master page called site.master. Each master page links to its own set of assets and styles, so depending on which master page is set will determine the theme used for the request.
The CreateView method now checks to make sure that the view being requested requires a master page and if it does not it will immediately return and call on the base method and execution will carry on as normal.
If the view being requested does require a master page the theme value that has been set in the RouteValueCollection will be inserted into the theme file path location.
If a master page called Site.Master exists on disk at the constructed file path location then it will be used when the view is served back to the user. If for some reason (a user typing in junk on the url) a file called Site.Master does not exist on disk at the constructed file path location then the default master page for the site will be used.
public class ThemedViewEngine : WebFormViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath,
string masterPath)
{
//some views may not have a master page - if the masterPath name is null or empty
//return without changing anything as the view has no master
if (string.IsNullOrEmpty(masterPath))
return base.CreateView(controllerContext, viewPath, masterPath);
//sets the path to the master page which will be manuipulated based on
//the theme route parameter value
const string path = "~/Theme/{0}/Site.Master";
//sets the file location of the default theme - which is changed if the theme route
//value has been set..
string defaultTheme = "~/Theme/Default/Site.Master";
//if the theme value has been set then manupulate which master page to use...
if (controllerContext.RouteData.Values["theme"] != null)
{
string themeMasterPath =
string.Format(path, controllerContext.RouteData.Values["theme"]);
//Check the path exists - if it does not the default theme will be used..
if(System.IO.File.Exists(controllerContext.HttpContext.
Server.MapPath(themeMasterPath)))
defaultTheme = themeMasterPath;
}
//call the create view method with the new information set....
return base.CreateView(controllerContext, viewPath, defaultTheme);
}
}
Now all you need to do is clear the default view engine from the ViewEngines collection which is set in the global.asax and add our new altered implementation of the web forms view engine to the collection.
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ThemedViewEngine());
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
Now when you browse to a URL on the site setting a theme that exists then the master page will be overridden to use the currently requested theme’s master page for that individual request. So calling http://localhost/Default/Home/Index the user will see the default theme and alternatively calling http://localhost/Dark/Home/Index the user will see the dark theme.
As always you can download the demo code from here. (Please note you will need MVC 2.0 RC, VS2008 SP1 Standard / Pro / Team to run this solution).
Update: This no longer works with MVC 2.0 RC + as the masterPath no longer get’s set when the CreateView method is called. A quick fix is as follows:-
Create an attribute called theme:
public class ThemeAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Controller.ViewData.Add("__theme", true);
base.OnActionExecuting(filterContext);
}
}
Place the theme attribute on your controller or actions:
public class BlogController : Controller
{
[Theme]
public ActionResult Home()
{
return View();
}
public ActionResult Go()
{
return View();
}
}
}
Update the View engine to check for the __theme key added to ViewData:
public class ThemedViewEngine : WebFormViewEngine
{
protected override IView CreateView(ControllerContext controllerContext, string viewPath,
string masterPath)
{
//Check to see if the action requires a theme to be set
if (!controllerContext.Controller.ViewData.ContainsKey("__theme"))
return base.CreateView(controllerContext, viewPath, masterPath);
//sets the path to the master page which will be manuipulated based on the theme setting
const string path = "~/Theme/{0}/Site.Master";
//manipulate which master page to use...
string themeMasterPath = string.Format(path, ThemeConfiguration.Theme);
//Check the path exists
if (File.Exists(controllerContext.HttpContext.Server.MapPath(themeMasterPath)))
//call the create view method with the new information set....
return base.CreateView(controllerContext, viewPath, themeMasterPath);
//If not theme and master page is found then throw
throw new FileNotFoundException("A master page called site.master cannot be found in the folder "
+ themeMasterPath);
}
}
I actually prefer this method of controlling which actions / controllers get themed via an attribute as it gives a more granulated control over what gets themed and what doesn’t.
Please remember not to place the theme attribute on actions that do not use a master page else you will receive a HttpException informing you that content controls have to be top level.
Writing code on top of RC and BETA is always fun hey! Sorry if this caught anyone out prior my update!