MySite - Part 2 - Theming Support

13. November 2008 14:55

Okay, I lied.  I really wanted to figure out how to get theming to work the way I wanted. As you can see to the right, I created a "Theme" folder, where I have a "Default" and in this case a "Red" theme.  Under the theme's folder, I moved the Content, and Views folders.  My goal was to allow for a fallback to using a View from the Default theme, but still allow for the appropriate masterpage to be applied from the theme's folder if one existed.  In this case, you can see that there is an Index view for the Home controller within the Default theme, but there is no matching view in the Red theme.

Project Tree

I started off with the view engine from Chris Pietschmann's post on the issue of theming.  The two things that weren't addressed in his post were how to account for falling back to Default (though there is a search pattern), and more importantly, how to apply the correct masterpage to a number of views within the default fallback.

The first part was easy enough, I simply added a "Default" search to his existing methods.  The second part is where things got tricky.  Thankfully the ASP.Net MVC Framework's source code is available for study, which was really helpful in inheriting enough functionality from the WebFormViewEngine and WebFormView classes to make this theming work the way I wanted it to.

What I did was to add an override for the CreateView method on the view engine class that used my own view class, which inherited from the WebFormView.  In order to make the view work, I lifted a few internal classes (namely the BuildManager) from the MVC project.  I found that the name of the MasterPageFile within the page class wasn't available until fairly low in the event chain.

override public void Render(ViewContext viewContext, TextWriter writer)
{
    if (viewContext == null)
    {
        throw new ArgumentNullException("viewContext");
    }

    object viewInstance = BuildManager.CreateInstanceFromVirtualPath(ViewPath, typeof(object));
    if (viewInstance != null)
    {
        ViewPage viewPage = viewInstance as ViewPage;
        if (viewPage != null)
        {
            RenderViewPage(viewContext, viewPage);
            return;
        }
    }

    base.Render(viewContext, writer);
}

As you can see, within the MySiteView class I had to override the Render method for the view I follow the same logic as within the WebFormView class' Render method up to the point where the RenderViewPage method is called, which is where the real magic happens.

private void RenderViewPage(ViewContext context, ViewPage page)
{
    if (!String.IsNullOrEmpty(MasterPath)) {
        page.MasterLocation = MasterPath;
    } else {
        if (HttpContext.Current.Items["themeName"].ToString() != "Default" && page.TemplateSourceDirectory.Contains("/Themes/Default/"))
            page.PreInit += new EventHandler(delegate(object sender, EventArgs e){
                //test for Default theme path, and replace with current theme
                string defaultthemepath = string.Format("{0}Themes/Default/", page.Request.ApplicationPath);
                if (!string.IsNullOrEmpty(page.MasterPageFile) && page.MasterPageFile.ToLower().StartsWith(defaultthemepath.ToLower()))
                {
                    string newMaster = string.Format(
                        "~/Themes/{0}/{1}",
                        HttpContext.Current.Items["themeName"],
                        page.MasterPageFile.Substring(defaultthemepath.Length)
                    );
                    if (File.Exists(page.Server.MapPath(newMaster)))
                        page.MasterLocation = newMaster;
                }
            });
    }

    page.ViewData = context.ViewData;
    page.RenderView(context);
}

If there wasn't a match for a MasterPage, the user isn't using the Default engine and the view came from the Default theme, I inject an anonymous delegate into the page's PreInit event since the value for the MasterPageFile isn't available until this point.  What I do then, is check and see if there is a corresponding MasterPage within the current theme, and use that instead.  This makes the whole thing work as I would expect.

One other point of code was to make the theme settable, and changable from a number of places.  What I did to support this was create an Application_BeginRequest method within the project's Global.asax.cs file.  You can see how this works below.

protected void Application_BeginRequest(Object Sender, EventArgs e)
{
    SetTheme();
}

private void SetTheme()
{
    //set the theme for the ViewEngine to utilize.
    HttpContext context = HttpContext.Current;
    if (context.Items.Contains("theme")) context.Items.Remove("theme");
    context.Items.Add(
        "themeName",
        CheckTheme(context.Request.QueryString["theme"])
            ?? CheckTheme(context.Request.Form["theme"])
            ?? CheckTheme(context.Request.Headers["theme"])
            ?? CheckTheme((context.Request.Cookies["theme"] == null) ? "" : context.Request.Cookies["theme"].Value)
            ?? CheckTheme((context.Session == null) ? "" : context.Session["theme"] as string)
            ?? CheckTheme(ConfigurationSettings.AppSettings["theme"])
            ?? "Default"
   );
}

private string CheckTheme(string themeName)
{
    if (!string.IsNullOrEmpty(themeName))
    {
        var path = HttpContext.Current.Server.MapPath(string.Format(
            "~/Themes/{0}",
            themeName
        ));
        if (Directory.Exists(path))
            return themeName;
    }
    return null;
}

private string CheckTheme(HttpCookie cookie) {
    if (cookie != null)
        return CheckTheme(cookie.Value);
    return null;
}

With the functionality changes I've made, I can now move forward with some refactoring, and segmentation of logic.  I haven't created any tests for this either, as it was essentially an experiment on getting the theming to work the way I wanted it to.  It's nice that the ASP.Net MVC framework from Microsoft allows for the level of modification that it does.  It would be nice to see similar functionality baked in though.  I'm including my solution's source code, as it is right now below.

 

MySite.zip (181.33 kb)

Tags: , , ,

Comments

11/13/2008 4:20:53 PM #

trackback

Trackback from DotNetKicks.com

MySite - Part 2 - Theming Support

DotNetKicks.com |

Comments are closed

Tracker1

Michael J. Ryan aka Tracker1

My name is Michael J. Ryan and I've been developing web based applications since the mid 90's.

I am an advanced Web UX developer with a near expert knowledge of JavaScript.