Microsoft Most Valuable Professional

Chris Pietschmann

An MVP From Wisconsin

Custom Themes in ASP.NET MVC Updated for Preview 5

About two weeks ago I posted on how to Implement Custom Theme support in ASP.NET MVC. There were some breaking changes made when the Preview 5 release was released yesterday.

Here's a short list to a couple of the changes I had to make to my code from the previous post to get it working in ASP.NET MVC Preview 5:

  • Delete WebFormThemeViewLocator - The contents of this object is now contained within the ViewEngine itself
  • Delete WebFormThemeControllerFactory- This isn't needed anymore.
  • Modify WebFormThemeViewEngine - Write a bunch of code that finds the appropriate View to use.
  • Modify Global.asax - Remove code that adds the old ControllerFactory, and replace it with code that adds our newly improved WebFormThemeViewEngine
  • Modify ControllerBase - Firstly, rename this to ThemeControllerBase since there is not a ControllerBase in System.Web.Mvc. Then, modify the code for the Execute method since it now takes in a RequestContext object as a parameter instead of a ControllerContext object.

Just for reference here's the code for the WebFormThemeViewEngine.

Download Code: ASPNETMVC_Preview5_CustomThemeImplementation.zip (226.05 kb)

 

Below is the entire code for the WebFormThemeViewEngine, just for reference. If you are interested in looking at how I implemented this, just download and check out the entire code sample at the link above.

using System;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;

public class WebFormThemeViewEngine : System.Web.Mvc.WebFormViewEngine
{
    public WebFormThemeViewEngine()
    {
        base.ViewLocationFormats = new string[] {
                "~/Views/{2}/{1}/{0}.aspx",
                "~/Views/{2}/{1}/{0}.ascx",
                "~/Views/{2}/Shared/{0}.aspx",
                "~/Views/{2}/Shared/{0}.ascx"
            };

        base.MasterLocationFormats = new string[] {
                "~/Views/{2}/{1}/{0}.master",
                "~/Views/{2}/Shared/{0}.master"
            };

        base.PartialViewLocationFormats = new string[] {
                "~/Views/{2}/{1}/{0}.aspx",
                "~/Views/{2}/{1}/{0}.ascx",
                "~/Views/{2}/Shared/{0}.aspx",
                "~/Views/{2}/Shared/{0}.ascx"
            };
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }
        if (string.IsNullOrEmpty(viewName))
        {
            throw new ArgumentException("Value is required.", "viewName");
        }

        string themeName = this.GetThemeToUse(controllerContext);

        string[] searchedViewLocations;
        string[] searchedMasterLocations;

        string controllerName = controllerContext.RouteData.GetRequiredString("controller");

        string viewPath = this.GetPath(this.ViewLocationFormats, viewName, controllerName, themeName, out searchedViewLocations);
        string masterPath = this.GetPath(this.MasterLocationFormats, viewName, controllerName, themeName, out searchedMasterLocations);

        if (!(string.IsNullOrEmpty(viewPath)) && (!(masterPath == string.Empty) || string.IsNullOrEmpty(masterName)))
        {
            return new ViewEngineResult(this.CreateView(controllerContext, viewPath, masterPath));
        }
        return new ViewEngineResult(searchedViewLocations.Union<string>(searchedMasterLocations));
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }
        if (string.IsNullOrEmpty(partialViewName))
        {
            throw new ArgumentException("Value is required.", partialViewName);
        }

        string themeName = this.GetThemeToUse(controllerContext);

        string[] searchedLocations;

        string controllerName = controllerContext.RouteData.GetRequiredString("controller");

        string partialPath = this.GetPath(this.PartialViewLocationFormats, partialViewName, controllerName, themeName, out searchedLocations);

        if (string.IsNullOrEmpty(partialPath))
        {
            return new ViewEngineResult(searchedLocations);
        }
        return new ViewEngineResult(this.CreatePartialView(controllerContext, partialPath));
    }

    private string GetThemeToUse(ControllerContext controllerContext)
    {
        string themeName = controllerContext.HttpContext.Items["themeName"] as string;
        if (themeName == null) themeName = "Default";
        return themeName;
    }

    private string GetPath(string[] locations, string viewName, string controllerName, string themeName, out string[] searchedLocations)
    {
        string path = null;

        searchedLocations = new string[locations.Length];

        for (int i = 0; i < locations.Length; i++)
        {
            path = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] { viewName, controllerName, themeName });
            if (this.VirtualPathProvider.FileExists(path))
            {
                searchedLocations = new string[0];
                return path;
            }
            searchedLocations[i] = path;
        }
        return null;
    }
}

