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" 
  17:         DestinationPageUrl="~/Default.aspx" 
  18:         HelpPageText="Do you have questions about signing into the 
  19:             website? Click here to get assistance." 
  20:         HelpPageUrl="~/Help/LogOnHelp.aspx" 
  21:         InstructionText="In order to log into the website, please 
  22:             enter your user name and password into the fields 
  23:             below." 
  24:         PasswordRecoveryText="Did you lost or forget your password? 
  25:             Click here to get assistance." 
  26:         PasswordRecoveryUrl="~/RecoverPassword.aspx" 
  27:         VisibleWhenLoggedIn="False">
  28:     </asp:Login>
  29:     </form>
  30: </body>
  31: </html>

 

Here's how this page looks by default in the browser:

image

The control looks decent, but it definitely needs some styling to make it look better.  I started off by creating a new ASP.NET theme complete with CSS style sheets.  I then created a skin for the Login control and mapped the Login control's parts to my style sheets using CSS classes:

   1: <asp:Login runat="server" CssClass="login">
   2:     <CheckBoxStyle CssClass="checkBox"/>
   3:     <FailureTextStyle CssClass="failureText"/>
   4:     <HyperLinkStyle CssClass="hyperlink"/>
   5:     <InstructionTextStyle CssClass="instructionText"/>
   6:     <LabelStyle CssClass="label"/>
   7:     <LoginButtonStyle CssClass="loginButton"/>
   8:     <TextBoxStyle CssClass="textBox"/>
   9:     <TitleTextStyle CssClass="titleText"/>
  10:     <ValidatorTextStyle CssClass="validatorText"/>
  11: </asp:Login>

 

What this skin does is assigns the login CSS class to the top-level element for the Login control.  For each component in the Login control's output, the skin assigns CSS classes to those components.  For example, the text boxes for the user name and password will be given the CSS class textBox.  The title for the Login control will be given the CSS class titleText.  Therefore, the Login control's elements will get the visual styles that I define in the CSS style sheets for the theme.

This is ok, but in thinking about how I want my Login control to render in my theme, the way that it's set up right now doesn't work for me.  First, look at the HTML representation of the Login control in the following image:

image

The problem really is that the Login control is rendered using very CSS-unfriendly markup that will be hard to manipulate to match my hypothetical theme.  The Login control is rendered as a table inside of a table.  Each label and text box are in their own cells in the table.  There's not very much that I can easily do to change how the Login control gets rendered by the browser.

What I would really like to do is change how the content for the Login control is output.  Fortunately, I can do that using the LayoutTemplate property of the Login control.  The Login control is a special kind of ASP.NET control called a template control.  Template controls allow users to replace portions of the HTML markup generated by the code with custom HTML.  Using the LayoutTemplate property, I can create a custom layout for the Login control that might look like the following:

   1: <asp:Login ...>
   2:     <LayoutTemplate>
   3:         <h1>Log in</h1>
   4:         <div>Instructions go here</div>
   5:         <div>
   6:             <asp:Literal ID="FailureText" runat="server" />
   7:         </div>
   8:         <fieldset>
   9:             <legend>User account information</legend>
  10:             <asp:Label runat="server" ID="UserNameLabel"
  11:                 AssociatedControlID="UserName"
  12:                 Text="User name:" />
  13:             <asp:TextBox runat="server" ID="UserName" />
  14:             <asp:Label runat="server" ID="PasswordLabel"
  15:                 AssociatedControlID="Password"
  16:                 Text="Password:"/>
  17:             <asp:TextBox runat="server" ID="Password"
  18:                 TextMode="Password" />
  19:         </fieldset>
  20:         <fieldset>
  21:             <legend>Options</legend>
  22:             <asp:CheckBox runat="server" ID="RememberMe"
  23:                 Text="Remember me next time?" />
  24:         </fieldset>
  25:         <asp:Button ID="LoginButton" runat="server"
  26:             CommandName="Login" Text="Login" />
  27:     </LayoutTemplate>
  28: </asp:Login>

 

Here's how the Login form looks, with the HTML markup:

image

