Styling a Site using Themes

Mar 3, 01:43 pm

Introduction

As a developer, the bulk of your job likely entails putting together functional applications. In the case of web applications, you may spend a lot of time designing and developing code that communicates with a data layer and controls user input, reacting to button clicks, and authenticating users. The other half of web application development is styling a site so that it looks and feels as the customer wants it to. In the previous article in this series, I introduced some of the tools to assist with styling your site - in particular, the concept of master pages in .NET 2, and how they can be used to define where content can be added to a page, and to provide a common template, possibly containing a selection of controls, which will be available to each page that uses that master page as a base.

However, once you have defined a master page for your site that controls what should appear on each page, you need to consider how the site will appear to your users. You may find yourself arguing over the color of a button, or the position of a menu, even though a developed site works as intended. Styling a site is important, and with Themes in ASP.NET v2, it's even simpler to design a site that looks and feels right. This article delves into the world of theming and styling a site, using the functionality described in the previous article as a structural framework, and presenting that content using a couple of different themes.

So what exactly can themes do? Well, as you are probably aware, CSS is a fantastic way not only to style elements, but to apply some really quite complex formatting (including absolute or relative positioning) to any element on a page. If you are a regular user of CSS for keeping your styling in a central location, you'll be aware that styling ASP.NET controls isn't always as easy as it sounds. Sure, you can use the CssClass attribute of the calendar control to apply a CSS style, but does that accurately style your calendar? Hell no - you've got to apply styles to the header, selected date, other month dates, and so on. And each calendar control you add to a site has to have these same styles applied to each part of the control - it soon becomes tiresome!

Now, using themes, you can not only apply a CSS style sheet file to all pages in a site by placing a copy of the selected sheet in a central location, but you can also define in one central location how controls can interact with the stylesheet, and define styles for specific ASP.NET elements. You can for example define how all the calendars on your site should appear by defining the appearance for one single calendar in a theme, and this will be applied automatically to each instance of a calendar control on your site. This technique also makes it easy to switch between different themes for a site with minimal fuss, which I'll look at in this article.

In addition, the themes can be applied in one of two ways. Firstly, you can apply a theme as a stylesheet theme, which means that you can apply themes to a site that control the look and feel, but you can still override individual styles by manually adding style attributes to your controls.

Themes an also be applied as customization themes, which forces the entire site to obey the rules specified in the theme, and overriding any style tags that may have been added manually.

System Requirements

To run the code in this article, you'll need the following:

  • Windows 2000, XP (any version), or .NET Server 2003
  • A copy of Visual Web Developer 2005 (or full Visual Studio 2005)
  • A machine (real or virtual) with at least 300Mb of RAM (512Mb or more is great) if you want the design-time experience to be smooth and usable!

Installing and Compiling the Sample Code

As with the previous article in this series, the sample code for this article is an example of a simple web application that makes use of the techniques described, including master pages, themes, and skins. To install the application, all you need to do is extract the contents of MediaLibrary2.zip into a folder on your development box. Open up Visual Studio .NET 2005 and select File | Open | Web Site... from the main menu. Enter the path to the MediaLibrary2 folder in the dialog and open it - the site can then be run by selecting an ASPX page from the Solution Explorer and hitting the run command on the main toolbar. This will launch the application using the local-only Visual Studio .NET web server so you can test it out without having to create a virtual directory in IIS.

Applying Styling to a Site

Once you have the layout that you want to use for the site (or at the least a set of elements that can be positioned on a page), it's time to add a common theme to the site. A theme can consist of a combination of a css file and one or many skin files that control how ASP.NET server controls are rendered. These files are all contained within a folder, the name of which determines the theme. This folder, in turn, resides within a parent app_Themes folder. Any subfolders of the app_Themes folder act as themes for a site. Hence if you have a folder called Loud within the app_Themes folder, you can apply that theme to a page using the following declaration:

<%@ Page ... Theme="Loud" %>

Alternatively, you can add a declaration to the web.config file, within the System.Web element:

<pages theme="Loud" />

