Posted on March 13, 2008 09:00 by mcollins

At the end of my previous post, I had built a custom ASP.NET server control that implemented a custom layout template for the ASP.NET Login control.  I had also presented this solution as saying that now anyone creating a theme could create and replace the template for a Login or similar ASP.NET control.  That much is true.  There were two questions that I really didn't answer:

  • What if you want to replace the default Login template for all themes?
  • How do I get rid of the enclosing table element that the Login control generates?

 

As I discussed in the previous post, one solution to the problem is to create a custom LayoutTemplate for the Login control in the host page.  Then the template will apply to the page and all ASP.NET themes.  However, this doesn't solve the second issue because we still have a containing TABLE element around our Login control.

To solve the first and second problem together, we need to completely rewrite the rendering logic for the Login control.  We can do this thanks to an extensibility mechanism built into ASP.NET: adapters.

Using adapters, you can instruct ASP.NET controls to call an external component to perform custom rendering of the control instead of using the default rendering.  In the case of the Login control, we can tell the control to call our LoginAdapter control and completely redo the rendering logic for the Login control while still getting the benefits mentioned previously of using the Login control and the Login control's event handling logic.  To the page developer, there's no difference because the Login control continues to work the same.  To the user and the browser, the Login control can look completely different.

The trick to using adapters is that you have to register them with ASP.NET.  This is done through the creation of a custom .browser file in the App_Browsers directory of the ASP.NET application.  Using the sample code from the ASP.NET 2.0 CSS Adapters sample, I created the following .browser file:

   1: <browsers>
   2:     <browser refID="Default">
   3:         <controlAdapters>
   4:             <adapter controlType="System.Web.UI.WebControls.Login"
   5:                      adapterType="LoginAdapter"/>
   6:         </controlAdapters>
   7:     </browser>
   8:     <browser id="W3C_Validator" parentID="default">
   9:         <identification>
  10:             <userAgent match="^W3C_Validator"/>
  11:         </identification>
  12:         <capabilities>
  13:             <capability name="browser" value="W3C Validator"/>
  14:             <capability name="ecmaScriptVersion" value="1.2"/>
  15:             <capability name="javascript" value="true"/>
  16:             <capability name="supportsCss" value="true"/>
  17:             <capability name="supportsCallback" value="true"/>
  18:             <capability name="tables" value="true"/>
  19:             <capability name="tagWriter" 
  20:                 value="System.Web.UI.HtmlTextWriter"/>
  21:             <capability name="w3cdomversion" value="1.0"/>
  22:         </capabilities>
  23:     </browser>
  24: </browsers>

 

This .browser file tells ASP.NET that when the System.Web.UI.WebControls.Login control is being used on a page, that ASP.NET should attach my LoginAdapter adapter to the Login control and use my adapter to perform the control rendering.

