Posted on March 13, 2008 15:03 by mcollins

Note to self: fully test out all features of your code before publishing it in a blog with a major defect.

This morning I posted my Login adapter code, and in theory my post was correct because it did correctly render my Login control in a CSS-friendly format.  The one test that I failed to run completely at the time was to perform an actual login and make sure that it worked correctly.  The negative case did work, but on the computer that I wrote the article on, I didn't have a good membership database so I expected it to fail.  But when I expected the login to succeed...I learned my lesson during lunch when I was playing with the adapter again.

The problem with the earlier version of my CSS adapter has to do with how the Login control does it's template.  When using an adapter, the Login control does not seem to get access to the UserName, Password, RememberMe, or FailureText fields in my custom template.  Therefore, when the Login control posts back, I needed to move the UserName, Password, and RememberMe values back to the Login control for authentication to occur correctly.  This leads to a problem: the Login.UserName and Login.RememberMeSet properties are read/write, but the Login.Password property is read-only.  Therefore, there's no way to pass the password to the Login control to perform authentication.

To solve this, with the help of Reflector,  I needed to modify the adapter to implement the authentication logic for the Login control.  I did this by adding an event handler for the Login.Authenticate event.  During the login process, the Login control will look to see if there are any event handlers attached to the Login.Authenticate event.  If there are, then the Login control will use the event handler to perform the authentication.  If not, then the Login control will use the ASP.NET membership providers to perform the authentication.  While I want to use the ASP.NET membership provider, it became clear to me that because I could not propagate the password to the Login control, I had to implement the authentication mechanism on my own using an event handler.

One other enhancement that I made to this latest code is that I overrode the WebControlAdapter.Render method in my LoginAdapter class to check to see if the Login.VisibleWhenLoggedIn property is set.  If the Login control is on a page where the user is authenticated, then the LoginAdapter class will skip rendering the Login control.