The HTML definitely looks better from a CSS perspective.  If we could decorate the template with CSS classes, it would definitely be easier to style and manipulate with CSS, even though the Login control still renders the outer HTML (we'll do something about that in a future post).

The problem that still exists with this method is that this template is going to stick around and apply to all themes, which may not be that bad.  But this template is also going to be the same, regardless of theme, and the template is not going to get to take advantage of several features of the ASP.NET Login control.  First, the CSS classes must be specified inline in the template.  The CSS classes specified on the skin won't apply to the custom template.  Also, the text settings and localization features of the Login control won't affect the custom template.  Changing the text for any of the prompts or properties on the Login control will not affect the template.  There are definitely some more issues that we have to consider.

What if we could just ask developers to drop the base ASP.NET Login control on a page and let our theme designers create custom templates for the Login control to match the theme?  Can that be done?  Surprisingly (to me at least), yes it can.

Whenever I've read about ASP.NET themes or seen them in use, I only see setting style properties for the controls.  But after testing it out, it turns out that the LayoutTemplate property for a Login control (and other ASP.NET controls), can be customized in a .skin file for a template.  So, let's revert our Login control back to the original form in the first example and customize the .skin file:

   1: <asp:Login runat="server" CssClass="login">
   2:     <CheckBoxStyle CssClass="checkBox"/>
   3:     <FailureTextStyle CssClass="failureText"/>
   4:     <HyperLinkStyle CssClass="hyperlink"/>
   5:     <InstructionTextStyle CssClass="instructionText"/>
   6:     <LabelStyle CssClass="label"/>
   7:     <LayoutTemplate>
   8:         <h1>Log in</h1>
   9:         <div>Instructions go here</div>
  10:         <div>
  11:             <asp:Literal ID="FailureText" runat="server" />
  12:         </div>
  13:         <fieldset>
  14:             <legend>User account information</legend>
  15:             <asp:Label runat="server" ID="UserNameLabel"
  16:                 AssociatedControlID="UserName"
  17:                 Text="User name:" />
  18:             <asp:TextBox runat="server" ID="UserName" />
  19:             <asp:Label runat="server" ID="PasswordLabel"
  20:                 AssociatedControlID="Password"
  21:                 Text="Password:"/>
  22:             <asp:TextBox runat="server" ID="Password"
  23:                 TextMode="Password" />
  24:         </fieldset>
  25:         <fieldset>
  26:             <legend>Options</legend>
  27:             <asp:CheckBox runat="server" ID="RememberMe"
  28:                 Text="Remember me next time?" />
  29:         </fieldset>
  30:         <asp:Button ID="LoginButton" runat="server"
  31:             CommandName="Login" Text="Login" />
  32:     </LayoutTemplate>
  33:     <LoginButtonStyle CssClass="loginButton"/>
  34:     <TextBoxStyle CssClass="textBox"/>
  35:     <TitleTextStyle CssClass="titleText"/>
  36:     <ValidatorTextStyle CssClass="validatorText"/>
  37: </asp:Login>

 

The rendered page will look the same as the previous example.  This is great.  Our developers only have to worry about what controls to use on a page, and the theme designers can now replace the markup for the controls in the .skin files for the themes that they are designing.  Win-win for everyone...almost.  There is a problem.

While the designers have complete control of the layout of the control, and can manually set the CSS classes in the markup generated by the .skin file, the content has issues.  Again, using a custom template makes a lot of the settings of the Login control useless.  The other big problem here is that the Login control cannot be localized using resource files because the content is static in the template.  To localize the website, each theme is going to have to be duplicated and translated.  Not a good thing if you care about building global websites.

So the solution that we're looking for is a way to allow designers to customize the templates for ASP.NET controls using themes and skins, while still being able to utilize the built-in settings of the ASP.NET controls and localization support, and still being able to generate CSS-friendly output.  One thought that crossed my mind was that maybe I could put the template in a user control, because then I could still localize the markup in the user control using resource files, but that didn't work out.  User controls are a naming container in ASP.NET.  What that basically means is that any child controls added to a user control are guaranteed to have a unique name in the page in which the user name is embedded.  The downside of this when looking at the Login control is that the Login control has special needs for its layout template.  Specifically, the name of the text boxes for the user name and password, the remember me checkbox, and the login button all need to have specific IDs and be locatable by the Login control.  This means that they need to be direct children of the Login control and not placed in a separate naming container (since the Login control is already a naming container of its own).  This left one possible alternative: server controls.

My hypothesis was that if I created an ASP.NET server control to render my special themed Login control, I could get around these problems while still being able to utilize the settings and localization support of the Login control.  By deriving a server control from the base Control class, I could get access to the parent Login control's settings as well as the overridden settings in the .skin file, to correctly render the desired markup.  Here's my updated .skin file:

   1: <asp:Login runat="server" CssClass="login">
   2:     <CheckBoxStyle CssClass="checkBox"/>
   3:     <FailureTextStyle CssClass="failureText"/>
   4:     <HyperLinkStyle CssClass="hyperlink"/>
   5:     <InstructionTextStyle CssClass="instructionText"/>
   6:     <LabelStyle CssClass="label"/>
   7:     <LayoutTemplate>
   8:         <web:LogOnTemplate runat="server"/>
   9:     </LayoutTemplate>
  10:     <LoginButtonStyle CssClass="loginButton"/>
  11:     <TextBoxStyle CssClass="textBox"/>
  12:     <TitleTextStyle CssClass="titleText"/>
  13:     <ValidatorTextStyle CssClass="validatorText"/>
  14: </asp:Login>

 

I created a special server control named LogOnTemplate that renders my CSS-friendly Login control using my theme settings.  I should note that I am able to use my custom control in the .skin file because I registered the control in the system.web/pages section of my Web.config file.  Here's how the page renders on the screen:

image

And here's the code for my custom template control:

   1: using System;
   2: using System.Diagnostics;
   3: using System.Web.UI;
   4: using System.Web.UI.HtmlControls;
   5: using System.Web.UI.WebControls;
   6:  
   7: namespace Sogeti {
   8:     /// <summary>
   9:     /// ASP.NET server control that renders the layout template for the
  10:     /// <see cref="Login"/> control for the default website template.
  11:     /// </summary>
  12:     public class LogOnTemplate : Control {
  13:         /// <summary>
  14:         /// The &lt;DIV&gt element that contains the login failure message.
  15:         /// </summary>
  16:         private HtmlGenericControl failureTextDiv;
  17:  
  18:         /// <summary>
  19:         /// The <see cref="TextBox"/> containing the user's password.
  20:         /// </summary>
  21:         private TextBox password;
  22:  
  23:         /// <summary>
  24:         /// The <see cref="CheckBox"/> that the user checks to persist
  25:         /// the user's identity in a cookie.
  26:         /// </summary>
  27:         private CheckBox rememberMe;
  28:  
  29:         /// <summary>
  30:         /// The <see cref="TextBox"/> containing the user's account name.
  31:         /// </summary>
  32:         private TextBox userName;
  33:  
  34:         /// <summary>
  35:         /// Creates the controls for the default <see cref="Login"/> form's
  36:         /// template.
  37:         /// </summary>
  38:         /// <param name="e">The event arguments.</param>
  39:         protected override void OnInit(EventArgs e) {
  40:             base.OnInit(e);
  41:  
  42:             var login = Parent.Parent as Login;
  43:             Debug.Assert(login != null, "login != null");
  44:  
  45:             CreateTitleControl(login);
  46:             CreateInstructionControl(login);
  47:             CreateFailureControl(login);
  48:             CreateUserInformationFieldSet(login);
  49:             CreateOptionsFieldSet(login);
  50:             CreateLoginButton(login);
  51:             CreateLinks(login);
  52:         }
  53:  
  54:         /// <summary>
  55:         /// Attaches an event handler to the <see cref="Login.LoginError"/>
  56:         /// event and sets the initial values for the controls on the form.
  57:         /// </summary>
  58:         /// <param name="e">The event arguments.</param>
  59:         protected override void OnLoad(EventArgs e) {
  60:             base.OnLoad(e);
  61:  
  62:             var login = Parent.Parent as Login;
  63:             Debug.Assert(login != null, "login != null");
  64:             login.LoginError += login_LoginError;
  65:  
  66:             if (!Page.IsPostBack) {
  67:                 userName.Text = login.UserName;
  68:                 password.Text = login.Password;
  69:                 rememberMe.Checked = login.RememberMeSet;
  70:             }
  71:         }
  72:  
  73:         /// <summary>
  74:         /// Detaches the event handler from the
  75:         /// <see cref="Login.LoginError"/> event.
  76:         /// </summary>
  77:         /// <param name="e">The event arguments.</param>
  78:         protected override void OnUnload(EventArgs e) {
  79:             var login = Parent.Parent as Login;
  80:             Debug.Assert(login != null, "login != null");
  81:             login.LoginError -= login_LoginError;
  82:  
  83:             base.OnUnload(e);
  84:         }
  85:  
  86:         /// <summary>
  87:         /// Creates the controls used to render the link to create a new
  88:         /// user account.
  89:         /// </summary>
  90:         /// <param name="login">The <see cref="Login"/> control.</param>
  91:         /// <param name="hasCreateUserIconUrl">
  92:         /// True if the icon should be rendered.
  93:         /// </param>
  94:         /// <param name="hasCreateUserText">
  95:         /// True if the link text should be rendered.
  96:         /// </param>
  97:         /// <returns>The link.</returns>
  98:         private Control CreateCreateUserLink(Login login,
  99:             bool hasCreateUserIconUrl, bool hasCreateUserText) {
 100:             var listItem = new HtmlGenericControl("LI") {
 101:                 EnableViewState = false
 102:             };
 103:  
 104:             var link = new HyperLink() {
 105:                 EnableViewState = false,
 106:                 NavigateUrl = login.CreateUserUrl
 107:             };
 108:             link.MergeStyle(login.HyperLinkStyle);
 109:             listItem.Controls.Add(link);
 110:  
 111:             if (hasCreateUserIconUrl) {
 112:                 var image = new Image() {
 113:                     EnableViewState = false,
 114:                     ImageUrl = login.CreateUserIconUrl
 115:                 };
 116:                 link.Controls.Add(image);
 117:             }
 118:             if (hasCreateUserText) {
 119:                 var text = new Literal() {
 120:                     Text = login.CreateUserText
 121:                 };
 122:                 link.Controls.Add(text);
 123:             }
 124:  
 125:             return listItem;
 126:         }
 127:  
 128:         /// <summary>
 129:         /// Creates the controls that are used to render the failure message
 130:         /// for the <see cref="Login"/> control.
 131:         /// </summary>
 132:         /// <param name="login">The <see cref="Login"/> control.</param>
 133:         private void CreateFailureControl(Login login) {
 134:             failureTextDiv = new HtmlGenericControl("DIV") {
 135:                 EnableViewState = false,
 136:                 Visible = false
 137:             };
 138:             SetCssStyles(failureTextDiv, login.FailureTextStyle);
 139:             Controls.Add(failureTextDiv);
 140:  
 141:             var failureText = new Literal {
 142:                 EnableViewState = false,
 143:                 ID = "FailureText"
 144:             };
 145:             failureTextDiv.Controls.Add(failureText);
 146:         }
 147:  
 148:         /// <summary>
 149:         /// Creates the controls used to render the link to navigate to a
 150:         /// help page for the login form.
 151:         /// </summary>
 152:         /// <param name="login">The <see cref="Login"/> control.</param>
 153:         /// <param name="hasCreateUserIconUrl">
 154:         /// True if the icon should be rendered.
 155:         /// </param>
 156:         /// <param name="hasCreateUserText">
 157:         /// True if the link text should be rendered.
 158:         /// </param>
 159:         /// <returns>The link.</returns>
 160:         private Control CreateHelpPageLink(Login login,
 161:             bool hasHelpPageIconUrl, bool hasHelpPageText) {
 162:             var listItem = new HtmlGenericControl("LI") {
 163:                 EnableViewState = false
 164:             };
 165:  
 166:             var link = new HyperLink() {
 167:                 EnableViewState = false,
 168:                 NavigateUrl = login.HelpPageUrl
 169:             };
 170:             link.MergeStyle(login.HyperLinkStyle);
 171:             listItem.Controls.Add(link);
 172:  
 173:             if (hasHelpPageIconUrl) {
 174:                 var image = new Image() {
 175:                     EnableViewState = false,
 176:                     ImageUrl = login.HelpPageIconUrl
 177:                 };
 178:                 link.Controls.Add(image);
 179:             }
 180:             if (hasHelpPageText) {
 181:                 var text = new Literal() {
 182:                     Text = login.HelpPageText
 183:                 };
 184:                 link.Controls.Add(text);
 185:             }
 186:  
 187:             return listItem;
 188:         }
 189:  
 190:         /// <summary>
 191:         /// Creates the controls that are used to render the instructions
 192:         /// for the <see cref="Login"/> control.
 193:         /// </summary>
 194:         /// <param name="login">The <see cref="Login"/> control.</param>
 195:         private void CreateInstructionControl(Login login) {
 196:             var instructions = new HtmlGenericControl("DIV") {
 197:                 EnableViewState = false,
 198:                 InnerHtml = login.InstructionText
 199:             };
 200:             SetCssStyles(instructions, login.InstructionTextStyle);
 201:             Controls.Add(instructions);
 202:         }
 203:  
 204:         /// <summary>
 205:         /// Creates the controls used to render helpful links to other pages.
 206:         /// </summary>
 207:         /// <param name="login">The <see cref="Login"/> control.</param>
 208:         private void CreateLinks(Login login) {
 209:             bool hasCreateUserIconUrl =
 210:                 !String.IsNullOrEmpty(login.CreateUserIconUrl);
 211:             bool hasCreateUserText =
 212:                 !String.IsNullOrEmpty(login.CreateUserText);
 213:             bool hasCreateUserLink = hasCreateUserIconUrl || hasCreateUserText;
 214:             bool hasPasswordRecoveryIconUrl =
 215:                 !String.IsNullOrEmpty(login.PasswordRecoveryIconUrl);
 216:             bool hasPasswordRecoveryText =
 217:                 !String.IsNullOrEmpty(login.PasswordRecoveryText);
 218:             bool hasPasswordRecoveryLink = hasPasswordRecoveryIconUrl ||
 219:                 hasPasswordRecoveryText;
 220:             bool hasHelpPageIconUrl =
 221:                 !String.IsNullOrEmpty(login.HelpPageIconUrl);
 222:             bool hasHelpPageText = !String.IsNullOrEmpty(login.HelpPageText);
 223:             bool hasHelpLink = hasHelpPageIconUrl || hasHelpPageText;
 224:             if (hasCreateUserLink || hasPasswordRecoveryLink || hasHelpLink) {
 225:                 var linkList = new HtmlGenericControl("UL") {
 226:                     EnableViewState = false
 227:                 };
 228:                 Controls.Add(linkList);
 229:  
 230:                 if (hasCreateUserLink) {
 231:                     linkList.Controls.Add(CreateCreateUserLink(login,
 232:                         hasCreateUserIconUrl, hasCreateUserText));
 233:                 }
 234:                 if (hasPasswordRecoveryLink) {
 235:                     linkList.Controls.Add(CreatePasswordRecoveryLink(login,
 236:                         hasPasswordRecoveryIconUrl, hasPasswordRecoveryText));
 237:                 }
 238:                 if (hasHelpLink) {
 239:                     linkList.Controls.Add(CreateHelpPageLink(login,
 240:                         hasHelpPageIconUrl, hasHelpPageText));
 241:                 }
 242:             }
 243:         }
 244:  
 245:         /// <summary>
 246:         /// Creates the <see cref="Button"/> control used to initiate the
 247:         /// login process.
 248:         /// </summary>
 249:         /// <param name="login">The <see cref="Login"/> control.</param>
 250:         private void CreateLoginButton(Login login) {
 251:             var loginButton = new Button {
 252:                 CommandName = "Login",
 253:                 EnableViewState = false,
 254:                 ID = "LoginButton",
 255:                 Text = login.LoginButtonText,
 256:                 ValidationGroup = login.ID
 257:             };
 258:             Controls.Add(loginButton);
 259:         }
 260:  
 261:         /// <summary>
 262:         /// Creates the controls that are used to render the
 263:         /// &lt;FIELDSET&gt; containing authentication options.
 264:         /// </summary>
 265:         /// <param name="login">The <see cref="Login"/> control.</param>
 266:         private void CreateOptionsFieldSet(Login login) {
 267:             var rememberMeFieldSet = new HtmlGenericControl("FIELDSET") {
 268:                 EnableViewState = false,
 269:                 ID = "RememberMeFieldSet"
 270:             };
 271:             Controls.Add(rememberMeFieldSet);
 272:  
 273:             HtmlGenericControl rememberMeLegend =
 274:                 new HtmlGenericControl("LEGEND") {
 275:                     EnableViewState = false,
 276:                     InnerText = "Options"
 277:                 };
 278:             rememberMeFieldSet.Controls.Add(rememberMeLegend);
 279:  
 280:             rememberMe = new CheckBox {
 281:                 EnableViewState = false,
 282:                 ID = "RememberMe",
 283:                 Text = login.RememberMeText
 284:             };
 285:             rememberMe.MergeStyle(login.CheckBoxStyle);
 286:             rememberMeFieldSet.Controls.Add(rememberMe);
 287:         }
 288:  
 289:         /// <summary>
 290:         /// Creates the controls used to capture the user's password.
 291:         /// </summary>
 292:         /// <param name="login">The <see cref="Login"/> control.</param>
 293:         /// <param name="container">
 294:         /// The container <see cref="Control"/> that the password controls
 295:         /// will be added to.
 296:         /// </param>
 297:         private void CreatePasswordControls(Login login, Control container) {
 298:             var passwordLabel = new Label {
 299:                 AssociatedControlID = "Password",
 300:                 EnableViewState = false,
 301:                 ID = "PasswordLabel",
 302:                 Text = login.PasswordLabelText
 303:             };
 304:             passwordLabel.MergeStyle(login.LabelStyle);
 305:             container.Controls.Add(passwordLabel);
 306:  
 307:             password = new TextBox {
 308:                 ID = "Password",
 309:                 TextMode = TextBoxMode.Password
 310:             };
 311:             password.MergeStyle(login.TextBoxStyle);
 312:             container.Controls.Add(password);
 313:  
 314:             var passwordValidator = new RequiredFieldValidator {
 315:                 ControlToValidate = "Password",
 316:                 EnableViewState = false,
 317:                 ID = "PasswordRequired",
 318:                 Text = login.PasswordRequiredErrorMessage,
 319:                 ToolTip = login.PasswordRequiredErrorMessage,
 320:                 ValidationGroup = login.ID
 321:             };
 322:             passwordValidator.MergeStyle(login.ValidatorTextStyle);
 323:             container.Controls.Add(passwordValidator);
 324:         }
 325:  
 326:         /// <summary>
 327:         /// Creates the controls used to render the link to recover a
 328:         /// lost password.
 329:         /// </summary>
 330:         /// <param name="login">The <see cref="Login"/> control.</param>
 331:         /// <param name="hasCreateUserIconUrl">
 332:         /// True if the icon should be rendered.
 333:         /// </param>
 334:         /// <param name="hasCreateUserText">
 335:         /// True if the link text should be rendered.
 336:         /// </param>
 337:         /// <returns>The link.</returns>
 338:         private Control CreatePasswordRecoveryLink(Login login,
 339:             bool hasPasswordRecoveryIconUrl, bool hasPasswordRecoveryText) {
 340:             var listItem = new HtmlGenericControl("LI") {
 341:                 EnableViewState = false
 342:             };
 343:  
 344:             var link = new HyperLink() {
 345:                 EnableViewState = false,
 346:                 NavigateUrl = login.PasswordRecoveryUrl
 347:             };
 348:             link.MergeStyle(login.HyperLinkStyle);
 349:             listItem.Controls.Add(link);
 350:  
 351:             if (hasPasswordRecoveryIconUrl) {
 352:                 var image = new Image() {
 353:                     EnableViewState = false,
 354:                     ImageUrl = login.PasswordRecoveryIconUrl
 355:                 };
 356:                 link.Controls.Add(image);
 357:             }
 358:             if (hasPasswordRecoveryText) {
 359:                 var text = new Literal() {
 360:                     Text = login.PasswordRecoveryText
 361:                 };
 362:                 link.Controls.Add(text);
 363:             }
 364:  
 365:             return listItem;
 366:         }
 367:  
 368:         /// <summary>
 369:         /// Creates the controls that are used to render the title of the
 370:         /// <see cref="Login"/> control.
 371:         /// </summary>
 372:         /// <param name="login">The <see cref="Login"/> control.</param>
 373:         private void CreateTitleControl(Login login) {
 374:             var title = new HtmlGenericControl("H1") {
 375:                 EnableViewState = false,
 376:                 InnerText = login.TitleText
 377:             };
 378:             SetCssStyles(title, login.TitleTextStyle);
 379:             Controls.Add(title);
 380:         }
 381:  
 382:         /// <summary>
 383:         /// Creates the controls that are used to render the user information
 384:         /// field set for the <see cref="Login"/> control.
 385:         /// </summary>
 386:         /// <param name="login">The <see cref="Login"/> control.</param>
 387:         private void CreateUserInformationFieldSet(Login login) {
 388:             var userInformationFieldSet =
 389:                 new HtmlGenericControl("FIELDSET") {
 390:                     EnableViewState = false,
 391:                     ID = "UserInformationFieldSet"
 392:                 };
 393:             Controls.Add(userInformationFieldSet);
 394:  
 395:             var userInformationLegend =
 396:                 new HtmlGenericControl("LEGEND") {
 397:                     EnableViewState = false,
 398:                     InnerText = "User account information"
 399:                 };
 400:             userInformationFieldSet.Controls.Add(userInformationLegend);
 401:  
 402:             CreateUserNameControls(login, userInformationFieldSet);
 403:             CreatePasswordControls(login, userInformationFieldSet);
 404:         }
 405:  
 406:         /// <summary>
 407:         /// Creates the controls used to capture the user's account name.
 408:         /// </summary>
 409:         /// <param name="login">The <see cref="Login"/> control.</param>
 410:         /// <param name="container">
 411:         /// The container <see cref="Control"/> that the user name controls
 412:         /// will be added to.
 413:         /// </param>
 414:         private void CreateUserNameControls(Login login, Control container) {
 415:             var userNameLabel = new Label {
 416:                 AssociatedControlID = "UserName",
 417:                 EnableViewState = false,
 418:                 ID = "UserNameLabel",
 419:                 Text = login.UserNameLabelText
 420:             };
 421:             userNameLabel.MergeStyle(login.LabelStyle);
 422:             container.Controls.Add(userNameLabel);
 423:  
 424:             userName = new TextBox {
 425:                 ID = "UserName"
 426:             };
 427:             userName.MergeStyle(login.TextBoxStyle);
 428:             container.Controls.Add(userName);
 429:  
 430:             var userNameValidator = new RequiredFieldValidator {
 431:                 ControlToValidate = "UserName",
 432:                 EnableViewState = false,
 433:                 ID = "UserNameRequired",
 434:                 Text = login.UserNameRequiredErrorMessage,
 435:                 ToolTip = login.UserNameRequiredErrorMessage,
 436:                 ValidationGroup = login.ID
 437:             };
 438:             userNameValidator.MergeStyle(login.ValidatorTextStyle);
 439:             container.Controls.Add(userNameValidator);
 440:         }
 441:  
 442:         /// <summary>
 443:         /// Displays the &lt;DIV&gt; containing the failure text message
 444:         /// when the login fails.
 445:         /// </summary>
 446:         /// <param name="sender">The <see cref="Login"/> control.</param>
 447:         /// <param name="e">The event arguments.</param>
 448:         private void login_LoginError(object sender, EventArgs e) {
 449:             failureTextDiv.Visible = true;
 450:         }
 451:  
 452:         /// <summary>
 453:         /// Sets the CSS class name and CSS styles for a
 454:         /// <see cref="HtmlControl"/>.
 455:         /// </summary>
 456:         /// <param name="control">
 457:         /// The <see cref="HtmlControl"/> control.
 458:         /// </param>
 459:         /// <param name="style">
 460:         /// A <see cref="Style"/> object containing the CSS settings for
 461:         /// the control.
 462:         /// </param>
 463:         private void SetCssStyles(HtmlControl control, Style style) {
 464:             if (!style.IsEmpty) {
 465:                 if (!String.IsNullOrEmpty(style.CssClass)) {
 466:                     control.Attributes.Add("class", style.CssClass);
 467:                 }
 468:  
 469:                 var styles = style.GetStyleAttributes(Page);
 470:                 foreach (string key in styles.Keys) {
 471:                     control.Style.Add(key, styles[key]);
 472:                 }
 473:             }
 474:         }
 475:     }
 476: }

 

Was it a lot of work, probably.  But in the end I get my custom-designed Login control that matches my theme, while still taking full advantage of the settings and localization power of the standard ASP.NET login control.  And I don't have to re-invent a new Login control to match the output that I want to use.  Plus, I can reuse this control and its layout with other custom themes.

Technorati tags: , ,

LogOnTemplate.cs (18.23 kb)



Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Be the first to rate this post

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