Using my custom template from the previous post as the starting point, I modified my template and created the following adapter code:

   1: using System;
   2: using System.Diagnostics;
   3: using System.Diagnostics.CodeAnalysis;
   4: using System.Web.UI;
   5: using System.Web.UI.HtmlControls;
   6: using System.Web.UI.WebControls;
   7: using System.Web.UI.WebControls.Adapters;
   8:  
   9: /// <summary>
  10: /// Implements a custom control adapter for the ASP.NET <see cref="Login"/>
  11: /// control that produces CSS-friendly HTML.
  12: /// </summary>
  13: public class LoginAdapter : WebControlAdapter {
  14:     private Control container;
  15:  
  16:     /// <summary>
  17:     /// Creates the child controls that will be used to render the
  18:     /// contents of the <see cref="Login"/> control.
  19:     /// </summary>
  20:     /// <param name="e">The event arguments.</param>
  21:     protected override void OnInit(EventArgs e) {
  22:         base.OnInit(e);
  23:  
  24:         Login login = Control as Login;
  25:         Debug.Assert(login != null, "login != null");
  26:  
  27:         login.LoginError += login_LoginError;
  28:  
  29:         ITemplate layoutTemplate = login.LayoutTemplate;
  30:         if (layoutTemplate == null) {
  31:             layoutTemplate = new DefaultLoginTemplate(login);
  32:         }
  33:  
  34:         container = new Control();
  35:         layoutTemplate.InstantiateIn(container);
  36:         login.Controls.Clear();
  37:         login.Controls.Add(container);
  38:     }
  39:  
  40:     /// <summary>
  41:     /// Sets the text and check box settings for the user name, password,
  42:     /// and remember me fields on the login form.
  43:     /// </summary>
  44:     /// <param name="e">The event arguments.</param>
  45:     protected override void OnLoad(EventArgs e) {
  46:         base.OnLoad(e);
  47:  
  48:         if (!Page.IsPostBack) {
  49:             Login login = Control as Login;
  50:             Debug.Assert(login != null, "login != null");
  51:  
  52:             ITextControl userName = container.FindControl("UserName") as
  53:                 ITextControl;
  54:             Debug.Assert(userName != null, "userName != null");
  55:             userName.Text = login.UserName;
  56:  
  57:             ITextControl password = container.FindControl("Password") as
  58:                 ITextControl;
  59:             Debug.Assert(password != null, "password != null");
  60:             password.Text = login.Password;
  61:  
  62:             ICheckBoxControl rememberMe = container.FindControl("RememberMe")
  63:                 as ICheckBoxControl;
  64:             Debug.Assert(rememberMe != null, "rememberMe != null");
  65:             rememberMe.Checked = login.RememberMeSet;
  66:         }
  67:     }
  68:  
  69:     /// <summary>
  70:     /// Detaches the adapter from the <see cref="Login.LoginError"/> event.
  71:     /// </summary>
  72:     /// <param name="e">The event arguments.</param>
  73:     protected override void OnUnload(EventArgs e) {
  74:         Login login = Control as Login;
  75:         Debug.Assert(login != null, "login != null");
  76:         login.LoginError -= login_LoginError;
  77:  
  78:         base.OnUnload(e);
  79:     }
  80:  
  81:     /// <summary>
  82:     /// Renders the opening tag that encapsulates the <see cref="Login"/>
  83:     /// control's content.
  84:     /// </summary>
  85:     /// <param name="writer">
  86:     /// The <see cref="HtmlTextWriter"/> object to use to output the
  87:     /// opening tag for the control.
  88:     /// </param>
  89:     protected override void RenderBeginTag(HtmlTextWriter writer) {
  90:         Login login = Control as Login;
  91:         Debug.Assert(login != null, "login != null");
  92:  
  93:         writer.AddAttribute(HtmlTextWriterAttribute.Id, login.ClientID);
  94:  
  95:         if (!login.ControlStyle.IsEmpty) {
  96:             if (!String.IsNullOrEmpty(login.ControlStyle.CssClass)) {
  97:                 writer.AddAttribute(HtmlTextWriterAttribute.Class,
  98:                     login.ControlStyle.CssClass);
  99:             }
 100:  
 101:             foreach (string key in login.Style.Keys) {
 102:                 writer.AddStyleAttribute(key, login.Style[key]);
 103:             }
 104:         }
 105:  
 106:         writer.RenderBeginTag(HtmlTextWriterTag.Div);
 107:     }
 108:  
 109:     /// <summary>
 110:     /// Renders the inner contents of the <see cref="Login"/> control.
 111:     /// </summary>
 112:     /// <param name="writer">
 113:     /// The <see cref="HtmlTextWriter"/> object to use to output the
 114:     /// control's contents.
 115:     /// </param>
 116:     protected override void RenderContents(HtmlTextWriter writer) {
 117:         container.RenderControl(writer);
 118:     }
 119:  
 120:     /// <summary>
 121:     /// Renders the closing outer tag for the <see cref="Login"/> control.
 122:     /// </summary>
 123:     /// <param name="writer">
 124:     /// The <see cref="HtmlTextWriter"/> object to use to output the
 125:     /// control's contents.
 126:     /// </param>
 127:     protected override void RenderEndTag(HtmlTextWriter writer) {
 128:         writer.RenderEndTag();
 129:     }
 130:  
 131:     /// <summary>
 132:     /// Displays the failure text in the <see cref="Login"/> control's
 133:     /// output if a login error occurred.
 134:     /// </summary>
 135:     /// <param name="sender">The <see cref="Login"/> control.</param>
 136:     /// <param name="e">The event arguments.</param>
 137:     private void login_LoginError(object sender, EventArgs e) {
 138:         Login login = Control as Login;
 139:         Debug.Assert(login != null, "login != null");
 140:  
 141:         ITextControl failureText = container.FindControl("FailureText") as
 142:             ITextControl;
 143:         Debug.Assert(failureText != null, "failureText != null");
 144:         failureText.Text = login.FailureText;
 145:  
 146:         Control parent = ((Control)failureText).Parent;
 147:         if (!parent.Visible) {
 148:             parent.Visible = true;
 149:         }
 150:     }
 151:  
 152:     /// <summary>
 153:     /// Implements the default template for the <see cref="Login"/> control.
 154:     /// </summary>
 155:     private class DefaultLoginTemplate : ITemplate {
 156:         /// <summary>
 157:         /// The <see cref="Login"/> control that the template is a template
 158:         /// for.
 159:         /// </summary>
 160:         private Login login;
 161:  
 162:         /// <summary>
 163:         /// Constructs a new <see cref="DefaultLoginTemplate"/> object.
 164:         /// </summary>
 165:         /// <param name="login">
 166:         /// The <see cref="Login"/> control that the template is a template
 167:         /// for.
 168:         /// </param>
 169:         public DefaultLoginTemplate(Login login) {
 170:             this.login = login;
 171:         }
 172:  
 173:         /// <summary>
 174:         /// Adds the standard <see cref="Login"/> controls to the template
 175:         /// container.
 176:         /// </summary>
 177:         /// <param name="container">
 178:         /// The parent container that the controls will be added to as
 179:         /// children.
 180:         /// </param>
 181:         public void InstantiateIn(Control container) {
 182:             CreateTitleControl(container);
 183:             CreateInstructionControl(container);
 184:             CreateFailureControl(container);
 185:             CreateUserInformationFieldSet(container);
 186:             CreateOptionsFieldSet(container);
 187:             CreateLoginButton(container);
 188:             CreateLinks(container);
 189:         }
 190:  
 191:         /// <summary>
 192:         /// Creates the controls used to render the link to create a new
 193:         /// user account.
 194:         /// </summary>
 195:         /// <param name="hasCreateUserIconUrl">
 196:         /// True if the icon should be rendered.
 197:         /// </param>
 198:         /// <param name="hasCreateUserText">
 199:         /// True if the link text should be rendered.
 200:         /// </param>
 201:         /// <returns>The link.</returns>
 202:         private Control CreateCreateUserLink(bool hasCreateUserIconUrl, 
 203:             bool hasCreateUserText) {
 204:             var listItem = new HtmlGenericControl("LI") {
 205:                 EnableViewState = false
 206:             };
 207:  
 208:             var link = new HyperLink() {
 209:                 EnableViewState = false,
 210:                 NavigateUrl = login.CreateUserUrl
 211:             };
 212:             link.MergeStyle(login.HyperLinkStyle);
 213:             listItem.Controls.Add(link);
 214:  
 215:             if (hasCreateUserIconUrl) {
 216:                 var image = new Image() {
 217:                     EnableViewState = false,
 218:                     ImageUrl = login.CreateUserIconUrl
 219:                 };
 220:                 link.Controls.Add(image);
 221:             }
 222:             if (hasCreateUserText) {
 223:                 var text = new Literal() {
 224:                     Text = login.CreateUserText
 225:                 };
 226:                 link.Controls.Add(text);
 227:             }
 228:  
 229:             return listItem;
 230:         }
 231:  
 232:         /// <summary>
 233:         /// Creates the controls that are used to render the failure message
 234:         /// for the <see cref="Login"/> control.
 235:         /// </summary>
 236:         /// <param name="container">
 237:         /// The parent container <see cref="Control"/> that the new control
 238:         /// is to be added to.
 239:         /// </param>
 240:         private void CreateFailureControl(Control container) {
 241:             var failureTextDiv = new HtmlGenericControl("DIV") {
 242:                 EnableViewState = false,
 243:                 Visible = false
 244:             };
 245:             SetCssStyles(failureTextDiv, login.FailureTextStyle);
 246:             container.Controls.Add(failureTextDiv);
 247:  
 248:             var failureText = new Literal {
 249:                 EnableViewState = false,
 250:                 ID = "FailureText"
 251:             };
 252:             failureTextDiv.Controls.Add(failureText);
 253:         }
 254:  
 255:         /// <summary>
 256:         /// Creates the controls used to render the link to navigate to a
 257:         /// help page for the login form.
 258:         /// </summary>
 259:         /// <param name="hasCreateUserIconUrl">
 260:         /// True if the icon should be rendered.
 261:         /// </param>
 262:         /// <param name="hasCreateUserText">
 263:         /// True if the link text should be rendered.
 264:         /// </param>
 265:         /// <returns>The link.</returns>
 266:         private Control CreateHelpPageLink(bool hasHelpPageIconUrl, 
 267:             bool hasHelpPageText) {
 268:             var lis