At this point you should have a clear idea of what you have to build and how to do it, so let's start developing the solution! Earlier in this chapter I explained how you can create a mock-up of your site using a graphics application such as Photoshop or Paint Shop Pro, and this mock-up could be saved in a PSD file. Once you have been given the go ahead to start coding, you need to break out the individual images from the PSD file into .gif and .jpg files that can be referenced directly in a web page. Regardless of the method you used to create your images, you can now take those images and use them to create the web site. The first step is to create a new web site project, and then create a master page, home page, and default theme. Later you can develop a second theme for the site, and implement the mechanism to switch themes at runtime.
First, create a new web site project in Visual Studio .NET 2005 (File ð New ð Web Site ð ASP.NET Web Site). Here's another new feature in Visual Studio 2005: You can create a project by specifying a folder on the file system (instead of specify a web location) if you select File System in the Location drop-down list, as shown in Figure 2-3.
This enables you to create an ASP.NET project without creating a related virtual application or virtual directory in the IIS metabase (the metabase is where IIS stores its configuration data), and the project is loaded from a real hard disk folder, and executed by an integrated lightweight web server (called ASP.NET Development Server) that handles requests on a TCP/IP port other than the one used by IIS (IIS uses port 80). The actual port number used is determined randomly every time you press F5 to run the web site in debug mode. For example, it handles requests such as http://localhost:1168/ProjName/Default.aspx. This makes it much easier to move and back up projects, because you can just copy the project's folder and you're done — there's no need to set up anything from the IIS Management console. In fact, Visual Studio 2005 does not even require IIS unless you choose to deploy to an IIS web server, or you specify a web URL instead of a local path when you create the web site project.
If you've developed with any previous version of ASP.NET or VS2005, I'm sure you will welcome this new option. I say this is an option because you can still create the project by using a URL as project path — creating and running the site under IIS — by selecting HTTP in the Location drop-down list. I suggest you create and develop the site by using the File System location, with the integrated web server, and then switch to the full-featured IIS web server for the test phase. VS2005 includes a new deployment wizard that makes it easier to deploy a complete solution to a local or remote IIS web server. For now, however, just create a new ASP.NET web site in a folder you want, and call it TheBeerHouse.
Important |
The integrated web server was developed for making development and quick testing easier. However, you should never use it for final Quality Assurance or Integration testing. You should use IIS for that. IIS has more features, such as caching, HTTP compression, and many security options that can make your site run very differently from what you see in the new integrated ASP.NET Development Server. |
After creating the new web site, right-click on Default.aspx and delete it. We'll make our own default page soon.
Creating the master page with the shared site design is not that difficult once you have a mock-up image (or a set of images if you made them separately). Basically, you cut the logo and the other graphics and put them in the HTML page. The other parts of the layout, such as the menu bar, the columns, and the footer, can easily be reproduced with HTML elements such as DIVs. The template provided by TemplateMonster (and just slightly modified and expanded by me) is shown in Figure 2-4.
From this picture you can cut out the header bar altogether and place some DIV containers over it, one for the menu links, one for the login box, and another one for the theme selector (a drop-down list containing the names of the available themes). These DIVs will use the absolute positioning so that you can place them right where you want them. It's easy to determine the correct top-left or top-right coordinates for them — you just hover the mouse cursor on the image opened in the graphics editor and then use the same x and y values you read from there.
The footer is created with a DIV that uses a slice of image with a width of 1 pixel, repeated horizontally as a background. It also contains a couple of sub-DIVs: one for the menu's links (the same shown in the header's menu) and a second for some copyright notices.
Finally, there is the content area of the page, divided into three columns. The center column has the right and left margins set to 200 pixels, and the margins are filled by two other DIVs docked on the page borders with an absolute positioning. Figure 2-5 provides a visual representation of this work, applied on the previous image
Important |
In this book I am assuming a certain amount of familiarity with ASP.NET and Visual Studio .NET. More specifically, I assume you have a working knowledge of the basic operation of any previous version of Visual Studio .NET. Therefore, the steps I explain here focus on the new changes in version 2.0, but do not otherwise cover every small detail. If you are not comfortable following the steps presented here, you should consult a beginner's book on ASP.NET before following the steps in this book. |
After creating the web site as explained above, create a new master page file (select Website ð Add New Item ð Master Page, and name it Template.master), and then use the visual designer to add the ASP.NET server-side controls and static HTML elements to its surface. However, when working with DIV containers and separate stylesheet files, I've found that the visual designer is not able to give me the flexibility I desire. I find it easier to work directly in the Source view, and write the code by hand. As I said earlier, creating the master page is not much different than creating a normal page; the most notable differences are just the @Master directive at the top of the file and the presence of ContentPlaceHolder controls where the .aspx pages will plug in their own content. What follows is the code that defines the standard HTML metatags, and the site's header for the file Template.master:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="Template.master.cs" Inherits="TemplateMaster" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>TheBeerHouse</title> </head> <body> <form id="Main" runat="server"> <div id="header"> <div id="header2"> <div id="headermenu"> <asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" StartingNodeOffset="0" /> <asp:Menu ID="mnuHeader" runat="server" CssClass="headermenulink" DataSourceID="SiteMapDataSource1" Orientation="Horizontal" MaximumDynamicDisplayLevels="0" SkipLinkText="" StaticDisplayLevels="2" /> </div> </div> <div id="loginbox">Login box here...</div> <div id="themeselector">Theme selector here...</div> </div>
As you can see, there is nothing in this first snippet of code that relates to the actual appearance of the header. That's because the appearance of the containers, text, and other objects will be specified in the stylesheet and skin files. The "loginbox" container will be left empty for now; we'll fill it in when we get to Chapter 4, which covers security and membership. The "themeselector" box will be filled in later in this chapter, as soon as we develop a control that displays the available styles from which users can select. The "headermenu" DIV contains a SiteMapPathDataSource, which loads the content of the Web.sitemap file that you'll create shortly. It also contains a Menu control, which uses the SiteMapPathDataSource as data source for the items to create.
Proceed by writing the DIVs for the central part of the page, with the three columns:
<div id="container"> <div id="container2"> <div id="rightcol"> <div class="text">Some text...</div> <asp:ContentPlaceHolder ID="RightContent" runat="server" /> </div> <div id="centercol"> <div id="breadcrumb"> <asp:SiteMapPath ID="SiteMapPath1" runat="server" /> </div> <div id="centercolcontent"> <asp:ContentPlaceHolder ID="MainContent" runat="server"> <p> </p><p> </p><p> </p><p> </p> <p> </p><p> </p><p> </p><p> </p> </asp:ContentPlaceHolder> </div> </div> </div> <div id="leftcol"> <div class="sectiontitle"> <asp:Image ID="imgArrow1" runat="server" ImageUrl="~/images/arrowr.gif" ImageAlign="left" hspace="6" />Site News </div> <div class="text"><b>20 Aug 2005 :: News Header</b><br /> News text... </div> <div class="alternatetext"><b>20 Aug 2005 :: News Header</b><br /> Other news text... </div> <asp:ContentPlaceHolder ID="LeftContent" runat="server" /> <div id="bannerbox"> <a href="http://www.templatemonster.com" target="_blank"> Website Template supplied by Template Monster, a top global provider of website design templates<br /><br /> <asp:Image runat="server" ID="TemplateMonsterBanner" ImageUrl="~/images/templatemonster.jpg" Width="100px" /> </a> </div> </div> </div>
Note that three ContentPlaceHolder controls are defined in the preceding code, one for each column. This way, a content page will be able to add text in different positions. Also remember that filling a ContentPlaceHolder with some content is optional, and in some cases we'll have pages that just add content to the central column, using the default content defined in the master page for the other two columns. The central column also contains a sub-DIV with a SiteMapPath control for the breadcrumb navigation system.
The remaining part of the master page defines the container for the footer, with its subcontainers for the footer's menu (which exactly replicates the header's menu, except for the style applied to it) and the copyright notices:
<div id="footer"> <div id="footermenu"> <asp:Menu ID="mnuFooter" runat="server" style="margin-left:auto; margin-right:auto;" CssClass="footermenulink" DataSourceID="SiteMapDataSource1" Orientation="Horizontal" MaximumDynamicDisplayLevels="0" SkipLinkText="" StaticDisplayLevels="2" /> </div> <div id="footertext"> <small>Copyright © 2005 Marco Bellinaso & <a href="http://www.wrox.com" target="_blank">Wrox Press</a><br /> Website Template kindly offered by <a href="http://www.templatemonster.com" target="_blank"> Template Monster</a></small> </div> </div> </form> </body> </html>
Note |
A note on cross-browser portability: The footertext DIV declared in the preceding code will have the text aligned in the center. However, the Menu control declared inside it is rendered as an HTML table at runtime, and tables are not considered as text by browsers such as Firefox, and therefore are not aligned as text. Because we're targeting both browsers, we must ensure that the table is aligned on the center on both of them, and so I've added the style attribute to the Menu declaration to put an equal margin on the left and on the right of the menu table, as large as possible. The result will be a table centered horizontally. The style attribute does not map to a server-side property exposed by the control; it appears that there is no such property or a similar one that is transformed to "align" at runtime, so I used the HTML attribute. Since it is not mapped to a control's property, it will be attached "as is" to the HTML table generated when the control is rendered. |
Given how easy it is to add, remove, and modify links in the site's menus when employing the sitemap file and the SiteMapPath control, at this point you don't have to worry about what links you'll need. You can fill in the link information later. You can add a few preliminary links to use as a sample for now, and then come back and modify the file when you need it. Therefore, add a Web.sitemap file to the project (select Website ð Add New Item… ð Site Map) and add the following XML nodes inside it:
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode title="Home" url="~/Default.aspx"> <siteMapNode title="Store" url="~/Store/Default.aspx"> <siteMapNode title="Shopping cart" url="~/Store/ShoppingCart.aspx" /> </siteMapNode> <siteMapNode title="Forum" url="~/Forum/Default.aspx" /> <siteMapNode title="About" url="~/About.aspx" /> <siteMapNode title="Contact" url="~/Contact.aspx" /> <siteMapNode title="Admin" url="~/Admin/Default.aspx" /> </siteMapNode> </siteMap>
You may be wondering why the Home node serves as root node for the others, and is not at the same level as the others. That would actually be an option, but I want the SiteMapPath control to always show the Home link, before the rest of the path that leads to the current page, so it must be the root node. In fact, the SiteMapPath does not work by remembering the previous pages — it just looks in the site map for an XML node that describes the current page, and displays the links of all parent nodes.
Important |
If you deploy the project to the root folder of an IIS site, you could identify the root folder with "/". However, if you deploy the site on a sub virtual folder, that will not work anymore. In the sitemap above, you see that "~/" is used to identify the root folder. These URLs will be resolved at runtime, according to where the pages are located, and it works fine if you deploy the pages to either the site's root folder or to a sub virtual folder. When you use the integrated web server, it always runs the website as if it were deployed on a virtual folder, and in fact the URL includes the project name. This forces you to use "~/" in your URLs, to minimize problems during deployment. |
It's time to create the first theme for the master page: TemplateMonster. There are two ways to do this that are functionally equivalent. You could add a new folder to the project named App_Themes, and then a new subfolder under it called TemplateMonster. Alternately, you could let VS2005 assist you: Select Website ð Add Folder ð Theme Folder, and name it TemplateMonster (the App_Themes folder is created for you in this case). The App_Themes folder is special because it uses a reserved name, and appears in gray in the Solution Explorer. Select the App_Themes\ TemplateMonster folder, and add a stylesheet file to this folder (select Website ð Add New Item ð Stylesheet, and name it Default.css). The name you give to the CSS file is not important, as all CSS files found in the current theme's folder will automatically be linked by the .aspx page at runtime.
For your reference, the code that follows includes part of the style classes defined in this file (refer to the downloadable code for the entire stylesheet):
body { margin: 0px; font-family: Verdana; font-size: 12px; } #container { background-color: #818689; } #container2 { background-color: #bcbfc0; margin-right: 200px; } #header { padding: 0px; margin: 0px; width: 100%; height: 184px; background-image: url(images/HeaderSlice.gif); } #header2 { padding: 0px; margin: 0px; width: 780px; height: 184px; background-image: url(images/Header.gif); } #headermenu { position: relative; top: 153px; left: 250px; width: 500px; padding: 2px 2px 2px 2px; } #breadcrumb { background-color: #202020; color: White; padding: 3px; font-size: 10px; } #footermenu { text-align: center; padding-top: 10px; } #loginbox { position: absolute; top: 16px; right: 10px; width: 180px; height: 80px; padding: 2px 2px 2px 2px; font-size: 9px; } #themeselector { position: absolute; text-align: right; top: 153px; right: 10px; width: 180px; height: 80px; padding: 2px 2px 2px 2px; font-size: 9px; } #footer { padding: 0px; margin: 0px; width: 100%; height: 62px; background-image: url(images/FooterSlice.gif); } #leftcol { position: absolute; top: 184px; left: 0px; width: 200px; background-color: #bcbfc0; font-size: 10px; } #centercol { position: relative inherit; margin-left: 200px; padding: 0px; background-color: white; height: 500px; } #centercolcontent { padding: 15px 6px 15px 6px; } #rightcol { position: absolute; top: 184px; right: 0px; width: 198px; font-size: 10px; color: White; background-color: #818689; } .footermenulink { font-family: Arial; font-size: 10px; font-weight: bold; text-transform: uppercase; } .headermenulink { font-family: Arial Black; font-size: 12px; font-weight: bold; text-transform: uppercase; } /* other styles omitted for brevity's sake */
Note how certain elements (such as "loginbox", "themeselector", "leftcol", and "rightcol") use absolute positioning. Also note that there are two containers with two different styles for the header. The former, header, is as large as the page (if you don't specify an explicit width and don't use absolute positioning, a DIV will always have an implicit width of 100%), has a background image that is 1 pixel large, and is (implicitly, by default) repeated horizontally. The latter, header2, is as large as the Header.gif image it uses as a background, and is placed over the first container. The result is that the first container serves to continue the background for the second container, which has a fixed width. This is required only because we want to have a dynamic layout that fills the whole width of the page. If we had used a fixed-width layout, we could have used just a single container.
Note |
All images pointed to in this stylesheet file are located in an Images folder under App_Themes/TemplateMonster. This way, you keep together all related objects that make up the theme. |
Now add a skin file named Controls.skin (Select the TemplateMonster folder and then select Website ð Add New Item ð Skin File). You will place all the server-side styles into this file to apply to controls of all types. Alternatively, you could create a different file for every control, but I find it easier to manage styles in a single file. The code that follows contains two unnamed skins for the TextBox and SiteMapPath controls, and two named (SkinID) skins for the Label control:
<asp:TextBox runat="server" BorderStyle="dashed" BorderWidth="1px" /> <asp:Label runat="server" SkinID="FeedbackOK" ForeColor="green" /> <asp:Label runat="server" SkinID="FeedbackKO" ForeColor="red" /> <asp:SiteMapPath runat="server"> <PathSeparatorTemplate> <asp:Image runat="server" ImageUrl="images/sepwhite.gif" hspace="4" align="middle" /> </PathSeparatorTemplate> </asp:SiteMapPath>
The first three skins are mainly for demonstrative purposes, because you can get the same results by defining normal CSS styles. The skin for the SiteMapPath control is something that you can't easily replicate with CSS styles, because this control does not map to a single HTML element. In the preceding code, this skin declares what to use as a separator for the links that lead to the current page — namely, an image representing an arrow.
Now that you have a complete master page and a theme, you can test it by creating a sample content page. To begin, add a new web page named Default.aspx to the project (select the project in Solution Explorer and choose Website ð Add New Item ð Web Form), select the checkbox called Select Master Page in the Add New Item dialog box, and you'll be presented with a second dialog window from which you can choose the master page to use — namely, Template.master. When you select this option the page will just contain Content controls that match the master page's ContentPlaceHolder controls, and not the <html>, <body>, <head>, and <form> tags that would be present otherwise. You can put some content in the central ContentPlaceHolder, as shown here:
<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="~/Template.master" CodeFile="Default.aspx.cs" Inherits="_Default" Title="The Beer House" %> <asp:Content ID="Content1" ContentPlaceHolderID="RightContent" Runat="Server"> </asp:Content> <asp:Content ID="MainContent" runat="server" ContentPlaceHolderID="MainContent"> <asp:Image ID="imgBeers" runat="server" ImageUrl="~/Images/3beers.jpg" ImageAlign="left" hspace="8" /> Lorem ipsum dolor sit amet, consectetuer adipiscing elit... </asp:Content> <asp:Content ID="Content3" ContentPlaceHolderID="LeftContent" Runat="Server"> </asp:Content>
You could have also added the Theme attribute to the @Page directive, setting it equal to "Template Monster". However, instead of doing this here, you can do it in the web.config file, once, and have it apply to all pages. Select the project ð Website ð Add New Item ð Web Configuration File. Remove the MasterPageFile attribute from the code of the Default.aspx page, because you'll also put that in web.config, as follows:
<?xml version="1.0"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <pages theme="TemplateMonster" masterPageFile="~/Template.master" /> <!-- other settings here... --> </system.net> </configuration>
Note |
Why select a master page from the New Item dialog box when creating Default.aspx, just to remove the master page attribute immediately afterwards? Because this way, VS2005 will create the proper Content controls, and not the HTML code of a normal page. |
To test the user-selectable theming feature described earlier in the chapter, we must have more than one theme. Thus, under the App_Themes folder, create another folder named PlainHtmlYellow (select the project, right-click Add Folder ð Theme Folder, and name it PlainHtmlYellow), and then copy and paste the whole Default.css file from the TemplateMonster folder, modifying it to make it look different. In the provided example I've changed most of the containers so that no background image is used, and the header and footer are filled with simple solid colors, like the left-and right-hand columns. Not only is the size for some elements different, but also the position. For the left-and right-hand columns in particular (which use absolute positioning), their position is completely switched, so that the container named leftcol gets docked on the right border, and the rightcol container gets docked on the left. This is done by changing just a couple of style classes, as shown below:
#leftcol { position: absolute; top: 150px; right: 0px; width: 200px; background-color: #ffb487; font-size: 10px; } #rightcol { position: absolute; top: 150px; left: 0px; width: 198px; color: White; background-color: #8d2d23; font-size: 10px; }
This is the power of DIVs and stylesheets: Change a few styles, and content that used to be on the left of the page will be moved to the right. This was a pretty simple example, but you can push this much further and create completely different layouts, with some parts hidden and others made bigger, and so on.
As for the skin file, just copy and paste the whole controls.skin file defined under TemplateMonster and remove the definition for the TextBox and SiteMapPath controls so that they will have the default appearance. You'll see a difference when we change the theme at runtime. If you later want to apply a non-default appearance to them, just go back and add a new style definition to this file, without modifying anything else.
You now have a master page, with a couple of themes for it, so now you can develop a user control that will display the list of available themes and allow the user to pick one. Once you have this control, you will plug it into the master page, in the "themeselector" DIV container. Before creating the user control, create a new folder named "Controls", inside of which you'll put all your user controls so that they are separate from pages, for better organization (select the project, right-click Add Folder ð Regular folder, and name it Controls). To create a new user control, right-click on the Controls folder, select Add New Item ð Web User Control, and name it ThemeSelector.ascx. The content of this .ascx file is very simple and includes just a string and a DropDownList:
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ThemeSelector.ascx.cs" Inherits="ThemeSelector" %> <b>Theme:</b> <asp:DropDownList runat="server" ID="ddlThemes" AutoPostBack="true" />
Note that the drop-down list has the AutoPostBack property set to true, so that the page is automatically submitted to the server as soon as the user changes the selected value. The real work of filling the drop-down list with the names of the available themes, and loading the selected theme, will be done in this control's code-beside file, and in a base page class that you'll see shortly. In the code-beside file, you need to fill the drop-down list with an array of strings returned by a helper method, and then select the item that has the same value of the current page Theme:
public partial class ThemeSelector : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { ddlThemes.DataSource = Helpers.GetThemes(); ddlThemes.DataBind(); ddlThemes.SelectedValue = this.Page.Theme; } }
The GetThemes method is defined in a Helpers.cs file that is located under another special folder named App_Code. Files in this folder are automatically compiled at runtime by the ASP.NET engine, so you don't need to compile them before running the project. You can even modify the C# source code files while the application is running, hit refresh, and the new request will recompile the modified file in a new temporary assembly, and load it. You'll read more about the new compilation model later in the book, and especially in Chapter 12 about deployment.
The GetThemes method uses the GetDirectories method of the System.IO.Directory class to retrieve an array with the paths of all folders contained in the ~/App_Themes folder (this method expects a physical path and not a URL — you can, however, get the physical path pointed to by a URL through the Server.MapPath method). The returned array of strings contains the entire path, not just the folder name, so you must loop through this array and overwrite each item with that item's folder name part (returned by the System.IO.Path.GetFileName static method). Once the array is filled for the first time it is stored in the ASP.NET cache, so that subsequent requests will retrieve it from there, more quickly. The following code shows the entire content of the Helpers class (App_Code\Helpers.cs):
namespace MB.TheBeerHouse.UI { public static class Helpers { /// <summary> /// Returns an array with the names of all local Themes /// </summary> public static string[] GetThemes() { if (HttpContext.Current.Cache["SiteThemes"] != null) { return (string[])HttpContext.Current.Cache["SiteThemes"]; } else { string themesDirPath = HttpContext.Current.Server.MapPath("~/App_Themes"); // get the array of themes folders under /app_themes string[] themes = Directory.GetDirectories(themesDirPath); for (int i = 0; i <= themes.Length - 1; i++) themes[i] = Path.GetFileName(themes[i]); // cache the array with a dependency to the folder CacheDependency dep = new CacheDependency(themesDirPath); HttpContext.Current.Cache.Insert("SiteThemes", themes, dep); return themes; } } } }
Now that you have the control, go back to the master page and add the following line at the top of the file in the Source view to reference the external user control:
<%@ Register Src="Controls/ThemeSelector.ascx" TagName="ThemeSelector" TagPrefix="mb" %>
Then declare an instance of the control where you want it to appear — namely, within the "themeselector" container:
<div id="themeselector"> <mb:ThemeSelector id="ThemeSelector1" runat="server" /> </div>
The code that handles the switch to a new theme can't be placed in the DropDownList's SelectedIndexChanged event, because that happens too late in the page's life cycle. As I said in the "Design" section, the new theme must be applied in the page's PreInit event. Also, instead of recoding it for every page, we'll just write that code once in a custom base page. Our objective is to read the value of the DropDownList's selected index from within our custom base class, and then we want to apply the theme specified by the DropDownList. However, you can't access the controls and their values from the PreInit event handler because it's still too early in the page's life cycle. Therefore, you need to read the value of this control in a server event that occurs later: The Load event is a good place to read it.
However, when you're in the Load event handler you won't know the specific ID of the DropDownList control, so you'll need a way to identify this control, and then you can read its value by accessing the row data that was posted back to the server, via the Request.Form collection. But there is still a remaining problem: You must know the ID of the control to retrieve its value from the collection, but the ID may vary according to the container in which you place it, and it's not a good idea to hard-code it because you might decide to change its location in the future. Instead, when the control is first created, you can save its client-side ID in a static field of a class, so that it will be maintained for the entire life of the application, between different requests (post backs), until the application shuts down (more precisely, until the application domain of the application's assemblies is unloaded). Therefore, add a Globals.cs file to the App_Code folder, and write the following code inside it:
namespace MB.TheBeerHouse { public static class Globals { public static string ThemesSelectorID = ""; } }
Then, go back to the ThemeSelector's code-beside file and add the code to save its ID in that static field:
public partial class ThemeSelector : System.Web.UI.UserControl { protected void Page_Load(object sender, EventArgs e) { if (Globals.ThemesSelectorID.Length == 0) Globals.ThemesSelectorID = ddlThemes.UniqueID; ddlThemes.DataSource = Helpers.GetThemes(); ddlThemes.DataBind(); ddlThemes.SelectedValue = this.Page.Theme; } }
You're ready to create the custom base class for your pages, and this will just be another regular class you place under App_Code, and which inherits from System.Web.UI.Page. You override its OnPreInit method to do the following:
Check whether the current request is a postback. If it is, check whether it was caused by the ThemeSelector drop-down list. As in ASP.NET 1.x, all pages with a server-side form have a hidden field named "__EVENTTARGET", which will be set with the ID of the HTML control that causes the postback (if it is not a Submit button). To verify this condition, you can just check whether the "__EVENTTARGET" element of the Form collection contains the ID of the drop-down list, based on the ID read from the Globals class.
If the conditions of point 1 are all verified, you retrieve the name of the selected theme from the Form collection's element with an Id equal to the ID saved in Globals, and use it for setting the page's Theme property. Then, you also store that value in a Session variable. This is done so that subsequent requests made by the same user will correctly load the newly selected theme, and will not reset it to the default theme.
If the current request is not a postback, check whether the Session variable used in point 2 is empty (null) or not. If it is not, retrieve that value and use it for the page's Theme property.
The following snippet translates this description to real code:
namespace MB.TheBeerHouse.UI { public class BasePage : System.Web.UI.Page { protected override void OnPreInit(EventArgs e) { string id = Globals.ThemesSelectorID; if (id.Length > 0) { // if this is a postback caused by the theme selector's dropdownlist, // retrieve the selected theme and use it for the current page request if (this.Request.Form["__EVENTTARGET"] == id && !string.IsNullOrEmpty(this.Request.Form[id])) { this.Theme = this.Request.Form[id]; this.Session["CurrentTheme"] = this.Theme; } else { // if not a postback, or a postback caused by controls other then // the theme selector, set the page's theme with the value found // in Session, if present if (this.Session["CurrentTheme"] != null) this.Theme = this.Session["CurrentTheme"].ToString(); } } base.OnPreInit(e); } } }
Note |
The downside of the approach used here is that the selected theme is stored in a session variable, which is cleared when the session ends — namely, when the user closes the browser, or when the user does not make a page request for 20 minutes (the duration can be customized). A much better solution would be to use Profile properties, which among other advantages are also persistent between sessions. You'll examine this new feature of ASP.NET 2.0 — and modify this code to use it — in Chapter 4. |
The last thing you have to do is change the default code-beside class for the Default.aspx page so that it uses your own BasePage class instead of the default Page class. Your custom base class, in turn, will call the original Page class. You only need to change one word, as shown below (change Page to BasePage):
public partial class _Default : MB.TheBeerHouse.UI.BasePage { protected void Page_Load(object sender, EventArgs e) { } }
You're done! If you now run the project, by default you'll see the home page shown in Figure 2-4 (except for the login box, which doesn't contain anything so far — it will be filled in Chapter 5), with the TemplateMonster theme applied to it. If you pick the PlainHtmlYellow item from the ThemeSelector drop-down list, the home page should change to something very similar to what is shown in Figure 2-6.
A single page (Default.aspx) is not enough to test everything we've discussed and implemented in this chapter. For example, we haven't really seen the SiteMapPath control in practice, because it doesn't show any link until we move away from the home page, of course. You can easily implement the Contact.aspx and About.aspx pages if you want to test it. I'll take the Contact.aspx page as an example for this chapter because I want to add an additional little bit of style to the TemplateMonster theme to further differentiate it from PlainHtmlYellow. The final page is represented in Figure 2-7.
I won't show you the code that drives the page and sends the mail in this chapter, because that's covered in the next chapter. I also will not show you the content of the .aspx file, because it is as simple as a Content control inside of which are some paragraphs of text and some TextBox controls with their accompanying validators. Instead, I want to direct your attention to the fact that the Subject textbox has a yellow background color, and that's because it is the input control with the focus. Highlighting the active control can facilitate users in quickly seeing which control they are in, something that may not have otherwise been immediately obvious if they were tabbing through controls displayed in multiple columns and rows. Implementing this effect is pretty easy: You just need to handle the onfocus and onblur client-side events of the input controls, and respectively apply or remove a CSS style to the control by setting its className attribute. The style class in the example sets the background color to yellow and the text color to blue. The class, shown below, should be added to the Default.css file of the TemplateMonster folder:
To add handlers for the onfocus and onblur JavaScript client-side event handlers, you just add a couple of attribute_name/value pairs to the control's Attributes collection, so that they will be rendered "as is" during runtime, in addition to the other attributes rendered by default by the control. You can add a new static method to the Helpers class created above, to wrap all the required code, and call it more easily when you need it. The new SetInputControlsHighlight method takes the following parameters: a reference to a control, the name of the style class to be applied to the active control, and a Boolean value indicating whether only textboxes should be affected by this routine or also DropDown List, ListBox, RadioButton, CheckBox, RadioButtonList and CheckBoxList controls. If the control passed in is of the right type, this method adds the onfocus and onblur attributes to it. Otherwise, if it has child controls, it recursively calls itself. This way, you can pass a reference to a Page (which is itself a control, since it inherits from the base System.Web.UI.Control class), a Panel, or some other type of container control, and have all child controls passed indirectly to this method as well. Following is the complete code for this method:
public static class Helpers { public static string[] GetThemes() { ... } public static void SetInputControlsHighlight(Control container, string className, bool onlyTextBoxes) { foreach (Control ctl in container.Controls) { if ((onlyTextBoxes && ctl is TextBox) || ctl is TextBox || ctl is DropDownList || ctl is ListBox || ctl is CheckBox || ctl is RadioButton || ctl is RadioButtonList || ctl is CheckBoxList) { WebControl wctl = ctl as WebControl; wctl.Attributes.Add("onfocus", string.Format( "this.className = '{0}';", className)); wctl.Attributes.Add("onblur", "this.className = '';"); } else { if (ctl.Controls.Count > 0) SetInputControlsHighlight(ctl, className, onlyTextBoxes); } } } }
To run this code in the Load event of any page, override the OnLoad method in the BasePage class created earlier, as shown below:
namespace MB.TheBeerHouse.UI { public class BasePage : System.Web.UI.Page { protected override void OnPreInit(EventArgs e) { ... } protected override void OnLoad(EventArgs e) { // add onfocus and onblur javascripts to all input controls on the forum, // so that the active control has a difference appearance Helpers.SetInputControlsHighlight(this, "highlight", false); base.OnLoad(e); } } }
This code will always run, regardless of the fact that the PlainHtmlYellow theme does not define a "highlight" style class in its Default.css file. For this theme, the active control will not have any particular style.
Little tricks like this one are easy and quick to implement, but they can really improve the user experience, and positively impress them. Furthermore, the simplicity afforded by the use of a custom base class for content pages greatly simplifies the implementation of future requirements.