Currently rated 3.7 by 3 people

  • Currently 3.666667/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: ,
Categories: ASP.NET MVC
Posted by crpietschmann on Friday, August 29, 2008 6:49 PM
Permalink | Comments (2) | Post RSSRSS comment feed


How To Setup Custom Theme Support In ASP.NET MVC Preview 4 using a Custom ViewEngine

Update 8/29/2008: I posted a new blog post today that contains updated code for this example that works with the newly released ASP.NET MVC Preview 5. The new code is located here.

One option to do theming in ASP.NET MVC is to use the standard ASP.NET Theme functionality (App_Themes folder glory and all), but it just doesn't seem complete. Also, by doing that you don't get to generate completely custom HTML specific to each Theme. After all, the beauty of ASP.NET MVC is being able to completely control the HTML output of the application. So, I played around with things a bit and figured out how to create a custom ViewEngine that will allow you to create a seperate sub-folder within the Views folder for each Theme. This way you can have a completely different version of each View for each Theme, and maintain complete control over the HTML output on a per Theme basis.

I used the ASP.NET MVC Preview 4 release to write this article.

Create Themed Views

First, we'll make some changes to the Views contained within the default ASP.NET MVC Template.

Here's a brief summary of what changes are needed:

  1. Create a sub-folder named "Default" within the Views folder, and paste all the files and folders within the Views folder into this folder. This will be out "Default" theme.
  2. Cut the Site.css file from the Content folder, and paste it into the Shared folder within the newly created "Default" theme folder.
  3. Modify the CSS link reference in the "Shared/Site.Master" file within the "Default" theme to reference the new location for the Site.css file.
  4. Modey all the .aspx View pages to reference the new location of the MasterPage file.

Once, the above changes are made we have our first "Default" theme created. Now to create a new theme, just create a copy of this folder and make sure to update all the references within the files to reference the theme folder the files are contained within.

To the right is a screenshot displaying the layout of the Theme folders as described above.

In the code example attached to this article, I created a second theme named "Red" and I changed the background color of the Site.css file to the color Red.

Create a Custom ViewEngine

I found the following article usefull and very descriptive on creating custom ViewEngines, so I'm not going to re-write it. Just refer to the following link on how to create a custom ViewEngine in ASP.NET MVC.

http://blog.maartenballiauw.be/post/2008/05/20/Creating-a-custom-ViewEngine-for-the-ASPNET-MVC-framework.aspx

Instead, I'll just list the code I wrote for setting up Themes and give a brief description of what it does.

First, we need to create a ViewLocator object that will contain VirtualPath references that describe to ASP.NET MVC how to find a specified View. To make things simpler I'm just inheriting from the WebFormViewLocator so I don't have to re-implement it's entire logic. Also, one thing to note is the constructor of our WebFormThemeViewLocator object takes a string argument that is the name of the Theme it is to describe the VirtualPath references for.

public class WebFormThemeViewLocator : System.Web.Mvc.WebFormViewLocator
{
    public WebFormThemeViewLocator(string themeName)
    {
        base.ViewLocationFormats = new string[] {
            "~/Views/" + themeName + "/{1}/{0}.aspx",
            "~/Views/" + themeName + "/{1}/{0}.ascx",
            "~/Views/" + themeName + "/Shared/{0}.aspx",
            "~/Views/" + themeName + "/Shared/{0}.ascx"
        };

            base.MasterLocationFormats = new string[] {
            "~/Views/" + themeName + "/{1}/{0}.master",
            "~/Views/" + themeName + "/Shared/{0}.master"
        };
    }
}

Next, we need to create a custom ViewEngine. Also, in this case, I'm inheriting the WebFormViewEngine so I don't have to re-implement it's entire logic. This WebFormThemeViewEngine just overrides the RenderView method so that it can get the Theme to display from the HttpContext.Items collection and implement the new WebFormThemeViewLocator instead of the default WebFormViewLocator.

public class WebFormThemeViewEngine : System.Web.Mvc.WebFormViewEngine
{
    protected override void RenderView(System.Web.Mvc.ViewContext viewContext)
    {
        string themeName = viewContext.HttpContext.Items["themeName"] as string;
        if (themeName == null) themeName = "Default";

        base.ViewLocator = new WebFormThemeViewLocator(themeName);

        base.RenderView(viewContext);
    }
}

Next, we need to create a custom ControllerFactory that we'll setup ASP.NET MVC to use so we can use our custom ViewEngine. Again, I'm inheriting the DefaultControllerFactory so I only have to add the necessary logic to use the custom ViewEngine so I don't have to re-implement it's entire logic.

