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");
 304:         userNameValue = userName.Text;
 305:     }
 306:  
 307:     /// <summary>
 308:     /// Implements the default template for the <see cref="Login"/> control.
 309:     /// </summary>
 310:     private class DefaultLoginTemplate : ITemplate {
 311:         /// <summary>
 312:         /// The <see cref="Login"/> control that the template is a template
 313:         /// for.
 314:         /// </summary>
 315:         private Login login;
 316:  
 317:         /// <summary>
 318:         /// Constructs a new <see cref="DefaultLoginTemplate"/> object.
 319:         /// </summary>
 320:         /// <param name="login">
 321:         /// The <see cref="Login"/> control that the template is a template
 322:         /// for.
 323:         /// </param>
 324:         public DefaultLoginTemplate(Login login) {
 325:             this.login = login;
 326:         }
 327:  
 328:         /// <summary>
 329:         /// Adds the standard <see cref="Login"/> controls to the template
 330:         /// container.
 331:         /// </summary>
 332:         /// <param name="container">
 333:         /// The parent container that the controls will be added to as
 334:         /// children.
 335:         /// </param>
 336:         public void InstantiateIn(Control container) {
 337:             CreateTitleControl(container);
 338:             CreateInstructionControl(container);
 339:             CreateFailureControl(container);
 340:             CreateUserInformationFieldSet(container);
 341:             CreateOptionsFieldSet(container);
 342:             CreateLoginButton(container);
 343:             CreateLinks(container);
 344:         }
 345:  
 346:         /// <summary>
 347:         /// Creates the controls used to render the link to create a new
 348:         /// user account.
 349:         /// </summary>
 350:         /// <param name="hasCreateUserIconUrl">
 351:         /// True if the icon should be rendered.
 352:         /// </param>
 353:         /// <param name="hasCreateUserText">
 354:         /// True if the link text should be rendered.
 355:         /// </param>
 356:         /// <returns>The link.</returns>
 357:         private Control CreateCreateUserLink(bool hasCreateUserIconUrl, 
 358:             bool hasCreateUserText) {
 359:             var listItem = new HtmlGenericControl("LI") {
 360:                 EnableViewState = false
 361:             };
 362:  
 363:             var link = new HyperLink() {
 364:                 EnableViewState = false,
 365:                 NavigateUrl = login.CreateUserUrl
 366:             };
 367:             link.MergeStyle(login.HyperLinkStyle);
 368:             listItem.Controls.Add(link);
 369:  
 370:             if (hasCreateUserIconUrl) {
 371:                 var image = new Image() {
 372:                     EnableViewState = false,
 373:                     ImageUrl = login.CreateUserIconUrl
 374:                 };
 375:                 link.Controls.Add(image);
 376:             }
 377:             if (hasCreateUserText) {
 378:                 var text = new Literal() {
 379:                     Text = login.CreateUserText
 380:                 };
 381:                 link.Controls.Add(text);
 382:             }
 383:  
 384:             return listItem;
 385:         }
 386:  
 387:         /// <summary>
 388:         /// Creates the controls that are used to render the failure message
 389:         /// for the <see cref="Login"/> control.
 390:         /// </summary>
 391:         /// <param name="container">
 392:         /// The parent container <see cref="Control"/> that the new control
 393:         /// is to be added to.
 394:         /// </param>
 395:         private void CreateFailureControl(Control container) {
 396:             var failureTextDiv = new HtmlGenericControl("DIV") {
 397:                 EnableViewState = false,
 398:                 Visible = false
 399:             };
 400:             SetCssStyles(failureTextDiv, login.FailureTextStyle);
 401:             container.Controls.Add(failureTextDiv);
 402:  
 403:             var failureText = new Literal {
 404:                 EnableViewState = false,
 405:                 ID = "FailureText"
 406:             };
 407:             failureTextDiv.Controls.Add(failureText);
 408:         }
 409:  
 410:         /// <summary>
 411:         /// Creates the controls used to render the link to navigate to a
 412:         /// help page for the login form.
 413:         /// </summary>
 414:         /// <param name="hasCreateUserIconUrl">
 415:         /// True if the icon should be rendered.
 416:         /// </param>
 417:         /// <param name="hasCreateUserText">
 418:         /// True if the link text should be rendered.
 419:         /// </param>
 420:         /// <returns>The link.</returns>
 421:         private Control CreateHelpPageLink(bool hasHelpPageIconUrl, 
 422:             bool hasHelpPageText) {
 423:             var listItem = new HtmlGenericControl("LI") {
 424:                 EnableViewState = false
 425:             };
 426:  
 427:             var link = new HyperLink() {
 428:                 EnableViewState = false,
 429:                 NavigateUrl = login.HelpPageUrl
 430:             };
 431:             link.MergeStyle(login.HyperLinkStyle);
 432:             listItem.Controls.Add(link);
 433:  
 434:             if (hasHelpPageIconUrl) {
 435:                 var image = new Image() {
 436:                     EnableViewState = false,
 437:                     ImageUrl = login.HelpPageIconUrl
 438:                 };
 439:                 link.Controls.Add(image);
 440:             }
 441:             if (hasHelpPageText) {
 442:                 var text = new Literal() {
 443:                     Text = login.HelpPageText
 444:                 };
 445:                 link.Controls.Add(text);
 446:             }
 447:  
 448:             return listItem;
 449:         }
 450:  
 451:         /// <summary>
 452:         /// Creates the controls that are used to render the instructions
 453:         /// for the <see cref="Login"/> control.
 454:         /// </summary>
 455:         /// <param name="container">
 456:         /// The parent container <see cref="Control"/> that the new control
 457:         /// is to be added to.
 458:         /// </param>
 459:         private void CreateInstructionControl(Control container) {
 460:             var instructions = new HtmlGenericControl("DIV") {
 461:                 EnableViewState = false,
 462:                 InnerHtml = login.InstructionText
 463:             };
 464:             SetCssStyles(instructions, login.InstructionTextStyle);
 465:             container.Controls.Add(instructions);
 466:         }
 467:  
 468:         /// <summary>
 469:         /// Creates the controls used to render helpful links to other pages.
 470:         /// </summary>
 471:         /// <param name="container">
 472:         /// The parent container <see cref="Control"/> that the new control
 473:         /// is to be added to.
 474:         /// </param>
 475:         private void CreateLinks(Control container) {
 476:             bool hasCreateUserIconUrl =
 477:                 !String.IsNullOrEmpty(login.CreateUserIconUrl);
 478:             bool hasCreateUserText =
 479:                 !String.IsNullOrEmpty(login.CreateUserText);
 480:             bool hasCreateUserLink = hasCreateUserIconUrl || hasCreateUserText;
 481:             bool hasPasswordRecoveryIconUrl =
 482:                 !String.IsNullOrEmpty(login.PasswordRecoveryIconUrl);
 483:             bool hasPasswordRecoveryText =
 484:                 !String.IsNullOrEmpty(login.PasswordRecoveryText);
 485:             bool hasPasswordRecoveryLink = hasPasswordRecoveryIconUrl ||
 486:                 hasPasswordRecoveryText;
 487:             bool hasHelpPageIconUrl =
 488:                 !String.IsNullOrEmpty(login.HelpPageIconUrl);
 489:             bool hasHelpPageText = !String.IsNullOrEmpty(login.HelpPageText);
 490:             bool hasHelpLink = hasHelpPageIconUrl || hasHelpPageText;
 491:             if (hasCreateUserLink || hasPasswordRecoveryLink || hasHelpLink) {
 492:                 var linkList = new HtmlGenericControl("UL") {
 493:                     EnableViewState = false
 494:                 };
 495:                 container.Controls.Add(linkList);
 496:  
 497:                 if (hasCreateUserLink) {
 498:                     linkList.Controls.Add(CreateCreateUserLink(
 499:                         hasCreateUserIconUrl, hasCreateUserText));
 500:                 }
 501:                 if (hasPasswordRecoveryLink) {
 502:                     linkList.Controls.Add(CreatePasswordRecoveryLink(
 503:                         hasPasswordRecoveryIconUrl, hasPasswordRecoveryText));
 504:                 }
 505:                 if (hasHelpLink) {
 506:                     linkList.Controls.Add(CreateHelpPageLink(
 507:                         hasHelpPageIconUrl, hasHelpPageText));
 508:                 }
 509:             }
 510:         }
 511:  
 512:         /// <summary>
 513:         /// Creates the <see cref="Button"/> control used to initiate the
 514:         /// login process.
 515:         /// </summary>
 516:         /// <param name="container">
 517:         /// The parent container <see cref="Control"/> that the new control
 518:         /// is to be added to.
 519:         /// </param>
 520:         private void CreateLoginButton(Control container) {
 521:             var loginButton = new Button {
 522:                 CommandName = "Login",
 523:                 EnableViewState = false,
 524:                 ID = "LoginButton",
 525:                 Text = login.LoginButtonText,
 526:                 ValidationGroup = login.ID
 527:             };
 528:             container.Controls.Add(loginButton);
 529:         }
 530:  
 531:         /// <summary>
 532:         /// Creates the controls that are used to render the
 533:         /// &lt;FIELDSET&gt; containing authentication options.
 534:         /// </summary>
 535:         /// <param name="container">
 536:         /// The parent container <see cref="Control"/> that the new control
 537:         /// is to be added to.
 538:         /// </param>
 539:         private void CreateOptionsFieldSet(Control container) {
 540:             var rememberMeFieldSet = new HtmlGenericControl("FIELDSET") {
 541:                 EnableViewState = false,
 542:                 ID = "RememberMeFieldSet"
 543:             };
 544:             container.Controls.Add(rememberMeFieldSet);
 545:  
 546:             HtmlGenericControl rememberMeLegend =
 547:                 new HtmlGenericControl("LEGEND") {
 548:                     EnableViewState = false,
 549:                     InnerText = "Options"
 550:                 };
 551:             rememberMeFieldSet.Controls.Add(rememberMeLegend);
 552:  
 553:             var rememberMe = new CheckBox {
 554:                 EnableViewState = false,
 555:                 ID = "RememberMe",
 556:                 Text = login.RememberMeText
 557:             };
 558:             rememberMe.MergeStyle(login.CheckBoxStyle);
 559:             rememberMeFieldSet.Controls.Add(rememberMe);
 560:         }
 561:  
 562:         /// <summary>
 563:         /// Creates the controls used to capture the user's password.
 564:         /// </summary>
 565:         /// <param name="container">
 566:         /// The container <see cref="Control"/> that the password controls
 567:         /// will be added to.
 568:         /// </param>
 569:         private void CreatePasswordControls(Control container) {
 570:             var passwordLabel = new Label {
 571:                 AssociatedControlID = "Password",
 572:                 EnableViewState = false,
 573:                 ID = "PasswordLabel",
 574:                 Text = login.PasswordLabelText
 575:             };
 576:             passwordLabel.MergeStyle(login.LabelStyle);
 577:             container.Controls.Add(passwordLabel);
 578:  
 579:             var password = new TextBox {
 580:                 ID = "Password",
 581:                 TextMode = TextBoxMode.Password
 582:             };
 583:             password.MergeStyle(login.TextBoxStyle);
 584:             container.Controls.Add(password);
 585:  
 586:             var passwordValidator = new RequiredFieldValidator {
 587:                 ControlToValidate = "Password",
 588:                 EnableViewState = false,
 589:                 ID = "PasswordRequired",
 590:                 Text = login.PasswordRequiredErrorMessage,
 591:                 ToolTip = login.PasswordRequiredErrorMessage,
 592:                 ValidationGroup = login.ID
 593:             };
 594:             passwordValidator.MergeStyle(login.ValidatorTextStyle);
 595:             container.Controls.Add(passwordValidator);
 596:         }
 597:  
 598:         /// <summary>
 599:         /// Creates the controls used to render the link to recover a
 600:         /// lost password.
 601:         /// </summary>
 602:         /// <param name="hasCreateUserIconUrl">
 603:         /// True if the icon should be rendered.
 604:         /// </param>
 605:         /// <param name="hasCreateUserText">
 606:         /// True if the link text should be rendered.
 607:         /// </param>
 608:         /// <returns>The link.</returns>
 609:         private Control CreatePasswordRecoveryLink(
 610:             bool hasPasswordRecoveryIconUrl, bool hasPasswordRecoveryText) {
 611:             var listItem = new HtmlGenericControl("LI") {
 612:                 EnableViewState = false
 613:             };
 614:  
 615:             var link = new HyperLink() {
 616:                 EnableViewState = false,
 617:                 NavigateUrl = login.PasswordRecoveryUrl
 618:             };
 619:             link.MergeStyle(login.HyperLinkStyle);
 620:             listItem.Controls.Add(link);
 621:  
 622:             if (hasPasswordRecoveryIconUrl) {
 623:                 var image = new Image() {
 624:                     EnableViewState = false,
 625:                     ImageUrl = login.PasswordRecoveryIconUrl
 626:                 };
 627:                 link.Controls.Add(image);
 628:             }
 629:             if (hasPasswordRecoveryText) {
 630:                 var text = new Literal() {
 631:                     Text = login.PasswordRecoveryText
 632:                 };
 633:                 link.Controls.Add(text);
 634:             }
 635:  
 636:             return listItem;
 637:         }
 638:  
 639:         /// <summary>
 640:         /// Creates the controls that are used to render the title of the
 641:         /// <see cref="Login"/> control.
 642:         /// </summary>
 643:         /// <param name="container">
 644:         /// The parent container <see cref="Control"/> that the new control
 645:         /// is to be added to.
 646:         /// </param>
 647:         private void CreateTitleControl(Control container) {
 648:             var title = new HtmlGenericControl("H1") {
 649:                 EnableViewState = false,
 650:                 InnerText = login.TitleText
 651:             };
 652:             SetCssStyles(title, login.TitleTextStyle);
 653:             container.Controls.Add(title);
 654:         }
 655:  
 656:         /// <summary>
 657:         /// Creates the controls that are used to render the user information
 658:         /// field set for the <see cref="Login"/> control.
 659:         /// </summary>
 660:         /// <param name="container">
 661:         /// The parent container <see cref="Control"/> that the new control
 662:         /// is to be added to.
 663:         /// </param>
 664:         private void CreateUserInformationFieldSet(Control container) {
 665:             var userInformationFieldSet =
 666:                 new HtmlGenericControl("FIELDSET") {
 667:                     EnableViewState = false,
 668:                     ID = "UserInformationFieldSet"
 669:                 };
 670:             container.Controls.Add(userInformationFieldSet);
 671:  
 672:             var userInformationLegend =
 673:                 new HtmlGenericControl("LEGEND") {
 674:                     EnableViewState = false,
 675:                     InnerText = "User account information"
 676:                 };
 677:             userInformationFieldSet.Controls.Add(userInformationLegend);
 678:  
 679:             CreateUserNameControls(userInformationFieldSet);
 680:             CreatePasswordControls(userInformationFieldSet);
 681:         }
 682:  
 683:         /// <summary>
 684:         /// Creates the controls used to capture the user's account name.
 685:         /// </summary>
 686:         /// <param name="container">
 687:         /// The container <see cref="Control"/> that the user name controls
 688:         /// will be added to.
 689:         /// </param>
 690:         private void CreateUserNameControls(Control container) {
 691:             var userNameLabel = new Label {
 692:                 AssociatedControlID = "UserName",
 693:                 EnableViewState = false,
 694:                 ID = "UserNameLabel",
 695:                 Text = login.UserNameLabelText
 696:             };
 697:             userNameLabel.MergeStyle(login.LabelStyle);
 698:             container.Controls.Add(userNameLabel);
 699:  
 700:             var userName = new TextBox {
 701:                 ID = "UserName"
 702:             };
 703:             userName.MergeStyle(login.TextBoxStyle);
 704:             container.Controls.Add(userName);
 705:  
 706:             var userNameValidator = new RequiredFieldValidator {
 707:                 ControlToValidate = "UserName",
 708:                 EnableViewState = false,
 709:                 ID = "UserNameRequired",
 710:                 Text = login.UserNameRequiredErrorMessage,
 711:                 ToolTip = login.UserNameRequiredErrorMessage,
 712:                 ValidationGroup = login.ID
 713:             };
 714:             userNameValidator.MergeStyle(login.ValidatorTextStyle);
 715:             container.Controls.Add(userNameValidator);
 716:         }
 717:  
 718:         /// <summary>
 719:         /// Sets the CSS class name and CSS styles for a
 720:         /// <see cref="HtmlControl"/>.
 721:         /// </summary>
 722:         /// <param name="control">
 723:         /// The <see cref="HtmlControl"/> control.
 724:         /// </param>
 725:         /// <param name="style">
 726:         /// A <see cref="Style"/> object containing the CSS settings for
 727:         /// the control.
 728:         /// </param>
 729:         private void SetCssStyles(HtmlControl control, Style style) {
 730:             if (!style.IsEmpty) {
 731:                 if (!String.IsNullOrEmpty(style.CssClass)) {
 732:                     control.Attributes.Add("class", style.CssClass);
 733:                 }
 734:  
 735:                 var styles = style.GetStyleAttributes(login.Page);
 736:                 foreach (string key in styles.Keys) {
 737:                     control.Style.Add(key, styles[key]);
 738:                 }
 739:             }
 740:         }
 741:     }
 742: }

 

