This will be part of a larger blog series when I get the time to sit down and write this weekend, but here's a sneak preview of something that I've been working on in my spare time (not much lately). Basically, I'm building a new website for fun and I'm trying to maximize the use of built-in ASP.NET features without having to go out and rebuild the wheel. The current feature that I'm working with is ASP.NET themes and how to build a website to maximize the ability to use ASP.NET themes.
Part of this project is a little frustration with the current state-of-the-art of available website frameworks out there. Too many people seem intent on believing that the built-in ASP.NET 2.0 features don't provide enough to do what they want, and they go out and build their own features when doing so may be unnecessary. What you end up with is something that isn't necessarily better implemented, but in some specific cases may be incompatible with the existing ASP.NET feature. I've discovered this quite often as I've been picking and choosing different web applications to try to piece together into the next generation of the Sogeti-Phoenix.com website, and let me tell you it's been a bit frustrating to find a great open source solution only to discover that it's going to be hell to integrate it with the other applications.
Given my rant, I decided to dive in and really explore and push the limits and boundaries of ASP.NET themes to see exactly what is possible. Until recently, I haven't done much with themes, because too often my customers don't need them. Also, until recently, my graphical design skills have completely sucked and I tried to stay away from client-side browser technologies such as JavaScript and CSS. It's only been in the last couple of months that I've become quite decent with both JavaScript and CSS, and thus have began to utilize the power of ASP.NET themes the most.
While exploring ASP.NET themes, I looked quite a bit at the skinning mechanism in ASP.NET. Basically, most ASP.NET controls are customizable using skins that are defined in themes. A skin is a file with a .skin extension that allows the developer or designer to override certain attributes on a control to better match the rest of the theme's settings. For example, using a skin, I might assign the CSS classes for the buttons or text boxes that I include in a web form. At runtime, the ASP.NET engine will use the skins when processing a request to make sure that the controls on a web form are rendered properly.
I started out with a simple login page that is using the ASP.NET Login control. I turned on a lot of the extra features of the control:
1: <%@ Page Language="C#" AutoEventWireup="true" ... %>
2:
3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
4: "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
5: <html xmlns="http://www.w3.org/1999/xhtml">
6: <head runat="server">
7: <title>Untitled Page</title>
8: </head>
9: <body>
10: <form id="PageForm" runat="server">
11: <asp:ScriptManager ID="AjaxScriptManager" runat="server">
12: </asp:ScriptManager>
13: <asp:Login ID="LogOnForm" runat="server"
14: meta:resourcekey="LogOnForm"
15: CreateUserText="Are you a new user? Click here to sign up."
16: CreateUserUrl="~/Join.aspx"
17: DestinationPageUrl="~/Default.aspx"
18: HelpPageText="Do you have questions about signing into the
19: website? Click here to get assistance."
20: HelpPageUrl="~/Help/LogOnHelp.aspx"
21: InstructionText="In order to log into the website, please
22: enter your user name and password into the fields
23: below."
24: PasswordRecoveryText="Did you lost or forget your password?
25: Click here to get assistance."
26: PasswordRecoveryUrl="~/RecoverPassword.aspx"
27: VisibleWhenLoggedIn="False">
28: </asp:Login>
29: </form>
30: </body>
31: </html>
Here's how this page looks by default in the browser:
The control looks decent, but it definitely needs some styling to make it look better. I started off by creating a new ASP.NET theme complete with CSS style sheets. I then created a skin for the Login control and mapped the Login control's parts to my style sheets using CSS classes:
1: <asp:Login runat="server" CssClass="login">
2: <CheckBoxStyle CssClass="checkBox"/>
3: <FailureTextStyle CssClass="failureText"/>
4: <HyperLinkStyle CssClass="hyperlink"/>
5: <InstructionTextStyle CssClass="instructionText"/>
6: <LabelStyle CssClass="label"/>
7: <LoginButtonStyle CssClass="loginButton"/>
8: <TextBoxStyle CssClass="textBox"/>
9: <TitleTextStyle CssClass="titleText"/>
10: <ValidatorTextStyle CssClass="validatorText"/>
11: </asp:Login>
What this skin does is assigns the login CSS class to the top-level element for the Login control. For each component in the Login control's output, the skin assigns CSS classes to those components. For example, the text boxes for the user name and password will be given the CSS class textBox. The title for the Login control will be given the CSS class titleText. Therefore, the Login control's elements will get the visual styles that I define in the CSS style sheets for the theme.
This is ok, but in thinking about how I want my Login control to render in my theme, the way that it's set up right now doesn't work for me. First, look at the HTML representation of the Login control in the following image:
The problem really is that the Login control is rendered using very CSS-unfriendly markup that will be hard to manipulate to match my hypothetical theme. The Login control is rendered as a table inside of a table. Each label and text box are in their own cells in the table. There's not very much that I can easily do to change how the Login control gets rendered by the browser.
What I would really like to do is change how the content for the Login control is output. Fortunately, I can do that using the LayoutTemplate property of the Login control. The Login control is a special kind of ASP.NET control called a template control. Template controls allow users to replace portions of the HTML markup generated by the code with custom HTML. Using the LayoutTemplate property, I can create a custom layout for the Login control that might look like the following:
1: <asp:Login ...>
2: <LayoutTemplate>
3: <h1>Log in</h1>
4: <div>Instructions go here</div>
5: <div>
6: <asp:Literal ID="FailureText" runat="server" />
7: </div>
8: <fieldset>
9: <legend>User account information</legend>
10: <asp:Label runat="server" ID="UserNameLabel"
11: AssociatedControlID="UserName"
12: Text="User name:" />
13: <asp:TextBox runat="server" ID="UserName" />
14: <asp:Label runat="server" ID="PasswordLabel"
15: AssociatedControlID="Password"
16: Text="Password:"/>
17: <asp:TextBox runat="server" ID="Password"
18: TextMode="Password" />
19: </fieldset>
20: <fieldset>
21: <legend>Options</legend>
22: <asp:CheckBox runat="server" ID="RememberMe"
23: Text="Remember me next time?" />
24: </fieldset>
25: <asp:Button ID="LoginButton" runat="server"
26: CommandName="Login" Text="Login" />
27: </LayoutTemplate>
28: </asp:Login>
Here's how the Login form looks, with the HTML markup:
The HTML definitely looks better from a CSS perspective. If we could decorate the template with CSS classes, it would definitely be easier to style and manipulate with CSS, even though the Login control still renders the outer HTML (we'll do something about that in a future post).
The problem that still exists with this method is that this template is going to stick around and apply to all themes, which may not be that bad. But this template is also going to be the same, regardless of theme, and the template is not going to get to take advantage of several features of the ASP.NET Login control. First, the CSS classes must be specified inline in the template. The CSS classes specified on the skin won't apply to the custom template. Also, the text settings and localization features of the Login control won't affect the custom template. Changing the text for any of the prompts or properties on the Login control will not affect the template. There are definitely some more issues that we have to consider.
What if we could just ask developers to drop the base ASP.NET Login control on a page and let our theme designers create custom templates for the Login control to match the theme? Can that be done? Surprisingly (to me at least), yes it can.
Whenever I've read about ASP.NET themes or seen them in use, I only see setting style properties for the controls. But after testing it out, it turns out that the LayoutTemplate property for a Login control (and other ASP.NET controls), can be customized in a .skin file for a template. So, let's revert our Login control back to the original form in the first example and customize the .skin file:
1: <asp:Login runat="server" CssClass="login">
2: <CheckBoxStyle CssClass="checkBox"/>
3: <FailureTextStyle CssClass="failureText"/>
4: <HyperLinkStyle CssClass="hyperlink"/>
5: <InstructionTextStyle CssClass="instructionText"/>
6: <LabelStyle CssClass="label"/>
7: <LayoutTemplate>
8: <h1>Log in</h1>
9: <div>Instructions go here</div>
10: <div>
11: <asp:Literal ID="FailureText" runat="server" />
12: </div>
13: <fieldset>
14: <legend>User account information</legend>
15: <asp:Label runat="server" ID="UserNameLabel"
16: AssociatedControlID="UserName"
17: Text="User name:" />
18: <asp:TextBox runat="server" ID="UserName" />
19: <asp:Label runat="server" ID="PasswordLabel"
20: AssociatedControlID="Password"
21: Text="Password:"/>
22: <asp:TextBox runat="server" ID="Password"
23: TextMode="Password" />
24: </fieldset>
25: <fieldset>
26: <legend>Options</legend>
27: <asp:CheckBox runat="server" ID="RememberMe"
28: Text="Remember me next time?" />
29: </fieldset>
30: <asp:Button ID="LoginButton" runat="server"
31: CommandName="Login" Text="Login" />
32: </LayoutTemplate>
33: <LoginButtonStyle CssClass="loginButton"/>
34: <TextBoxStyle CssClass="textBox"/>
35: <TitleTextStyle CssClass="titleText"/>
36: <ValidatorTextStyle CssClass="validatorText"/>
37: </asp:Login>
The rendered page will look the same as the previous example. This is great. Our developers only have to worry about what controls to use on a page, and the theme designers can now replace the markup for the controls in the .skin files for the themes that they are designing. Win-win for everyone...almost. There is a problem.
While the designers have complete control of the layout of the control, and can manually set the CSS classes in the markup generated by the .skin file, the content has issues. Again, using a custom template makes a lot of the settings of the Login control useless. The other big problem here is that the Login control cannot be localized using resource files because the content is static in the template. To localize the website, each theme is going to have to be duplicated and translated. Not a good thing if you care about building global websites.
So the solution that we're looking for is a way to allow designers to customize the templates for ASP.NET controls using themes and skins, while still being able to utilize the built-in settings of the ASP.NET controls and localization support, and still being able to generate CSS-friendly output. One thought that crossed my mind was that maybe I could put the template in a user control, because then I could still localize the markup in the user control using resource files, but that didn't work out. User controls are a naming container in ASP.NET. What that basically means is that any child controls added to a user control are guaranteed to have a unique name in the page in which the user name is embedded. The downside of this when looking at the Login control is that the Login control has special needs for its layout template. Specifically, the name of the text boxes for the user name and password, the remember me checkbox, and the login button all need to have specific IDs and be locatable by the Login control. This means that they need to be direct children of the Login control and not placed in a separate naming container (since the Login control is already a naming container of its own). This left one possible alternative: server controls.
My hypothesis was that if I created an ASP.NET server control to render my special themed Login control, I could get around these problems while still being able to utilize the settings and localization support of the Login control. By deriving a server control from the base Control class, I could get access to the parent Login control's settings as well as the overridden settings in the .skin file, to correctly render the desired markup. Here's my updated .skin file:
1: <asp:Login runat="server" CssClass="login">
2: <CheckBoxStyle CssClass="checkBox"/>
3: <FailureTextStyle CssClass="failureText"/>
4: <HyperLinkStyle CssClass="hyperlink"/>
5: <InstructionTextStyle CssClass="instructionText"/>
6: <LabelStyle CssClass="label"/>
7: <LayoutTemplate>
8: <web:LogOnTemplate runat="server"/>
9: </LayoutTemplate>
10: <LoginButtonStyle CssClass="loginButton"/>
11: <TextBoxStyle CssClass="textBox"/>
12: <TitleTextStyle CssClass="titleText"/>
13: <ValidatorTextStyle CssClass="validatorText"/>
14: </asp:Login>
I created a special server control named LogOnTemplate that renders my CSS-friendly Login control using my theme settings. I should note that I am able to use my custom control in the .skin file because I registered the control in the system.web/pages section of my Web.config file. Here's how the page renders on the screen:
And here's the code for my custom template control:
1: using System;
2: using System.Diagnostics;
3: using System.Web.UI;
4: using System.Web.UI.HtmlControls;
5: using System.Web.UI.WebControls;
6:
7: namespace Sogeti {
8: /// <summary>
9: /// ASP.NET server control that renders the layout template for the
10: /// <see cref="Login"/> control for the default website template.
11: /// </summary>
12: public class LogOnTemplate : Control {
13: /// <summary>
14: /// The <DIV> element that contains the login failure message.
15: /// </summary>
16: private HtmlGenericControl failureTextDiv;
17:
18: /// <summary>
19: /// The <see cref="TextBox"/> containing the user's password.
20: /// </summary>
21: private TextBox password;
22:
23: /// <summary>
24: /// The <see cref="CheckBox"/> that the user checks to persist
25: /// the user's identity in a cookie.
26: /// </summary>
27: private CheckBox rememberMe;
28:
29: /// <summary>
30: /// The <see cref="TextBox"/> containing the user's account name.
31: /// </summary>
32: private TextBox userName;
33:
34: /// <summary>
35: /// Creates the controls for the default <see cref="Login"/> form's
36: /// template.
37: /// </summary>
38: /// <param name="e">The event arguments.</param>
39: protected override void OnInit(EventArgs e) {
40: base.OnInit(e);
41:
42: var login = Parent.Parent as Login;
43: Debug.Assert(login != null, "login != null");
44:
45: CreateTitleControl(login);
46: CreateInstructionControl(login);
47: CreateFailureControl(login);
48: CreateUserInformationFieldSet(login);
49: CreateOptionsFieldSet(login);
50: CreateLoginButton(login);
51: CreateLinks(login);
52: }
53:
54: /// <summary>
55: /// Attaches an event handler to the <see cref="Login.LoginError"/>
56: /// event and sets the initial values for the controls on the form.
57: /// </summary>
58: /// <param name="e">The event arguments.</param>
59: protected override void OnLoad(EventArgs e) {
60: base.OnLoad(e);
61:
62: var login = Parent.Parent as Login;
63: Debug.Assert(login != null, "login != null");
64: login.LoginError += login_LoginError;
65:
66: if (!Page.IsPostBack) {
67: userName.Text = login.UserName;
68: password.Text = login.Password;
69: rememberMe.Checked = login.RememberMeSet;
70: }
71: }
72:
73: /// <summary>
74: /// Detaches the event handler from the
75: /// <see cref="Login.LoginError"/> event.
76: /// </summary>
77: /// <param name="e">The event arguments.</param>
78: protected override void OnUnload(EventArgs e) {
79: var login = Parent.Parent as Login;
80: Debug.Assert(login != null, "login != null");
81: login.LoginError -= login_LoginError;
82:
83: base.OnUnload(e);
84: }
85:
86: /// <summary>
87: /// Creates the controls used to render the link to create a new
88: /// user account.
89: /// </summary>
90: /// <param name="login">The <see cref="Login"/> control.</param>
91: /// <param name="hasCreateUserIconUrl">
92: /// True if the icon should be rendered.
93: /// </param>
94: /// <param name="hasCreateUserText">
95: /// True if the link text should be rendered.
96: /// </param>
97: /// <returns>The link.</returns>
98: private Control CreateCreateUserLink(Login login,
99: bool hasCreateUserIconUrl, bool hasCreateUserText) {
100: var listItem = new HtmlGenericControl("LI") {
101: EnableViewState = false
102: };
103:
104: var link = new HyperLink() {
105: EnableViewState = false,