public class WebFormThemeControllerFactory : System.Web.Mvc.DefaultControllerFactory
{
    protected override System.Web.Mvc.IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
    {
        System.Web.Mvc.Controller controller = base.CreateController(requestContext, controllerName) as System.Web.Mvc.Controller;
        controller.ViewEngine = new WebFormThemeViewEngine();
        return controller;
    }
}

Finally, to tell ASP.NET MVC to use the WebFormThemeControllerFactory object, and ultimately our custom ViewEngine, the following line of code needs to be added to the Application_Start event handler:

ControllerBuilder.Current.SetControllerFactory(typeof(WebFormThemeControllerFactory));

Dynamically Determine Which Theme to Display to the User

The way I've set things up in the above code for the custom ViewEngine, I've wired it up so that you can set the Theme to display anywhere within your Controller. This gives the most flexibility I could think of to set the Theme since you'll most likely need to access a database of some kind to determine which Theme to display, and this way you can do that before, after or at the same time you get the data to display.

Here's an example of how to set the Theme to "Red", and you can do this anywhere within one of your Action methods of your Controller.

controllerContext.HttpContext.Items["themeName"]

The reason I'm storing the Theme Name using HttpContext.Items is so that we can access the Theme Name from a single place within both the custom ViewEngine and the View itself.

Also, the code example attached to this article uses a ControllerBase class that each of the Controllers inherit so it's more resuable, and it also grabs the Theme to display from the QueryString for simplicity.

Sharing UI Elements Between Themes

One thing that is done alot is sharing certain UI elements between Themes that are similar or almost identical. This can be done by creating user controls within a seperate folder than the View folder, and using/referencing them within the Pages/Views (.aspx) or Controls (.ascx) within the Themes. This way you can create shared UI elements that can be shared across multiple Themes.

In the code attached to this article, I've created a "~/Controls" folder and placed in it a ThemeLinks.ascx control that is used by both the "Default" and "Red" themes to display links (as tabs in the top navigation bar) for you to select which Theme to see.

Download the Code

Ok, after reading all the above, I'm sure you're probably a little confuses since I only gave a brief overview. Well, here's the code. I started with an ASP.NET MVC Website (as described in one of my previous posts) using the Preview 4 release and made all the changes described above. Have fun, and please post any comments you have on this, or any suggestions on making it better.

Download Code: ASPNETMVC_CustomThemeImplementation.zip (207.98 kb)

Currently rated 5.0 by 2 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: ,
Categories: asp.net | ASP.NET MVC
Posted by crpietschmann on Sunday, August 17, 2008 12:09 AM
Permalink | Comments (3) | Post RSSRSS comment feed


How To Use ASP.NET MVC in Website rather than Web Application Project

I recently started getting to know the ASP.NET MVC Framework (more specifically the Preview 4 release). The first thing I noticed upon installation is that it only comes with a Web Application Project Template. There is no regular ASP.NET Website template. So, I decided to try converting the Web Application Project Template to a regular ASP.NET Website. I actually prefer to use Website projects instead of Web Application projects since it gives a little more flexibility to deploy changes to the website after the website is already live. Plus, you can still throw anything you want into a DLL that goes into the Bin if you want.

I only needed to make a couple small changes when copying over the code into a new ASP.NET Website:

  1. Convert the .aspx pages from using CodeBehind to CodeFile.
  2. Place the Controllers folder into the App_Code.
  3. Change the Global.asax to be ASP.NET Website friendly (aka Not compile into DLL like in Web Application Project)

That's pretty much it, now I have a nice ASP.NET MVC Website template that I can use. To the right is an image of the file layout of the new ASP.NET MVC Website project.

The ASP.NET MVC release I used for this was the ASP.NET MVC Preview 4 release available on CodePlex.

Also, here's a link to download the code: ASP.NET MVC_Preview4_Website Project.zip (192.03 kb)

Currently rated 5.0 by 3 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags: ,
Categories: asp.net | ASP.NET MVC
Posted by crpietschmann on Friday, August 15, 2008 4:15 PM
Permalink | Comments (2) | Post RSSRSS comment feed


About the author

I'm Chris Pietschmann, go to the About Me page to learn more about me.

Search

Sponsors

Web.Maps.VE - ASP.NET AJAX Virtual Earth Mapping Server Control

Recent comments

Disclaimer


This work is licensed under a Creative Commons Attribution 3.0 United States License, unless explicitly stated otherwise within the posted content.
© Copyright 2004 - 2008 Chris Pietschmann