Technorati Tags: ,,

LoginAdapter.cs (27.63 kb)



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

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 listItem = new HtmlGenericControl("LI") {
 269:                 EnableViewState = false
 270:             };
 271:  
 272:             var link = new HyperLink() {
 273:                 EnableViewState = false,
 274:                 NavigateUrl = login.HelpPageUrl
 275:             };
 276:             link.MergeStyle(login.HyperLinkStyle);
 277:             listItem.Controls.Add(link);
 278:  
 279:             if (hasHelpPageIconUrl) {
 280:                 var image = new Image() {
 281:                     EnableViewState = false,
 282:                     ImageUrl = login.HelpPageIconUrl
 283:                 };
 284:                 link.Controls.Add(image);
 285:             }
 286:             if (hasHelpPageText) {
 287:                 var text = new Literal() {
 288:                     Text = login.HelpPageText
 289:                 };
 290:                 link.Controls.Add(text);
 291:             }
 292:  
 293:             return listItem;
 294:         }
 295:  
 296:         /// <summary>
 297:         /// Creates the controls that are used to render the instructions
 298:         /// for the <see cref="Login"/> control.
 299:         /// </summary>
 300:         /// <param name="container">
 301:         /// The parent container <see cref="Control"/> that the new control
 302:         /// is to be added to.
 303:         /// </param>
 304:         private void CreateInstructionControl(Control container) {
 305:             var instructions = new HtmlGenericControl("DIV") {
 306:                 EnableViewState = false,
 307:                 InnerHtml = login.InstructionText
 308:             };
 309:             SetCssStyles(instructions, login.InstructionTextStyle);
 310:             container.Controls.Add(instructions);
 311:         }
 312:  
 313:         /// <summary>
 314:         /// Creates the controls used to render helpful links to other pages.
 315:         /// </summary>
 316:         /// <param name="container">
 317:         /// The parent container <see cref="Control"/> that the new control
 318:         /// is to be added to.
 319:         /// </param>
 320:         private void CreateLinks(Control container) {
 321:             bool hasCreateUserIconUrl =
 322:                 !String.IsNullOrEmpty(login.CreateUserIconUrl);
 323:             bool hasCreateUserText =
 324:                 !String.IsNullOrEmpty(login.CreateUserText);
 325:             bool hasCreateUserLink = hasCreateUserIconUrl || hasCreateUserText;
 326:             bool hasPasswordRecoveryIconUrl =
 327:                 !String.IsNullOrEmpty(login.PasswordRecoveryIconUrl);
 328:             bool hasPasswordRecoveryText =
 329:                 !String.IsNullOrEmpty(login.PasswordRecoveryText);
 330:             bool hasPasswordRecoveryLink = hasPasswordRecoveryIconUrl ||
 331:                 hasPasswordRecoveryText;
 332:             bool hasHelpPageIconUrl =
 333:                 !String.IsNullOrEmpty(login.HelpPageIconUrl);
 334:             bool hasHelpPageText = !String.IsNullOrEmpty(login.HelpPageText);
 335:             bool hasHelpLink = hasHelpPageIconUrl || hasHelpPageText;
 336:             if (hasCreateUserLink || hasPasswordRecoveryLink || hasHelpLink) {
 337:                 var linkList = new HtmlGenericControl("UL") {
 338:                     EnableViewState = false
 339:                 };
 340:                 container.Controls.Add(linkList);
 341:  
 342:                 if (hasCreateUserLink) {
 343:                     linkList.Controls.Add(CreateCreateUserLink(
 344:                         hasCreateUserIconUrl, hasCreateUserText));
 345:                 }
 346:                 if (hasPasswordRecoveryLink) {
 347:                     linkList.Controls.Add(CreatePasswordRecoveryLink(
 348:                         hasPasswordRecoveryIconUrl, hasPasswordRecoveryText));
 349:                 }
 350:                 if (hasHelpLink) {
 351:                     linkList.Controls.Add(CreateHelpPageLink(
 352:                         hasHelpPageIconUrl, hasHelpPageText));
 353:                 }
 354:             }
 355:         }
 356:  
 357:         /// <summary>
 358:         /// Creates the <see cref="Button"/> control used to initiate the
 359:         /// login process.
 360:         /// </summary>
 361:         /// <param name="container">
 362:         /// The parent container <see cref="Control"/> that the new control
 363:         /// is to be added to.
 364:         /// </param>
 365:         private void CreateLoginButton(Control container) {
 366:             var loginButton = new Button {
 367:                 CommandName = "Login",
 368:                 EnableViewState = false,
 369:                 ID = "LoginButton",
 370:                 Text = login.LoginButtonText,
 371:                 ValidationGroup = login.ID
 372:             };
 373:             container.Controls.Add(loginButton);
 374:         }
 375:  
 376:         /// <summary>
 377:         /// Creates the controls that are used to render the
 378:         /// &lt;FIELDSET&gt; containing authentication options.
 379:         /// </summary>
 380:         /// <param name="container">
 381:         /// The parent container <see cref="Control"/> that the new control
 382:         /// is to be added to.
 383:         /// </param>
 384:         private void CreateOptionsFieldSet(Control container) {
 385:             var rememberMeFieldSet = new HtmlGenericControl("FIELDSET") {
 386:                 EnableViewState = false,
 387:                 ID = "RememberMeFieldSet"
 388:             };
 389:             container.Controls.Add(rememberMeFieldSet);
 390:  
 391:             HtmlGenericControl rememberMeLegend =
 392:                 new HtmlGenericControl("LEGEND") {
 393:                     EnableViewState = false,
 394:                     InnerText = "Options"
 395:                 };
 396:             rememberMeFieldSet.Controls.Add(rememberMeLegend);
 397:  
 398:             var rememberMe = new CheckBox {
 399:                 EnableViewState = false,
 400:                 ID = "RememberMe",
 401:                 Text = login.RememberMeText
 402:             };
 403:             rememberMe.MergeStyle(login.CheckBoxStyle);
 404:             rememberMeFieldSet.Controls.Add(rememberMe);
 405:         }
 406:  
 407:         /// <summary>
 408:         /// Creates the controls used to capture the user's password.
 409:         /// </summary>
 410:         /// <param name="container">
 411:         /// The container <see cref="Control"/> that the password controls
 412:         /// will be added to.
 413:         /// </param>
 414:         private void CreatePasswordControls(Control container) {
 415:             var passwordLabel = new Label {
 416:                 AssociatedControlID = "Password",
 417:                 EnableViewState = false,
 418:                 ID = "PasswordLabel",
 419:                 Text = login.PasswordLabelText
 420:             };
 421:             passwordLabel.MergeStyle(login.LabelStyle);
 422:             container.Controls.Add(passwordLabel);
 423:  
 424:             var password = new TextBox {
 425:                 ID = "Password",
 426:                 TextMode = TextBoxMode.Password
 427:             };
 428:             password.MergeStyle(login.TextBoxStyle);
 429:             container.Controls.Add(password);
 430:  
 431:             var passwordValidator = new RequiredFieldValidator {
 432:                 ControlToValidate = "Password",
 433:                 EnableViewState = false,
 434:                 ID = "PasswordRequired",
 435:                 Text = login.PasswordRequiredErrorMessage,
 436:                 ToolTip = login.PasswordRequiredErrorMessage,
 437:                 ValidationGroup = login.ID
 438:             };
 439:             passwordValidator.MergeStyle(login.ValidatorTextStyle);
 440:             container.Controls.Add(passwordValidator);
 441:         }
 442:  
 443:         /// <summary>
 444:         /// Creates the controls used to render the link to recover a
 445:         /// lost password.
 446:         /// </summary>
 447:         /// <param name="hasCreateUserIconUrl">
 448:         /// True if the icon should be rendered.
 449:         /// </param>
 450:         /// <param name="hasCreateUserText">
 451:         /// True if the link text should be rendered.
 452:         /// </param>
 453:         /// <returns>The link.</returns>
 454:         private Control CreatePasswordRecoveryLink(
 455:             bool hasPasswordRecoveryIconUrl, bool hasPasswordRecoveryText) {
 456:             var listItem = new HtmlGenericControl("LI") {
 457:                 EnableViewState = false
 458:             };
 459:  
 460:             var link = new HyperLink() {
 461:                 EnableViewState = false,
 462:                 NavigateUrl = login.PasswordRecoveryUrl
 463:             };
 464:             link.MergeStyle(login.HyperLinkStyle);
 465:             listItem.Controls.Add(link);
 466:  
 467:             if (hasPasswordRecoveryIconUrl) {
 468:                 var image = new Image() {
 469:                     EnableViewState = false,
 470:                     ImageUrl = login.PasswordRecoveryIconUrl
 471:                 };
 472:                 link.Controls.Add(image);
 473:             }
 474:             if (hasPasswordRecoveryText) {
 475:                 var text = new Literal() {
 476:                     Text = login.PasswordRecoveryText
 477:                 };
 478:                 link.Controls.Add(text);
 479:             }
 480:  
 481:             return listItem;
 482:         }
 483:  
 484:         /// <summary>
 485:         /// Creates the controls that are used to render the title of the
 486:         /// <see cref="Login"/> control.
 487:         /// </summary>
 488:         /// <param name="container">
 489:         /// The parent container <see cref="Control"/> that the new control
 490:         /// is to be added to.
 491:         /// </param>
 492:         private void CreateTitleControl(Control container) {
 493:             var title = new HtmlGenericControl("H1") {
 494:                 EnableViewState = false,
 495:                 InnerText = login.TitleText
 496:             };
 497:             SetCssStyles(title, login.TitleTextStyle);
 498:             container.Controls.Add(title);
 499:         }
 500:  
 501:         /// <summary>
 502:         /// Creates the controls that are used to render the user information
 503:         /// field set for the <see cref="Login"/> control.
 504:         /// </summary>
 505:         /// <param name="container">
 506:         /// The parent container <see cref="Control"/> that the new control
 507:         /// is to be added to.
 508:         /// </param>
 509:         private void CreateUserInformationFieldSet(Control container) {
 510:             var userInformationFieldSet =
 511:                 new HtmlGenericControl("FIELDSET") {
 512:                     EnableViewState = false,
 513:                     ID = "UserInformationFieldSet"
 514:                 };
 515:             container.Controls.Add(userInformationFieldSet);
 516:  
 517:             var userInformationLegend =
 518:                 new HtmlGenericControl("LEGEND") {
 519:                     EnableViewState = false,
 520:                     InnerText = "User account information"
 521:                 };
 522:             userInformationFieldSet.Controls.Add(userInformationLegend);
 523:  
 524:             CreateUserNameControls(userInformationFieldSet);
 525:             CreatePasswordControls(userInformationFieldSet);
 526:         }
 527:  
 528:         /// <summary>
 529:         /// Creates the controls used to capture the user's account name.
 530:         /// </summary>
 531:         /// <param name="container">
 532:         /// The container <see cref="Control"/> that the user name controls
 533:         /// will be added to.
 534:         /// </param>
 535:         private void CreateUserNameControls(Control container) {
 536:             var userNameLabel = new Label {
 537:                 AssociatedControlID = "UserName",
 538:                 EnableViewState = false,
 539:                 ID = "UserNameLabel",
 540:                 Text = login.UserNameLabelText
 541:             };
 542:             userNameLabel.MergeStyle(login.LabelStyle);
 543:             container.Controls.Add(userNameLabel);
 544:  
 545:             var userName = new TextBox {
 546:                 ID = "UserName"
 547:             };
 548:             userName.MergeStyle(login.TextBoxStyle);
 549:             container.Controls.Add(userName);
 550:  
 551:             var userNameValidator = new RequiredFieldValidator {
 552:                 ControlToValidate = "UserName",
 553:                 EnableViewState = false,
 554:                 ID = "UserNameRequired",
 555:                 Text = login.UserNameRequiredErrorMessage,
 556:                 ToolTip = login.UserNameRequiredErrorMessage,
 557:                 ValidationGroup = login.ID
 558:             };
 559:             userNameValidator.MergeStyle(login.ValidatorTextStyle);
 560:             container.Controls.Add(userNameValidator);
 561:         }
 562:  
 563:         /// <summary>
 564:         /// Sets the CSS class name and CSS styles for a
 565:         /// <see cref="HtmlControl"/>.
 566:         /// </summary>
 567:         /// <param name="control">
 568:         /// The <see cref="HtmlControl"/> control.
 569:         /// </param>
 570:         /// <param name="style">
 571:         /// A <see cref="Style"/> object containing the CSS settings for
 572:         /// the control.
 573:         /// </param>
 574:         private void SetCssStyles(HtmlControl control, Style style) {
 575:             if (!style.IsEmpty) {
 576:                 if (!String.IsNullOrEmpty(style.CssClass)) {
 577:                     control.Attributes.Add("class", style.CssClass);
 578:                 }
 579:  
 580:                 var styles = style.GetStyleAttributes(login.Page);
 581:                 foreach (string key in styles.Keys) {
 582:                     control.Style.Add(key, styles[key]);
 583:                 }
 584:             }
 585:         }
 586:     }
 587: }

 