All pages in an application will use the theme specified in the web.config file unless a Theme attribute appears in the Page declaration, in which case, the selected theme for that page will override any selected theme in the web.config file.

There is a third technique you could use, which I'll show you later in this article, which is to store theme preferences in a user profile.

Applying CSS Stylesheets

In order to apply a default stylesheet for a theme, all you need to do is create a .css file within the appropriate theme folder in the Themes directory that contains the definitions you require. For example, you could place a css file called LoudStylesheet.css within the Themes\Loud folder. When you select the Loud theme in either the web.config , or in the @page directive in a page, this css file is automatically applied. Note that you can still apply a stylesheet manually in the HTML <head ... > tag. You can also access the Head control programmatically in your code, since you can add a runat="server" to the <head ... > tag just like any other element. This means that you could specify a link to a css file or set the value of any other element that resides within a head block from your code.

You can actually place more than one stylesheet file within a theme folder - in which case, ASP.NET will attempt to apply all stylesheets in that theme folder to the site, combining definitions where possible. Note, however, if you defined the same style in two different files, the style defined in the file that comes furthest down the alphabet will be applied to the site.

Here's an extract from the LoudStylesheet.css file, the full source of which is available to download along with this article.

body
{
  color: white;
  font-family: 'Century Gothic' , Arial, Sans-Serif;
  background-color: midnightblue;
  margin: 0;
}
.Header
{
  font-weight: bold;
  color: midnightblue;  
  border-bottom: yellow thin solid;
  background-color: orangered;
  border-right: yellow thin solid;
  margin-bottom:20px;
  width:800px;
}
.PageHead
{
  width: 640px;
  text-align: left;
  float:left;
  vertical-align:text-top;
}
.Corner
{
  width: 150px;
  vertical-align: middle;
  float:right;
}

This stylesheet can be accessed by elements defined in the skin file associated with the theme, and can also be used be elements and controls defined on the main pages of the site directly. Let's move on to looking at using skin files with both explicit styles and by linking skinned controls to css styles.

Using Theme .skin Files

Skins look a bit like ASP.NET files in that they contain server control definitions, and are used to alter the visual appearance of ASP.NET elements. For example:

<asp:button BackColor="orangered" ForeColor="midnightblue" 
  Font-Name="Arial Narrow" runat="server" />

This is an example of the contents of a skin file. Notice that you don't need anything else in the file - this one line of code alone will automatically apply the specified formatting to any asp:Button elements that are placed on a page to which the particular theme is applied. The control definitions must include a runat="server" attribute, but they should never include an id attribute. Also, the style attributes for a control can be set to standard styles, or linked to the styles defined in a css sheet, for example, the following declaration exists in quiet.skin :

<asp:dropdownlist runat="server" cssclass="shadedback"></asp:dropdownlist>

This links to the following definition in quiet.css :

.ShadedBack
{
  background-color:darkkhaki;
}

You can add multiple entries for the same type of control to a skin file, identifying each one individually using a SkinID attribute. This means that you could have, for example, more than one type of label control on a page.

Adding the following line to the skin file means that you can style the header on a page quite simply:

<asp:label runat="server" Font-Size="36px" font-style="italic" 
  ForeColor="White" SkinID="HeaderText" />

To use this skin definition, I added a simple attribute to one of the controls on the master page for the site:

<asp:label runat="server" id="HeaderText"  text="Welcome to the site!" 
  skinid="HeaderText" />

If I hadn't added this attribute, the label would remain unstyled. And if I were to add any other label controls to either the master page or to any content pages, these labels would not be styled with the HeaderText definition unless the SkinID is set.

I created a default.aspx page as a content page based on the master page defined earlier, and added the following controls to the content tag on the page:

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" 
      AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" 
      title="Media Library Home" %>
<asp:Content ID="Content1" ContentPlaceHolderID="PageBodyPlaceHolder" 
  Runat="Server">
  Welcome to the ASP Today Media Library!<br /><br />
  <asp:label id="label1" runat="server" 
     text="Select your preferred theme and click Apply" />&nbsp;<br />
  <asp:DropDownList ID="ddlThemePref" runat="server" name="ddlThemePref">
    <asp:ListItem>Quiet</asp:ListItem>
    <asp:ListItem Value="Loud"></asp:ListItem>
  </asp:DropDownList>
  <asp:Button ID="btnSetTheme" runat="server" Text="Apply" />