This time I tested both positive and negative login scenarios, so I'm pretty positive that it will authenticate the user as well as render CSS-friendly HTML.  Here's the code:

   1: using System;
   2: using System.Diagnostics;
   3: using System.Diagnostics.CodeAnalysis;
   4: using System.Web.Security;
   5: using System.Web.UI;
   6: using System.Web.UI.HtmlControls;
   7: using System.Web.UI.WebControls;
   8: using System.Web.UI.WebControls.Adapters;
   9:  
  10: /// <summary>
  11: /// Implements a custom control adapter for the ASP.NET <see cref="Login"/>
  12: /// control that produces CSS-friendly HTML.
  13: /// </summary>
  14: public class LoginAdapter : WebControlAdapter {
  15:     /// <summary>
  16:     /// The container <see cref="Control"/> that contains the
  17:     /// <see cref="Login"/> control's template.
  18:     /// </summary>
  19:     private Control container;
  20:  
  21:     /// <summary>
  22:     /// The value entered into the password text box.
  23:     /// </summary>
  24:     private string passwordValue;
  25:  
  26:     /// <summary>
  27:     /// The value of the remember me check box.
  28:     /// </summary>
  29:     private bool rememberMeValue;
  30:  
  31:     /// <summary>
  32:     /// The value entered into the user name text box.
  33:     /// </summary>
  34:     private string userNameValue;
  35:  
  36:     /// <summary>
  37:     /// Creates the child controls for the <see cref="Login"/> control.
  38:     /// </summary>
  39:     protected override void CreateChildControls() {
  40:         base.CreateChildControls();
  41:  
  42:         Login login = Control as Login;
  43:         Debug.Assert(login != null, "login != null");
  44:  
  45:         ITemplate layoutTemplate = login.LayoutTemplate;
  46:         if (layoutTemplate == null) {
  47:             layoutTemplate = new DefaultLoginTemplate(login);
  48:         }
  49:  
  50:         container = new Control();
  51:         layoutTemplate.InstantiateIn(container);
  52:         login.Controls.Clear();
  53:         login.Controls.Add(container);
  54:  
  55:         IEditableTextControl userName = container.FindControl("UserName") as
  56:             IEditableTextControl;
  57:         Debug.Assert(userName != null, "userName != null");
  58:         userName.TextChanged += UserName_TextChanged;
  59:  
  60:         IEditableTextControl password = container.FindControl("Password") as
  61:             IEditableTextControl;
  62:         Debug.Assert(password != null, "password != null");
  63:         password.TextChanged += Password_TextChanged;
  64:  
  65:         ICheckBoxControl rememberMe = container.FindControl("RememberMe") as
  66:             ICheckBoxControl;
  67:         Debug.Assert(rememberMe != null, "rememberMe != null");
  68:         rememberMe.CheckedChanged += RememberMe_CheckedChanged;
  69:     }
  70:  
  71:     /// <summary>
  72:     /// Creates the child controls that will be used to render the
  73:     /// contents of the <see cref="Login"/> control.
  74:     /// </summary>
  75:     /// <param name="e">The event arguments.</param>
  76:     protected override void OnInit(EventArgs e) {
  77:         base.OnInit(e);
  78:  
  79:         Login login = Control as Login;
  80:         Debug.Assert(login != null, "login != null");
  81:         login.Authenticate += login_Authenticate;
  82:         login.LoginError += login_LoginError;
  83:     }
  84:  
  85:     /// <summary>
  86:     /// Sets the text and check box settings for the user name, password,
  87:     /// and remember me fields on the login form.
  88:     /// </summary>
  89:     /// <param name="e">The event arguments.</param>
  90:     protected override void OnLoad(EventArgs e) {
  91:         base.OnLoad(e);
  92:  
  93:         if (!Page.IsPostBack) {
  94:             Login login = Control as Login;
  95:             Debug.Assert(login != null, "login != null");
  96:  
  97:             userNameValue = login.UserName;
  98:             passwordValue = "";
  99:             rememberMeValue = login.RememberMeSet;
 100:         }
 101:     }
 102:  
 103:     /// <summary>
 104:     /// Sets the value of the user name text box and the remember me
 105:     /// checkbox before the control is rendered.
 106:     /// </summary>
 107:     /// <param name="e"></param>
 108:     protected override void OnPreRender(EventArgs e) {
 109:         IEditableTextControl userName = container.FindControl("UserName") as
 110:             IEditableTextControl;
 111:         Debug.Assert(userName != null, "userName != null");
 112:         userName.Text = userNameValue;
 113:  
 114:         IEditableTextControl password = container.FindControl("Password") as
 115:             IEditableTextControl;
 116:         Debug.Assert(password != null, "password != null");
 117:         password.Text = "";
 118:  
 119:         ICheckBoxControl rememberMe = container.FindControl("RememberMe") as
 120:             ICheckBoxControl;
 121:         Debug.Assert(rememberMe != null, "rememberMe != null");
 122:         rememberMe.Checked = rememberMeValue;
 123:     }
 124:  
 125:     /// <summary>
 126:     /// Detaches the adapter from the <see cref="Login.LoginError"/> event.
 127:     /// </summary>
 128:     /// <param name="e">The event arguments.</param>
 129:     protected override void OnUnload(EventArgs e) {
 130:         Login login = Control as Login;
 131:         Debug.Assert(login != null, "login != null");
 132:         login.Authenticate -= login_Authenticate;
 133:         login.LoginError -= login_LoginError;
 134:  
 135:         IEditableTextControl userName = container.FindControl("UserName") as
 136:             IEditableTextControl;
 137:         Debug.Assert(userName != null, "userName != null");
 138:         userName.TextChanged -= UserName_TextChanged;
 139:  
 140:         IEditableTextControl password = container.FindControl("Password") as
 141:             IEditableTextControl;
 142:         Debug.Assert(password != null, "password != null");
 143:         password.TextChanged -= Password_TextChanged;
 144:  
 145:         ICheckBoxControl rememberMe = container.FindControl("RememberMe") as
 146:             ICheckBoxControl;
 147:         Debug.Assert(rememberMe != null, "rememberMe != null");
 148:         rememberMe.CheckedChanged -= RememberMe_CheckedChanged;
 149:  
 150:         base.OnUnload(e);
 151:     }
 152:  
 153:     /// <summary>
 154:     /// Prevents the login control from rendering if the user is
 155:     /// authenticated and the <see cref="Login.VisibleWhenLoggedIn"/>
 156:     /// property is set to false.
 157:     /// </summary>
 158:     /// <param name="writer">
 159:     /// The <see cref="HtmlTextWriter"/> object to use to render the
 160:     /// <see cref="Login"/> control.
 161:     /// </param>
 162:     protected override void Render(HtmlTextWriter writer) {
 163:         Login login = Control as Login;
 164:         Debug.Assert(login != null, "login != null");
 165:  
 166:         if (!Page.Request.IsAuthenticated || login.VisibleWhenLoggedIn) {
 167:             base.Render(writer);
 168:         }
 169:     }
 170:  
 171:     /// <summary>
 172:     /// Renders the opening tag that encapsulates the <see cref="Login"/>
 173:     /// control's content.
 174:     /// </summary>
 175:     /// <param name="writer">
 176:     /// The <see cref="HtmlTextWriter"/> object to use to output the
 177:     /// opening tag for the control.
 178:     /// </param>
 179:     protected override void RenderBeginTag(HtmlTextWriter writer) {
 180:         Login login = Control as Login;
 181:         Debug.Assert(login != null, "login != null");
 182:  
 183:         writer.AddAttribute(HtmlTextWriterAttribute.Id, login.ClientID);
 184:  
 185:         if (!login.ControlStyle.IsEmpty) {
 186:             if (!String.IsNullOrEmpty(login.ControlStyle.CssClass)) {
 187:                 writer.AddAttribute(HtmlTextWriterAttribute.Class,
 188:                     login.ControlStyle.CssClass);
 189:             }
 190:  
 191:             foreach (string key in login.Style.Keys) {
 192:                 writer.AddStyleAttribute(key, login.Style[key]);
 193:             }
 194:         }
 195:  
 196:         writer.RenderBeginTag(HtmlTextWriterTag.Div);
 197:     }
 198:  
 199:     /// <summary>
 200:     /// Renders the inner contents of the <see cref="Login"/> control.
 201:     /// </summary>
 202:     /// <param name="writer">
 203:     /// The <see cref="HtmlTextWriter"/> object to use to output the
 204:     /// control's contents.
 205:     /// </param>
 206:     protected override void RenderContents(HtmlTextWriter writer) {
 207:         container.RenderControl(writer);
 208:     }
 209:  
 210:     /// <summary>
 211:     /// Renders the closing outer tag for the <see cref="Login"/> control.
 212:     /// </summary>
 213:     /// <param name="writer">
 214:     /// The <see cref="HtmlTextWriter"/> object to use to output the
 215:     /// control's contents.
 216:     /// </param>
 217:     protected override void RenderEndTag(HtmlTextWriter writer) {
 218:         writer.RenderEndTag();
 219:     }
 220:  
 221:     /// <summary>
 222:     /// Authenticates the user using the ASP.NET membership provider.
 223:     /// </summary>
 224:     /// <param name="sender">The <see cref="Login"/> control.</param>
 225:     /// <param name="e">The event arguments.</param>
 226:     private void login_Authenticate(object sender, AuthenticateEventArgs e) {
 227:         Login login = sender as Login;
 228:         Debug.Assert(login != null, "login != null");
 229:  
 230:         MembershipProvider provider;
 231:         string providerName = login.MembershipProvider;
 232:         if (!String.IsNullOrEmpty(providerName)) {
 233:             provider = Membership.Providers[providerName];
 234:         } else {
 235:             provider = Membership.Provider;
 236:         }
 237:  
 238:         e.Authenticated = provider.ValidateUser(userNameValue,
 239:             passwordValue);
 240:     }
 241:  
 242:     /// <summary>
 243:     /// Displays the failure text in the <see cref="Login"/> control's
 244:     /// output if a login error occurred.
 245:     /// </summary>
 246:     /// <param name="sender">The <see cref="Login"/> control.</param>
 247:     /// <param name="e">The event arguments.</param>
 248:     private void login_LoginError(object sender, EventArgs e) {
 249:         Login login = Control as Login;
 250:         Debug.Assert(login != null, "login != null");
 251:  
 252:         ITextControl failureText = container.FindControl("FailureText") as
 253:             ITextControl;
 254:         Debug.Assert(failureText != null, "failureText != null");
 255:         failureText.Text = login.FailureText;
 256:  
 257:         Control parent = ((Control)failureText).Parent;
 258:         if (!parent.Visible) {
 259:             parent.Visible = true;
 260:         }
 261:     }
 262:  
 263:     /// <summary>
 264:     /// Called when the value in the password text box has been changed.
 265:     /// </summary>
 266:     /// <param name="sender">
 267:     /// The <see cref="IEditableTextControl"/> object.
 268:     /// </param>
 269:     /// <param name="e">The event arguments.</param>
 270:     private void Password_TextChanged(object sender, EventArgs e) {
 271:         IEditableTextControl password = sender as IEditableTextControl;
 272:         Debug.Assert(password != null, "password != null");
 273:         passwordValue = password.Text;
 274:     }
 275:  
 276:     /// <summary>
 277:     /// Called when the check box status for the remember me check box
 278:     /// has been changed.
 279:     /// </summary>
 280:     /// <param name="sender">
 281:     /// The <see cref="ICheckBoxControl"/> object.
 282:     /// </param>
 283:     /// <param name="e">The event arguments.</param>
 284:     private void RememberMe_CheckedChanged(object sender, EventArgs e) {
 285:         ICheckBoxControl rememberMe = sender as ICheckBoxControl;
 286:         Debug.Assert(rememberMe != null, "rememberMe != null");
 287:         rememberMeValue = rememberMe.Checked;
 288:  
 289:         Login login = Control as Login;
 290:         Debug.Assert(login != null, "login != null");
 291:         login.RememberMeSet = rememberMeValue;
 292:     }
 293:  
 294:     /// <summary>
 295:     /// Called when the value in the user name text box has been changed.
 296:     /// </summary>
 297:     /// <param name="sender">
 298:     /// The <see cref="IEditableTextControl"/> object.
 299:     /// </param>
 300:     /// <param name="e">The event arguments.</param>
 301:     private void UserName_TextChanged(object sender, EventArgs e) {
 302:         IEditableTextControl userName = sender as IEditableTextControl;
 303:         Debug.Assert(userName != null, "userName != null");