There are a couple of things to notice about my adapter compared to the sample code in the ASP.NET 2.0 CSS Friendly Control Adapters website.  First, while the sample code does all of the rendering for the Login control manually, I don't.  I'm doing what I did in my custom template and creating child ASP.NET server controls and adding them to a container Control object.  The reason why I'm using server controls is that the paths for hyperlinks may be application virtual paths that need to be resolved.  Also, there are validation controls that are going to have JavaScript to be executed on the client side that needs to be registered.  I just decided to go the easy route.

My template works using the code on lines 36 and 37 in the listing above.  On line 36, I'm clearing out the child controls of the Login control.  I do this because by the time that the adapter is called, the Login control would have already created its default template and those controls will already be in the Controls collection of the Login control.  If I add my template as a child of the Login control, then there will be duplicate user name, password, remember me, failure text, and login button controls in the Controls collection.  Once the Controls collection is cleared, I add my custom template as a new child control of the Login control.

On line 117, I do the rendering of the contents of the Login control.  You can see that I perform the rendering using the Control.RenderControl method.  I use this approach to completely render the template, including all controls, text, and JavaScript event handlers for the Login form.  When you combine this call with the fact that all of the controls are children of the Login control and thus attached to the host Page object, everything renders correctly and all of the client-side JavaScript validation and event handlers are hooked up correctly.

The last piece is removing the containing TABLE element and replacing it with a containing DIV element.  This is handled by the RenderBeginTag method starting on line 89.  I render a containing DIV element, complete with the CSS class and styles that have been declared on the Login control.

The rendered HTML output using this adapter is shown below:

image

As you can see from the DOM tree in the IE Developer Toolbar, we have completely eliminated the table-based layouts provided by the standard Login control rendering.  We now have a containing DIV and our standard template, and using the adapter in our website, this will be the default rendering behavior for the Login control across all of our application's themes.

One last question that you may have is will this Login adapter prevent you from using my previous technique for creating theme-specific layouts for the Login form?  The answer is that it won't.  You can still use the Login.LayoutTemplate property to customize the layout of the Login control.  If you look at line 29 in the source code listing above, you'll see that I'm looking to see if a template has been stored in the Login.LayoutTemplate property.  If a custom template has been provided, then my adapter will use that template to render the contents of the Login control.  The adapter will still render the containing DIV instead of the default TABLE elements, however.

With this done, all that we need to do now is build the CSS for our themes and style the Login control.  Theme away!

LoginAdapter.cs (21.83 kb)



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted on March 12, 2008 23:53 by mcollins

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"