</asp:Content>

This highlights the fact that the header label (from the master) has different formatting applied compared with the standard label on the page, as shown in Figure 1 (notice I've switched to using the rather tasteless Loud theme in this part of the article):

Figure 1. Applying a theme
This figure has been reduced in size to fit in the text. To view the full image Click here

A skin file can be used to apply styling and formatting to any ASP.NET element on a page, but you can't specify attributes that are non-visual by default. For example, you couldn't add a standard NavigateUrl attribute to be applied to all asp:Hyperlink controls on a site. This is a design feature that will help to ensure that the skin file is only ever used to apply formatting to a page.

When I first saw how Visual Web Developer made it possible to use the all-on-one-page coding model that Visual Studio .NET 1.x used to loathe, I was a bit concerned as to the impact that this would have to the whole paradigm of working on a project, as a developer, with separate designers for a site. However, I reckon that this skinning functionality will help to keep some of those fears at bay, since it makes it extremely simple to alter the display and layout of a page using themes, css and skin files without affecting the content.

Themes and User Controls

I mentioned earlier that you can't by default specify non-visual elements on existing controls, but what about user controls? Well, the good news is that it's not hard to skin user controls by adding a couple of small modifiers to a control.

In order to display information on this site, I created a user control, ItemsDisplay.ascx , which is designed to display information about CDs or DVDs on my site. A list of sample CDs and DVDs is included in the code for this article - here's an extract from that file ( media.xml ):

<?xml version="1.0" encoding="utf-8" ?>
<media>
  <cds>
    <rock>
      <cd>
        <id>0001</id>
        <title>Absolution - Muse</title>
        <description>Loud, ambitious, and a lot of fun</description>
      </cd>
      <cd>
        ...
      </cd>
    </rock>
    <pop>
      ...
    </pop>
  </cds>
  <dvds>
    <action>
      <dvd>
        <id>0007</id>
        <title>The Matrix</title>
        <description>The first of the trilogy - a must have in any DVD 
          collection</description>
      </dvd>
      ...
    </action>
    ...
  </dvds>
  <books>
    ...
  </books>
</media>

Now let's look at the controls I've added to the user control to display details of an item:

<%@ control language="VB" compilewith="ItemsDisplay.ascx.vb" 
   classname="ASP.ItemsDisplay_ascx"%>
<script runat="server" language="vb">
</script>
<asp:panel style="width:400px" id="pnlItem" runat="server">
  <asp:detailsview id="itemDetails" runat="server" autogeneraterows="False" 
                   datasourceid="MediaData">
    <rowfields>
      <asp:templatefield headertext="Title">
        <itemtemplate>
          <asp:label id="Label1" runat="server" text='<%# XPath ("title") %>'>
          </asp:label>
        </itemtemplate>
      </asp:templatefield>
      <asp:templatefield headertext="Description">
        <itemtemplate>
          <asp:label id="Label2" runat="server" 
             text='<%# XPath ("description") %>'>
          </asp:label>
        </itemtemplate>
      </asp:templatefield>
    </rowfields>
  </asp:detailsview>
  <br />
  <asp:xmldatasource id="MediaData" runat="server" datafile="Media.xml">
  </asp:xmldatasource></asp:panel>

Notice that I've used an XmlDataSource control to link to an XML file that contains data about some CDs and DVDs on the site. I've also added some XPath statements to extract details of both the title and description of an element from the XML source.

The following listing is the code-beside file for the control. In this listing, you'll see that I've added two properties to this control that have attributes that specify whether to allow or disallow themes:

Imports Microsoft.VisualBasic
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Namespace ASP
    <Themeable(True)> _
    Partial Class ItemsDisplay_ascx
        Inherits System.Web.UI.UserControl
        Private _code As String = ""
        Private _color As String = ""
        <Themeable(True)> _
        Public Property ItemCode() As String
            Get
                Return _code
            End Get
            Set(ByVal Value As String)
                _code = Value
            End Set
        End Property
        <Themeable(True)> _
        Public Property Color() As String
            Get
                Return _color
            End Get
            Set(ByVal Value As String)
                _color = Value
            End Set
        End Property
        Sub Page_Load()
            pnlItem.BackColor = System.Drawing.Color.FromName(_color)
            ' DB access code to retrieve media details here  
            MediaData.XPath = _
              String.Format("//*[./id='{0}']", Page.Request.QueryString("id"))
        End Sub
    End Class
End Namespace

In VB.NET, don't forget the underscore at the end of the Themeable attribute if the following statement occurs on a separate line! And in C#, don't forget that you'll need square, not angled brackets to specify the attribute ( [Themeable(True)] )

Using this control on a page is quite simple. Dragging an instance of it onto an ASPX page (called item.aspx ; a content page based on the master) produces the following:

<%@ page language="VB" master="~/MasterPage.master" %>
<%@ register tagprefix="uc1" tagname="ItemsDisplay" 
  src="~/ItemsDisplay.ascx" %>
<asp:content id="Content1" contentplaceholderid="PageBodyPlaceHolder" 
             runat="server">
  <uc1:itemsdisplay id="selectedItemControl" runat="server"></uc1:itemsdisplay>
</asp:content>

Requesting this page now requires that we add an id to the querystring for the page, so use the following url:

http://localhost/medialibrary2/Item.aspx?id=0011

This produces the following:

Figure 2. A themed user control
This figure has been reduced in size to fit in the text. To view the full image Click here

Ok, I never promised that this site was going to be pretty!

Storing Theme Preference in User Profiles

The ability to theme an entire site in one go is quite enticing, and if you only want to use one theme on your site, the web.config file is perfectly adequate. However, if you want to give users preferences on which theme they would like to use, the config file option is not the best option.

There are basically two options available to you. Firstly, you could create a simple base class and use this class as the base page for all of the pages in your site, for example:

Imports Microsoft.VisualBasic
Imports System.Web
Public Class BasePage
  Inherits System.Web.UI.Page
  Protected Overrides Sub OnPreInit(ByVal e As System.EventArgs)
    If Not (HttpContext.Current.Profile.GetPropertyValue("Theme") Is Nothing) _
     Then
      Page.Theme = _
        HttpContext.Current.Profile.GetPropertyValue("Theme").ToString()
    End If
  End Sub
End Class

Now this is one way around the problem, but this does require that all of the pages in your site will inherit from this base page. An alternative solution is to try to intercept each page request before a page is served and apply the theme to the page object very early in its lifecycle. To achieve this, I added an HttpModule to the MediaLibrary project, and applied the theme to each page whenever the code in the module was run. (This is a technique that has been detailed by a couple of people online, but this version was inspired by my good friend Dave Sussman  thanks Dave!)

Firstly, start off by declaring the module in the web.config file:

<httpModules>
  <add name="Page" type="AspToday.ThemeSwitcher" />
</httpModules>

Another change to the web.config that has to be made is that a profile needs to be defined, and anonymous identification needs to be enabled - in this example, I'm demonstrating how anonymous users can change themes, and when they access the site again at a later date, the theme will remain applied as specified:

<anonymousIdentification enabled="true" />
<profile enabled="true">
  <properties>
    <add name="Theme" allowAnonymous="true"/>
  </properties>
</profile>

On the default.aspx page, there's a simple drop-down list and a button that I've set up to store the theme within the anonymous user's profile:

Protected Sub btnSetTheme_Click(ByVal sender As Object, _
                        ByVal e As System.EventArgs) Handles btnSetTheme.Click
  Profile.Theme = ddlThemePref.SelectedValue
  Response.Redirect("default.aspx")
End Sub

Notice that I've got a Response.Redirect() statement in here - without this, the theme would not be applied after the first click, and would only be applied after the second click. It all comes down to page event order - the button click happens too late in the chain for the httpModule to intercept that value the first time round, so the redirect helps to maintain the user experience.

Finally, here's the module code itself:

Imports Microsoft.VisualBasic
Imports System
Imports System.Web
Imports System.Web.UI
Namespace AspToday
  Public Class ThemeSwitcher
    Implements IHttpModule
    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose
    End Sub
    Public Sub Init(ByVal context As System.Web.HttpApplication) _
      Implements System.Web.IHttpModule.Init
      AddHandler context.PreRequestHandlerExecute, _
        New EventHandler(AddressOf app_PreRequestHandlerExecute)
     End Sub
    Private Sub app_PreRequestHandlerExecute(ByVal Sender As Object, _
      ByVal E As EventArgs)
      'Try to get a reference to the page being requested. 
      'If the object being requested is not a page, requestedPage will be null.
      Dim requestedPage As Page = TryCast(HttpContext.Current.Handler, Page)
      If requestedPage IsNot Nothing Then
        Dim activeProfile As ProfileCommon = _
          DirectCast(HttpContext.Current.Profile, ProfileCommon)
        'Switch the theme on the requested page to the theme 
        ' specified in the active profile
        If activeProfile.Theme <> "" And activeProfile.Theme IsNot Nothing Then
          requestedPage.Theme = activeProfile.Theme
        Else
          requestedPage.Theme = "Quiet"
        End If
      End If
    End Sub
  End Class
End Namespace

There are a couple of points in here that are worth noting. Within the app_PreRequestHandlerExecute method, we have programmatic access to the current Page object:

      Dim requestedPage As Page = TryCast(HttpContext.Current.Handler, Page)
      If requestedPage IsNot Nothing Then

Notice that we also have programmatic access and the current user's Profile object (thanks to some cunning casting):

        Dim activeProfile As ProfileCommon = _
          DirectCast(HttpContext.Current.Profile, ProfileCommon)

All that remains is to change the active theme in use on the requested page to correspond to the current theme specified in the user profile, or to default to the quiet theme, if no theme has been specified:

        'Switch the theme on the requested page to the theme 
        ' specified in the active profile
        If activeProfile.Theme <> "" And activeProfile.Theme IsNot Nothing Then
          requestedPage.Theme = activeProfile.Theme
        Else
          requestedPage.Theme = "Quiet"
        End If
      End If

Switching to real user accounts instead of tracking anonymous users is a bit outside the scope of this chapter, but the good news is that the code in here will not need to change. The only changes required for that switch would be in the web.config file (if you wanted to disable anonymous profiles), in the configuration for the website itself, where you can define user accounts, and to the site design, where you would have to implement some kind of login and authentication functionality.

Conclusion

Themes and skins are really great powerful tools for styling a site without risk of interfering with the site content. In this sense, it is easier to achieve total separation of content from presentation than previously. Thanks to the use of themes, my master page contains no layout or styling code (other than a quick 'n dirty HTML table for layout) which makes it really easy to alter the look and feel of the site with minimal effort.

One of the most important parts of developing a website of any kind, from intranet information store to enterprise ecommerce site, is controlling the look and feel of the site. The Internet today is full of sites that are elegantly styled, where the presentation is equally important as the content, as indeed it should be. As expectations rise about what is possible in a web application, you may be asked to do more and more complex designs for a site, or hand over the design aspect of the development to a dedicated designer. If you find yourself in this position, you'll know it's important that your code is made available to a designer so that they can apply the appropriate design to a site while you concentrate on functionality.

For anyone using ASP.NET 1.0 and 1.1, my advice is to use code-behind to help with this process. However, you'll often find that it's difficult to cleanly separate out all the content from presentation in the ASPX page itself, for example, if you have any Databinder.Eval expressions in your code. ASP.NET 2.0 has started to address this issue with the inclusion of some great tools that make it much easier to achieve this sort of parallel development, keeping the functionality well away from the presentation information for a site, and centralizing the code used to style a site.

Founders at Work



Add your comments

Please keep your comments relevant to this blog entry: inappropriate or purely promotional comments may be removed. To add hyperlink, please follow this example: "your link text":http://your.link.url