Implementing a Composite Control The CompositeLogin Example


Implementing a Composite Control ”The CompositeLogin Example

We'll now implement a composite control ( CompositeLogin ) that generates a UI that enables a user to enter a name and a password to log on to a Web site. CompositeLogin is similar in functionality to the Login control we implemented in Chapter 9. The Login control rendered HTML to generate the UI, implemented IPostBackDataHandler to process postback data, and implemented IPostBackEventHandler to raise events. CompositeLogin implements its functionality by delegating to its child controls ”a pair of TextBox controls that correspond to the name and password fields, and a Button control to submit the form. CompositeLogin also contains two RequiredFieldValidator controls that are associated with the name and password fields.

Figure 12-1 shows the UI rendered by the CompositeLogin control. The asterisks next to the empty text boxes and the tool tip are rendered by the RequiredFieldValidator controls associated with the name and password fields. The page, shown later in this section, also has a ValidationSummary control, which renders a summary of the error messages at the bottom of the page.

Figure 12-1. The CompositeLoginTest.aspx page viewed in a browser. The page illustrates the UI generated by the CompositeLogin control.

graphics/f12hn01.jpg

Listing 12-1 contains the code for the CompositeLogin control. CompositeLogin exposes properties of its child controls as top-level properties and bubbles the Command event of its Button control as a top-level event.

Listing 12-1 CompositeLogin.cs
 usingSystem; usingSystem.ComponentModel; usingSystem.ComponentModel.Design; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; 
 namespaceMSPress.ServerControls{ [ DefaultEvent("Logon"), DefaultProperty("Name"), Designer(typeof(MSPress.ServerControls.Design.CompositeControlDesigner)) ] publicclassCompositeLogin:WebControl,INamingContainer{ privateButton_button; privateTextBox_nameTextBox; privateLabel_nameLabel; privateTextBox_passwordTextBox; privateLabel_passwordLabel; privateRequiredFieldValidator_nameValidator; privateRequiredFieldValidator_passwordValidator; privatestaticreadonlyobjectEventLogon=newobject(); #regionOverridenproperties publicoverrideControlCollectionControls{ get{ EnsureChildControls(); returnbase.Controls; } } #endregionOverridenproperties #regionPropertiesdelegatedtochildcontrols [ Bindable(true), Category("Appearance"), DefaultValue(""), Description("ThetexttodisplayontheButton") ] publicstringButtonText{ get{ EnsureChildControls(); return_button.Text; } set{ EnsureChildControls(); _button.Text=value; } } 
 [ Bindable(true), Category("Default"), DefaultValue(""), Description("Theusername") ] publicstringName{ get{ EnsureChildControls(); return_nameTextBox.Text; } set{ EnsureChildControls(); _nameTextBox.Text=value; } } [ Bindable(true), Category("Appearance"), DefaultValue(""), Description("ErrormessageofthevalidatorusedfortheName") ] publicstringNameErrorMessage{ get{ EnsureChildControls(); return_nameValidator.ErrorMessage; } set{ EnsureChildControls(); _nameValidator.ErrorMessage=value; _nameValidator.ToolTip=value; } } [ Bindable(true), Category("Apperance"), DefaultValue(""), Description("ThetextforthenameLabel") ] publicstringNameLabel{ get{ EnsureChildControls(); return_nameLabel.Text; } 
 set{ EnsureChildControls(); _nameLabel.Text=value; } } [ Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) ] publicstringPassword{ get{ EnsureChildControls(); return_passwordTextBox.Text; } } [ Bindable(true), Category("Appearance"), DefaultValue(""), Description("ErrormessageofthevalidatorusedforthePassword") ] publicstringPasswordErrorMessage{ get{ EnsureChildControls(); return_passwordValidator.ErrorMessage; } set{ EnsureChildControls(); _passwordValidator.ErrorMessage=value; _passwordValidator.ToolTip=value; } } [ Bindable(true), Category("Appearance"), DefaultValue(""), Description("ThetextforthepasswordLabel") ] 
 publicstringPasswordLabel{ get{ EnsureChildControls(); return_passwordLabel.Text; } set{ EnsureChildControls(); _passwordLabel.Text=value; } } #endregionPropertiesdelegatedtochildcontrols #regionEvents [ Category("Action"), Description("Raisedwhentheuserclickstheloginbutton") ] publiceventEventHandlerLogon{ add{ Events.AddHandler(EventLogon,value); } remove{ Events.RemoveHandler(EventLogon,value); } } protectedvirtualvoidOnLogon(EventArgse){ EventHandlerlogonHandler= (EventHandler)Events[EventLogon]; if(logonHandler!=null){ logonHandler(this,e); } } #endregion #regionEventbubbling //Theuseofeventbubblinginthisscenarioissomewhatcontrived; //wehaveimplementeditmainlyfordemonstrationpurposes. //Inthiscaseyoucouldinstead //raisetheLogoneventfromaneventhandlerwiredtothe //ClickeventortotheCommandeventoftheButtoncontrol. protectedoverride boolOnBubbleEvent(objectsource,EventArgse){ boolhandled=false; 
 if(eisCommandEventArgs){ CommandEventArgsce=(CommandEventArgs)e; if(ce.CommandName=="Logon"){ OnLogon(EventArgs.Empty); handled=true; } } returnhandled; } #endregionEventbubbling #regionOverridenmethods protectedoverridevoidCreateChildControls(){ Controls.Clear(); _nameLabel=newLabel(); _nameTextBox=newTextBox(); _nameTextBox.ID="nameTextBox"; _nameValidator=newRequiredFieldValidator(); _nameValidator.ID="validator1"; _nameValidator.ControlToValidate=_nameTextBox.ID; _nameValidator.Text="*"; _nameValidator.Display=ValidatorDisplay.Static; _passwordLabel=newLabel(); _passwordTextBox=newTextBox(); _passwordTextBox.TextMode=TextBoxMode.Password; _passwordTextBox.ID="passwordTextBox"; _passwordValidator=newRequiredFieldValidator(); _passwordValidator.ID="validator2"; _passwordValidator.ControlToValidate=_passwordTextBox.ID; _passwordValidator.Text="*"; _passwordValidator.Display=ValidatorDisplay.Static; _button=newButton(); _button.ID="button1"; _button.CommandName="Logon"; this.Controls.Add(_nameLabel); this.Controls.Add(_nameTextBox); this.Controls.Add(_nameValidator); this.Controls.Add(_passwordLabel); 
 this.Controls.Add(_passwordTextBox); this.Controls.Add(_passwordValidator); this.Controls.Add(_button); } protectedoverridevoidRender(HtmlTextWriterwriter){ AddAttributesToRender(writer); writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "1",false); writer.RenderBeginTag(HtmlTextWriterTag.Table); writer.RenderBeginTag(HtmlTextWriterTag.Tr); writer.RenderBeginTag(HtmlTextWriterTag.Td); _nameLabel.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderBeginTag(HtmlTextWriterTag.Td); _nameTextBox.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderBeginTag(HtmlTextWriterTag.Td); _nameValidator.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderEndTag();//Tr writer.RenderBeginTag(HtmlTextWriterTag.Tr); writer.RenderBeginTag(HtmlTextWriterTag.Td); _passwordLabel.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderBeginTag(HtmlTextWriterTag.Td); _passwordTextBox.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderBeginTag(HtmlTextWriterTag.Td); _passwordValidator.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderEndTag();//Tr writer.RenderBeginTag(HtmlTextWriterTag.Tr); writer.AddAttribute(HtmlTextWriterAttribute.Colspan,"2"); writer.AddAttribute(HtmlTextWriterAttribute.Align, "right"); writer.RenderBeginTag(HtmlTextWriterTag.Td); _button.RenderControl(writer); writer.RenderEndTag();//Td writer.RenderBeginTag(HtmlTextWriterTag.Td); writer.Write(" "); writer.RenderEndTag();//Td writer.RenderEndTag();//Tr 
 writer.RenderEndTag();//Table } #endregionOverridenmethods } } 

CompositeLogin performs the following tasks , which are common to composite controls:

  • Implements INamingContainer to provide a new naming scope for its child controls. This causes the page to assign unique identifiers to the child controls. On postback, the page uses the unique identifiers to find the child controls and route form data and the postback event to the children. In addition, because the parent control provides a new naming scope, each validator control can find the sibling it is associated with by simply using the ID property (instead of the UniqueID property) of the sibling.

  • Derives from WebControl so that it inherits the common set of style properties, which allows a page developer to customize the visual appearance of the control.

  • Overrides the accessor of the Controls property to invoke the EnsureChildControls method before returning the Controls collection of the base class. This ensures that child controls are created before any code tries to access them.

  • Exposes properties that are implemented by delegating to the properties of child controls. CompositeLogin first invokes EnsureChildControls in the property accessors of the delegated properties to make sure that child controls are created before it accesses them.

  • Instantiates, initializes, and adds child controls to its Controls collection by overriding the CreateChildControls method. If you want to attach event handlers to the events of child controls, you should do so in CreateChildControls . For optimization, CompositeLogin initializes each child control (sets properties) before adding the control to the Controls collection. Note that if the parent control is tracking changes to property values, the child control starts tracking changes to its property values as soon as it is added to the control tree, as we explained in Chapter 9. When you perform initialization before adding a control to the control tree, any property values you assign are considered initial values, which are not persisted into view state.

  • Clears its Controls collection before performing any other logic in CreateChildControls . This action ensures that multiple copies of child controls are not added to the Controls collection when your control ”or a control that derives from your control ”invokes the protected CreateChildControls method.

  • Handles the Command event that is bubbled by its contained Button control and exposes it as a top-level Logon event. We'll explain the event bubbling architecture in the "Event Bubbling" section later in this chapter.

  • Renders its child controls within a table by rendering an outer < table > tag and HTML for creating rows and cells (< tr > and < td > tags). CompositeLogin invokes the AddAttributesToRender method before rendering the < table > tag so that the top-level style properties are automatically rendered as attributes on the < table > tag. CompositeLogin does not invoke the RenderContents method because it does not want to utilize the default rendering of the control tree. Instead, the control renders its child controls by invoking RenderControl on each child control within table cells (< td > tags). In general, as shown in Listing 12-1, you should directly render HTML for formatting and layout in the Render phase, instead of creating and adding Literal ­Control instances that contain the required formatting markup in CreateChildControls . When you create LiteralControl s, such as new Literal ­Control("<td>") for formatting, you incur the additional expense of control creation and increase the size of the control tree. Furthermore, you violate the semantics of the control tree when you add to it controls that represent static text and are used only for formatting and layout.

  • Exposes properties (instead of using hard-coded strings in its code) to enable the page developer to provide text for the contained name and password labels and the login button. A reusable control should not render hard-coded strings or fixed strings in the user interface.

Base Class for Composite Controls

You might find it useful to define a CompositeControl base class from which to derive all your composite controls. The base class should implement INamingContainer and override the Controls collection:

 usingSystem; usingSystem.ComponentModel; usingSystem.ComponentModel.Design; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; [ Designer(typeof(CompositeControlDesigner)) ] publicabstractclassCompositeControl:     WebControl,INamingContainer{ publicoverrideControlCollectionControls{ get{ EnsureChildControls(); returnbase.Controls; } } } 

In addition, you should define a designer for composite controls and associate it with your composite controls via the DesignerAttribute . The code for the designer is included in the samples files and described in Chapter 15, "Design-Time Functionality."

Ideally, these base classes should be included in the ASP.NET Framework to serve as a guide to composite control implementers. However, they are not provided in the present version. They might be available in a future version of the .NET Framework as part of the class library.

Listing 12-2 contains the CompositeLoginTest.aspx page that uses the CompositeLogin control. You saw the page viewed in a browser in Figure 12-1.

Listing 12-2 CompositeLoginTest.aspx
 <%@PageLanguage="C#"%> <%@RegisterTagPrefix="msp"Namespace="MSPress.ServerControls" Assembly="MSPress.ServerControls"%> 
 <html> <head> <scriptrunat="server"> voidcompositeLogin1_Logon(objectsender,EventArgse) { if(AuthenticateUser()){ compositeLogin1.Visible=false; label1.Text="Hello,"+compositeLogin1.Name+ ".Youareloggedin."; } else{ label1.Text="Loginfailed."+ "Enteryourusernameandpassword."; } } boolAuthenticateUser(){ //Performlogictoauthenticateuser.We'll //simplyreturntrueforthisdemo. returntrue; } </script> </head> <body> <formrunat="server"> <msp:CompositeLoginid="compositeLogin1"runat="server" OnLogon="compositeLogin1_Logon"ButtonText="Submit" NameLabel="Name:"PasswordLabel="Password:" BackColor="Silver"BorderColor="Gray"BorderWidth="1px" NameErrorMessage="Youmustenteryourname." PasswordErrorMessage="Youmustenteryourpassword."/> <br> <asp:Labelid="label1"runat="server"/> <asp:ValidationSummaryrunat="server"id="ValidationSummary1"/> </form> </body> </html> 

Listing 12-3 shows the HTML rendered by the CompositeLogin control in the CompositeLoginTest.aspx page. The HTML rendered by the control includes the tags used to create the HTML table layout. The HTML within the <td> tags is rendered by the child controls that CompositeLogin contains.

Listing 12-3 HTML rendered by the CompositeLogin control within the CompositeLoginTest.aspx page
 <tableid="compositeLogin1"cellpadding="1" style="background-color:Silver;border-color:Gray;border-width:1px; order-style:solid;"> <tr> <td><span>Name:</span></td> <td><inputname="compositeLogin1:nameTextBox"type="text" id="compositeLogin1_nameTextBox"/></td> <td><spanid="compositeLogin1_validator1" title="Youmustenteryourname." controltovalidate="compositeLogin1_nameTextBox" errormessage="Youmustenteryourname." evaluationfunction="RequiredFieldValidatorEvaluateIsValid" initialvalue=""style="color:Red;visibility:hidden;">*</span> </td> </tr> <tr> <td><span>Password:</span></td> <td><inputname="compositeLogin1:passwordTextBox"type="password" id="compositeLogin1_passwordTextBox"/></td> <td><spanid="compositeLogin1_validator2" title="Youmustenteryourpassword." controltovalidate="compositeLogin1_passwordTextBox" errormessage="Youmustenteryourpassword." evaluationfunction="RequiredFieldValidatorEvaluateIsValid" initialvalue=""style="color:Red;visibility:hidden;">*</span> </td> </tr> <tr> <tdcolspan="2"align="right"><inputtype="submit" name="compositeLogin1:button1"value="Submit" onclick="if(typeof(Page_ClientValidate)=='function') Page_ClientValidate();" language="javascript"id="compositeLogin1_button1"/></td> <td>&nbsp; </td> </tr> </table> 

The name and id attributes in the HTML tag rendered by a control correspond to the UniqueID and ClientID property values assigned to the control by the page. When a composite control implements INamingContainer , the page generates UniqueID s for each child control by concatenating the UniqueID of the NamingContainer to the ID of the child by using the colon character as a separator. For example, the value of the UniqueID property of the first TextBox instance is "compositeLogin1:nameTextBox" . The NamingContainer provides a new naming scope so that the UniqueID of the child control does not conflict with that of another control on the page. The UniqueID thus represents a hierarchically qualified unique identifier that specifies the exact location of a control within the control tree.

The ClientID is also unique on the page and is the client script “friendly version of the UniqueID . For example, the value of the ClientID property of the first TextBox instance in CompositeLoginTest.aspx is "compositeLogin1_nameTextBox" . The ClientID uses the underscore as the separator character because the colon character is not allowed in variable names that are accessed in JavaScript. To ensure that the ClientID does not contain the colon character, the page framework does not allow the colon as a valid character in the string assigned to the ID property of a control. If you use the colon character in the string that is assigned to the ID property of a control, you will find that the page parser returns an error message.

The page framework uses the colon character as the separator character for generating the UniqueID because it cannot be used inside a valid ID property. This enables the FindControl method to use the colon character to recursively split the UniqueID string, navigate the control tree, and locate a control.

APIs Related to Composite Controls

As a handy reference, this section will list the various members that the Control class exposes for working with child controls. Unless otherwise noted, you do not have to override the implementation provided by the base class:

  • The public Controls property of type ControlCollection represents the child controls. You can utilize the methods of ControlCollection , such as Add , Remove , and Clear , to work with the Controls collection. You must override the accessor of the Controls property, as we showed in the CompositeLogin control in the previous section, to ensure that child controls are created before any code tries to access them.

  • The public NamingContainer property returns the first control upward in the control hierarchy that implements the INamingContainer interface. A composite control that implements INamingContainer is the naming container of its child controls.

  • The protected CreateChildControls method is where you perform the logic related to creating child controls, as we described earlier in this chapter.

  • The protected EnsureChildControls method checks the ChildControlsCreated property. If the value of that property is false , EnsureChildControls invokes the CreateChildControls method. You should always invoke EnsureChildControls before accessing a child control to ensure that child controls have been created.

  • The public FindControl method takes in a string that represents the ID or the UniqueID of a control and recursively searches the control tree to locate the control. FindControl invokes EnsureChildControls before it starts searching. The automatically generated UniqueID of a control provides the exact location of a control in the control tree, as we showed at the end of the previous section. If the search is successful, FindControl returns the control; otherwise, the method returns null.

  • The protected ChildControlsCreated property indicates whether the CreateChildControls method was invoked. EnsureChildControls checks this property before invoking the CreateChildControls method.

  • The public HasControls method returns a Boolean value that indicates whether the Controls collection contains controls.

  • The protected CreateControlCollection method is invoked by the getter of the Controls property when the Controls collection is null. The default implementation of CreateControlCollection creates and returns an instance of the ControlCollection class.

In addition, Control provides two members that are used for state management of child controls:

  • The protected HasChildViewState property indicates whether the control has view state corresponding to child controls.

  • The protected ClearChildViewState method clears any view state corresponding to child controls. This method is used when a composite control needs to rebuild a new control hierarchy upon postback that does not correspond to the earlier control hierarchy it created.

View State and Child Controls

To implement a composite control, you do not need to know how the view state mechanism works in child controls. However, if you are curious about view state in composite controls, this section will provide a behind-the-scenes look. In Chapter 9, we described the phases in a control's life cycle where it performs state management, and in Chapter 10, "Complex Properties and State Management," we showed how you can implement custom state management. In this section, we will not repeat the background information we provided earlier. Instead, we will focus on how state management is applied to child controls.

The Control class has built-in functionality to track, save, and restore the state of its child controls. In the Begin Tracking View State phase, Control sequentially invokes the TrackViewState method on the controls in its Controls collection to start tracking state in the child controls. In addition, if a child control is added to the Controls collection after the parent has started tracking state, the child control's TrackViewState method is invoked as soon as it is added to the control tree.

In the Save View State phase, Control first invokes its SaveViewState method. By default, this method invokes SaveViewState on its ViewState dictionary and saves the returned object as the first part of a control's view state. Control next invokes SaveViewState on each child control in the control tree. If the state that a child returns is non-null, Control saves the index of the child control and the corresponding view state in two ArrayLists , which correspond to two additional parts of the view state. The first ArrayList holds the indices (in the Controls collection) of child controls that have non-null state to serialize, and the second ArrayList holds the saved states of those children. Finally, at the end of the Save View State phase, Control returns its three-part view state.

In the Load View State phase, Control performs the inverse of the operations it performed in the Save View State phase. The state that the page hands to Control in the Load View State phase is the same state that the control saved at the end of the previous request. Control loads the first part of the saved state by invoking its LoadViewState method, which in turn loads the saved state into its ViewState dictionary. Control then accesses its Controls collection to load the remaining state into child controls. The remaining state consists of the Array ­List s that represent the indices and saved states of child controls. Control uses the indices and the states to load state into each child control that saved its state at the end of the previous request. This completes the state restoration in a control and in its child controls. If child controls have not been created in the Load View State phase, Control stores the state of its child controls for later use. Control then loads state into the child controls when they are created and added to the control tree. Note that view state tracking starts before the Load View State phase. Therefore, any properties that a control restores by using view state automatically are marked dirty and therefore are resaved in view state during the Save View State phase. On postback, these properties are reloaded during the Load View State phase. The view state mechanism thus perpetuates itself over subsequent requests .

The steps that we just described for each state management phase are recursively executed within the control tree. The view state of a control thus represents the collective view state of the entire control hierarchy under that control.

Notice that Control uses the index of a child control in the Controls collection to identify the saved view state of a child control. Control does not use the type or the ID of the child control. This leads to better performance because it reduces the size of the view state. However, this implies that the view state mechanism can work only if the control tree is re-created on postback in exactly the same order in which it was saved at the end of the previous request. You can maintain that order ”for the controls you create in CreateChildControls ” by overriding the Controls property. We showed how in the CompositeLogin example earlier in the chapter:

 publicoverrideControlCollectionControls{ get{ EnsureChildControls(); returnbase.Controls; } } 

This code causes the child controls that you created in CreateChildControls to be added to the control tree before other controls (if any) are added by user code. The child controls you created in CreateChildControls are thus re-created on postback in the same order in which they were saved. This allows the page framework to restore their state by using view state.

Event Bubbling

The page framework provides an event bubbling architecture that allows a control to bubble an event up the control hierarchy. A bubbled event can be handled at the location where it is raised or at another, more convenient location higher up in the control tree. A composite control can use this feature to expose events bubbled by its child controls as top-level events. For example, the DataList control exposes the Command event of a Button control contained in its ItemTemplate as a top-level ItemCommand event. While command events (events whose event data class derives from CommandEventArgs ) are the only events that are bubbled by the built-in ASP.NET controls, you can implement other events that initiate bubbling, as we will show at the end of this section.

Event bubbling is enabled by the OnBubbleEvent and RaiseBubbleEvent methods, which are defined in the Control class like this:

 protectedvirtualboolOnBubbleEvent(objectsource,EventArgsargs){ returnfalse; } protectedvoidRaiseBubbleEvent(objectsource,EventArgsargs){ ControlcurrentTarget=_parent; while(currentTarget!=null){ if(currentTarget.OnBubbleEvent(source,args)){ return; } currentTarget=currentTarget.Parent; } } 

By default, an event that initiates bubbling is automatically bubbled up through the control hierarchy, as you can see from the definition of the RaiseBubbleEvent method and the default implementation of OnBubbleEvent .

To handle a bubbled event, you override the OnBubbleEvent method. A composite control often contains more than one child control that bubbles an event. An event can also be bubbled from farther down in the control hierarchy (from a child of a child). You can use the arguments passed into the OnBubbleEvent method to determine which events to handle. After you have handled the event, return true from the OnBubbleEvent method if you want to stop the event from bubbling further.

One way to handle a bubbled event is to raise a new event in response to the bubbled event. This enables a page developer to handle a bubbled event as a top-level event of your control. To expose a bubbled event as a top-level event from your control, define an event in your control and raise that event from the OnBubbleEvent method. The following fragment shows how the CompositeLogin control captures the Command event of the Button child control and raises its own Logon event:

 protectedoverrideboolOnBubbleEvent(objectsource,EventArgse){ boolhandled=false; if(eisCommandEventArgs){ CommandEventArgsce=(CommandEventArgs)e; if(ce.CommandName=="Logon"){ OnLogon(EventArgs.Empty); handled=true; } } returnhandled; } 

Although we provided this example as a simple case of event bubbling, it is somewhat contrived because it is not essential to use event bubbling to raise the Logon event. You could instead attach an event handler to the Click event of the Button control and raise the Logon event from the handler, as we show in the StyledCompositeLogin control in the sample files. Bubbling is more useful when your control does not have a direct reference to the control that raises the event. We will show a more realistic example of event bubbling when we examine the ListView control in Chapter 20, "Data-Bound Templated Controls."

Finally, let's take a look at how you can implement an event that initiates bubbling. Define your event as always, but invoke the RaiseBubbleEvent method from the On< EventName > method that raises your event. The following example shows how the OnCommand method that raises the Command event of the Button control initiates the bubbling of this event:

 protectedvirtualvoidOnCommand(CommandEventArgse){ CommandEventHandlerhandler= (CommandEventHandler)Events[EventCommand]; if(handler!=null){ handler(this,e); } //BubbletheCommandeventupthecontrolhierarchy. RaiseBubbleEvent(this,e); } 

Styles in Composite Controls ”The StyledCompositeLogin Example

To enable the page developer to customize the appearance of child controls, you can expose styles and apply them to child controls. Top-level styles for child controls enable the page developer to easily access and modify the style properties of a child control. When you do not provide styles, the page developer has to use the error-prone technique of indexing the Controls collection to access child controls and then modifying their styles.

One way to enable page developers to access the style properties of child controls is to implement multiple delegated properties such as ButtonForeColor , ButtonBackColor , LabelForeColor , and LabelBackColor for each child control. However, this approach does not scale as the number of child controls increases because each child Web control defines numerous style properties. The recommended technique, which we will demonstrate in this section, is to implement properties of type Style that correspond to each child control (or each type of child control), such as ButtonStyle and LabelStyle . These top-level style properties are equivalent to the ControlStyle property of your control.

We will now implement a StyledCompositeLogin control, which is similar to the CompositeLogin control we developed earlier in the chapter but exposes styles for child controls. In the designer, the style properties are associated with an expand/collapse UI in the property browser, which allows the page developer to set individual style properties, as Figure 12-2 shows.

Figure 12-2. Styles of StyledCompositeLogin displayed in the property browser of Microsoft Visual Studio .NET

graphics/f12hn02.jpg

Listing 12-4 shows the code for the StyledCompositeLogin control that contains the style implementation. The code that is not shown was described in the CompositeLogin sample earlier in the chapter and is included in the sample files. StyledCompositeLogin exposes the ButtonStyle , LabelStyle , and TextBoxStyle properties; applies these styles to its child controls; and performs state management for the style properties. Because styles are complex properties, they require custom state management, as we described in Chapter 10, "Complex Properties and State Management" and Chapter 11, "Styles in Controls."

Listing 12-4 StyledCompositeLogin.cs
 usingSystem; usingSystem.ComponentModel; usingSystem.ComponentModel.Design; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; namespaceMSPress.ServerControls{ [ DefaultEvent("Logon"), DefaultProperty("Name"), Designer(typeof(MSPress.ServerControls.Design.CompositeControlDesigner)) ] publicclassStyledCompositeLogin:WebControl,INamingContainer{ #regionCompositecontrolimplementation  #endregionCompositecontrolimplementation privateStyle_buttonStyle; privateStyle_labelStyle; privateStyle_textBoxStyle; #regionStylesforchildcontrols [ Category("Style"), Description("Thestyletobeappliedtothebutton"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), NotifyParentProperty(true), PersistenceMode(PersistenceMode.InnerProperty), ] publicvirtualStyleButtonStyle{ get{ if(_buttonStyle==null){ _buttonStyle=newStyle(); if(IsTrackingViewState) ((IStateManager)_buttonStyle).TrackViewState(); } return_buttonStyle; } } 
 [ Category("Style"), Description("Thestyletobeappliedtoalabel"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), NotifyParentProperty(true), PersistenceMode(PersistenceMode.InnerProperty), ] publicvirtualStyleLabelStyle{ get{ if(_labelStyle==null){ _labelStyle=newStyle(); if(IsTrackingViewState) ((IStateManager)_labelStyle).TrackViewState(); } return_labelStyle; } } [ Category("Style"), Description("Thestyletobeappliedtoatextbox"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content), NotifyParentProperty(true), PersistenceMode(PersistenceMode.InnerProperty), ] publicvirtualStyleTextBoxStyle{ get{ if(_textBoxStyle==null){ _textBoxStyle=newStyle(); if(IsTrackingViewState) ((IStateManager)_textBoxStyle).TrackViewState(); } return_textBoxStyle; } } #endregion protectedoverridevoidRender(HtmlTextWriterwriter){ AddAttributesToRender(writer); writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "1",false); writer.RenderBeginTag(HtmlTextWriterTag.Table); 
 //Childcontrolsarealwayscreatedbythistime. //Therefore,wedonothavetocheckfortheir //existencebeforeaccessingthem. //WeapplystylestothechildcontrolsintheRendermethod //sothatstylepropertychangesarenotpersistedinthe //viewstateofthechildcontrols. if(_buttonStyle!=null){ _button.ApplyStyle(ButtonStyle); } if(_textBoxStyle!=null){ _nameTextBox.ApplyStyle(TextBoxStyle); _passwordTextBox.ApplyStyle(TextBoxStyle); } if(_labelStyle!=null){ _nameLabel.ApplyStyle(LabelStyle); _passwordLabel.ApplyStyle(LabelStyle); } //Renderchildcontrolswithintablecells.  writer.RenderEndTag();//Table } #regionCustomstatemanagementforstylesofchildcontrols protectedoverridevoidLoadViewState(objectsavedState){ if(savedState==null){ //AlwaysinvokeLoadViewStateonthebaseclass, //evenifthereisnosavedstate,becausethebase //classmighthaveimplementedsomelogicthatneeds //tobeexecutedevenifthereisnostatetorestore. base.LoadViewState(null); return; } if(savedState!=null){ object[]myState=(object[])savedState; if(myState.Length!=4){ thrownewArgumentException("Invalidviewstate"); } base.LoadViewState(myState[0]); 
 if(myState[1]!=null) ((IStateManager)ButtonStyle).LoadViewState(myState[1]);if(myState[2]!=null) ((IStateManager)LabelStyle).LoadViewState(myState[2]); if(myState[3]!=null) ((IStateManager)TextBoxStyle).LoadViewState(myState[3]); } } protectedoverrideobjectSaveViewState(){ //Customizedstatemanagementtosavethestateofstyles. object[]myState=newobject[4]; myState[0]=base.SaveViewState(); myState[1]=(_buttonStyle!=null)? ((IStateManager)_buttonStyle).SaveViewState():null; myState[2]=(_labelStyle!=null)? ((IStateManager)_labelStyle).SaveViewState():null; myState[3]=(_textBoxStyle!=null)? ((IStateManager)_textBoxStyle).SaveViewState():null; for(inti=0;i<4;i++){ if(myState[i]!=null){ returnmyState; } } //Ifthereisnosavedstate,itismoreperformantto //returnnullthantoreturnanarrayofnullvalues. returnnull; } protectedoverridevoidTrackViewState(){ //Customizedstatemanagementtotrackthestate //ofstyles. base.TrackViewState(); if(_buttonStyle!=null) ((IStateManager)_buttonStyle).TrackViewState(); if(_labelStyle!=null) ((IStateManager)_labelStyle).TrackViewState(); 
 if(_textBoxStyle!=null) ((IStateManager)_textBoxStyle).TrackViewState(); } #endregionCustomstatemanagementforstylesofchildcontrols } } 

The definition of the ButtonStyle , LabelStyle , and TextBoxStyle properties shows that when creating a style that is associated with a child control you must use the parameterless constructor of Style . You should not pass the control's ViewState into the constructor of the Style class because a control shares its ViewState only with its own ControlStyle property, as we described in Chapter 11. Style properties of child controls use their own internal ViewState instance, which is independent of the parent control's ViewState .

StyledCompositeLogin applies styles to child controls at the beginning of the Render phase via calls to the ApplyStyle method. When styles are applied to child controls in the Render phase, the resulting property changes do not contribute to the view state of the child controls. (Remember, the Render phase occurs after the Save View State phase.) Because the parent composite control performs state management for styles, as we will describe next, you should not duplicate state information for styles in the view state of the child controls.

Styles are complex properties and require custom state management, as we described in Chapter 10. The code at the end of the StyledCompositeLogin example shows you how to implement state management for the styles of child controls. Style implements IStateManager and manages its own state, as we described in Chapter 11. You essentially delegate state management to each style instance that you create. In the TrackViewState method of your control, you must invoke TrackViewState on each typed style that you define. In SaveViewState , you must collect the state from each typed style and return an array of those states in addition to the state that your base class saves. In LoadViewState , you should load the first part of the saved state into the base class and the remaining state into the typed styles of the child controls.

Listing 12-5 shows a fragment from the StyledCompositeLoginTest.aspx page that uses the StyledCompositeLogin control. Notice that child style properties are persisted as inner (nested) properties.

Listing 12-5 Fragment from the StyledCompositeLoginTest.aspx page that shows nested style properties
 <msp:StyledCompositeLoginid="styledCompositeLogin1"runat="server" OnLogon="styledCompositeLogin1_Logon"ButtonText="Submit" NameLabel="Name:"PasswordLabel="Password:"BackColor="Silver" BorderColor="Gray"BorderWidth="1px" NameErrorMessage="Youmustenteryourname." PasswordErrorMessage="Youmustenteryourpassword."> <LabelStyleFont-Size="Smaller" Font-Names="Verdana"Font-Bold="True"/> <ButtonStyleBorderStyle="Outset"Font-Size="Smaller" Font-Names="Verdana"BorderWidth="2px" ForeColor="Black"BorderColor="Gray" BackColor="#E0E0E0"/> <TextBoxStyleFont-Names="Arial"/> </msp:StyledCompositeLogin> 

Templated Controls Overview

A templated control enables page developers to specify some or all of the UI it renders via templates . Templates are fragments of page syntax that can include server controls along with static HTML and other literal text. Templated controls offer significant customization capabilities and are often referred to as lookless controls because they do not render a predetermined user interface.

It is important to understand the difference between styles and templates ”two distinct mechanisms that a control offers to page developers for customizing the UI it renders. Styles allow page developers to customize the visual appearance of the rendered UI, while templates allow page developers to customize the content of the rendered UI. For example, styles allow a page developer to modify the BackColor or ForeColor of text that a control might render. On the other hand, a template allows a page developer to tell the control which element to render: a check box, a table, or some other content.

We will assume that you are familiar with the standard ASP.NET templated controls, such as Repeater and DataList , and will focus on showing you how to implement a templated control. The main functionality of a templated control is expressed via one or more properties of the System.Web.UI.ITemplate type. (We will describe the ITemplate interface later in this section.) The user typically specifies a template property by using declarative syntax, as shown in the following example, which specifies the ItemTemplate property for the DataList control:

 <asp:DataListrunat="server"> <ItemTemplate> <asp:Labelid="label"runat="server" Text='<%#Container.DataItem%>'/> &nbsp;&nbsp; <asp:Buttonbutton="button"runat="server" id="selectButton"CommandName="Select" Text="Select"ForeColor="Blue"/> </ItemTemplate> </asp:DataList> 

The page parser parses the text within the template tags and generates a parse tree that represents the content of the template, just as it would when parsing an entire page. The parser uses the parse tree (which consists of System.Web.UI . ControlBuilder objects) to create an instance of an ITemplate type. The ITemplate instance is capable of creating within a given container control the control hierarchy that represents the content of the template. Here is the definition of the ITemplate interface:

 publicinterfaceITemplate{ //Iterativelypopulatesaprovidedcontrol //withasubhierarchyofchildcontrols //representedbythetemplate. voidInstantiateIn(Controlcontainer); } 

The parser assigns the ITemplate instance to the corresponding ITemplate property of your control. Your control can call the InstantiateIn method of the template one or more times when building its control hierarchy. Each time its InstantiateIn method is invoked, the template creates a copy of the control tree that is represented by the template's content. You can define a template in code by implementing the ITemplate interface manually, as we will describe at the end of the next section.

Templates are often used in the context of data-bound controls. This chapter focuses on core concepts related to implementing controls that use templates; Chapter 20 provides a sample of a data-bound control that uses templates.

Implementing a Templated Control ”The ContactInfo Example

We'll now demonstrate how to implement a templated control by implementing the ContactInfo control. ContactInfo resembles a Web business card and provides a customizable UI through a template property that can be used to provide contact information. If the page developer does not define a template, the control generates a default UI. Figure 12-3 shows a page that uses the ContactInfo control.

Figure 12-3. The ContactInfoTest.aspx page that shows the UI rendered by using a template in the ContactInfo control

graphics/f12hn03.jpg

Listing 12-6 contains the ContactInfoTest.aspx page that uses the ContactInfo control. The first instance of the control contains a template that is specified within the <ContactTemplate> tags. The second instance uses the default template that is provided by the control itself.

Listing 12-6 ContactInfoTest.aspx
 <%@PageLanguage="C#"%> <%@RegisterTagPrefix="msp"Namespace="MSPress.ServerControls" Assembly="MSPress.ServerControls"%> <html> <head> <scriptrunat="server"> voidPage_Load(objectsender,EventArgse){ contactInfo1.DataBind(); contactInfo2.DataBind(); } </script> </head> <body> <formrunat="server"> ThisUIiscreatedbytheContactInfocontrolusinga templatespecifiedonthepage: 
 <p> <msp:ContactInfoid="contactInfo1"runat="server" Caption="NetGeek"Info="NetGeek@DevCepts.com" BorderWidth="2px"BorderColor="Gainsboro"> <ContactTemplate> <table> <tr> <td> <imgsrc="Logo.gif"align="middle"/> &nbsp; <asp:LabelText="<%#Container.Caption%>" runat="server"id="Label1" Font-Names="Verdana"Font-Size="12"/> </td> </tr> <tr> <td> <asp:HyperLinkrunat="server" Text="<%#Container.Info%>" NavigateUrl='<%#String.Format("mailto:{0}", Container.Info)%>'/> </td> </tr> </table> </ContactTemplate> </msp:ContactInfo> </p> ThisUIiscreatedbytheContactInfocontrolusingits defaulttemplate: <p> <msp:ContactInfoid="contactInfo2"runat="server" Caption="NetGeek"Info="NetGeek@DevCepts.com"/> </p> </form> </body> </html> 

Listing 12-7 contains the code for the ContactInfo control. ContactInfo exposes two properties, Caption and Info , which allow a user to specify a name and associated contact information. The main items of interest in the ContactInfo control are the ContactTemplate property and the implementation of the CreateChildControls method. The designer associated with the ContactInfo control via the DesignerAttribute metadata attribute is described in Chapter 15.

Listing 12-7 ContactInfo.cs
 usingSystem; usingSystem.ComponentModel; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; namespaceMSPress.ServerControls{ [ DefaultProperty("Caption"), Designer(typeof(MSPress.ServerControls.Design.ContactInfoDesigner)) ] publicclassContactInfo:WebControl,INamingContainer{ privateITemplate_contactTemplate; privateContactPanel_contactPanel; [ Bindable(true), Category("Behavior"), DefaultValue(""), Description("Thecaptionforcontactinformation") ] publicvirtualstringCaption{ get{ Stringcaption=(string)ViewState["Caption"]; return((caption==null)?String.Empty:caption); } set{ ViewState["Caption"]=value; } } [ Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden) ] publicContactPanelContactPanel{ get{ EnsureChildControls(); return_contactPanel; } } 
 [ Browsable(false), DefaultValue(null), Description("Thetemplateproperty"), PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(ContactPanel)) ] publicvirtualITemplateContactTemplate{ get{ return_contactTemplate; } set{ _contactTemplate=value; } } publicoverrideControlCollectionControls{ get{ EnsureChildControls(); returnbase.Controls; } } [ Bindable(true), Category("Behavior"), DefaultValue(""), Description("Thecontactinformation") ] publicvirtualstringInfo{ get{ Stringinfo=(string)ViewState["Info"]; return((info==null)?String.Empty:info); } set{ ViewState["Info"]=value; } } protectedoverridevoidCreateChildControls(){ Controls.Clear(); _contactPanel=newContactPanel(Caption,Info); 
 ITemplatetemplate=ContactTemplate; if(template==null){ template=newDefaultContactTemplate(); } template.InstantiateIn(_contactPanel); Controls.Add(_contactPanel); } publicoverridevoidDataBind(){ CreateChildControls(); ChildControlsCreated=true; base.DataBind(); } #regionImplementationoftheDefaultContactTemplateclass  #endregion } } 

As with any other composite control, ContactInfo implements the INamingContainer interface and overrides the accessor of the Controls property to invoke the EnsureChildControls method.

The ContactTemplate property demonstrates the following main characteristics of an ITemplate property. It is

  • Implemented as a read-write property. The template value is held in a member variable.

  • Marked with the TemplateContainerAttribute metadata attribute. The argument of this attribute specifies the type of the control, which is the naming container for the child controls in the template. The page parser uses the type specified in the TemplateContainerAttribute to infer the exact type of the Container identifier when parsing data-binding syntax such as <%# Container.Info %> within the template content. When the control instance passed into the InstantiateIn method of the ITemplate property implements the INamingContainer interface, you specify its type in the TemplateContainerAttribute . When the control passed into InstantiateIn does not implement the INamingContainer interface, in the TemplateContainerAttribute you specify the type of the first naming container upward in the control hierarchy.

  • Marked with the PersistenceModeAttribute metadata attribute. The argument passed into this attribute ( PersistenceMode.InnerProperty ) indicates that the designer should persist the template as an inner property within the control's tag in the .aspx file.

  • Marked with the BrowsableAttribute metadata attribute. The argument passed into this attribute ( false ) indicates that the property browser should not display an ITemplate property. ITemplate properties are edited on the design surface directly rather than in the property browser.

If your control exposes more than one ITemplate property, you might define additional containers for the templates.

In Chapter 15, "Design-Time Functionality," we'll show you how to implement an associated designer for a templated control that enables editing of its ITemplate properties. A visual designer allows the page developer to edit the contents of a template property by dragging controls in a WYSIWIG fashion onto the design surface.

ContactInfo demonstrates the following steps, which you must perform in the CreateChildControls method:

  1. Create an instance of the container class for the template property ( ContactPanel in our example). The container class typically implements INamingContainer and provides a new naming scope for ID s of controls defined within the template.

  2. Pass the template container instance into the InstantiateIn method of the template property ( ContactTemplate in our example). As we described in the previous section, the InstantiateIn method adds the child controls represented by the template to the container instance.

  3. Add the template container instance to the Controls collection of your control.

In a data-bound templated control, you must repeat these steps to create multiple instances of the template in different container instances, as we will demonstrate in Chapter 20.

ContactInfo has the following additional features:

  • Does not implement rendering logic because the default implementation of the RenderContents method in the Control class renders the child controls.

  • Exposes the template container instance as a (read-only) ContactPanel property. This enables a page developer to invoke FindControl on the container and locate child controls in the template. The ContactPanel property is analogous to each RepeaterItem within the Items collection of the Repeater or to each DataListItem within the Items collection of the DataList control.

  • Provides a default template by implementing an ITemplate type ( DefaultContactTemplate ). If the ContactTemplate property has not been set by the page developer, ContactInfo uses the DefaultContactTemplate type as the default ITemplate implementation. We'll describe the DefaultContactTemplate class later in this section.

Because ContactInfo derives from WebControl , it does not need the ParseChildren(true) metadata attribute; this attribute is already applied to the WebControl class. However, if you implement a templated control that derives from Control , you must apply the ParseChildren(true) metadata attribute to tell the parser to interpret nested content as properties rather than as child controls. We will describe control parsing in more detail in the next section. You must also apply the related design-time PersistChildren(false) attribute when your templated control derives from Control .

Listing 12-8 shows the code for the ContactPanel class that acts as the container for the ContactTemplate property. The ToolboxItem(false) metadata attribute applied to ContactPanel is a design-time attribute that tells the designer not to display this control in the toolbox. When a page developer customizes the toolbox to add components from an assembly, by default the designer lists every class in that assembly that derives from Component . We do not want to display ContactPanel in the toolbox because it is a helper control used internally by the ContactInfo control.

Listing 12-8 ContactPanel.cs
 usingSystem; usingSystem.ComponentModel; usingSystem.Web.UI; usingSystem.Web.UI.WebControls; namespaceMSPress.ServerControls{ [ToolboxItem(false)] publicclassContactPanel:Panel,INamingContainer{ privatestring_caption; privatestring_info; 
 internalContactPanel(stringcaption,stringinfo){ _caption=caption; _info=info; } publicstringCaption{ get{ return_caption; } } publicstringInfo{ get{ return_info; } } } } 

The template container must derive from Control ; you can derive from any control that will serve as an appropriate container. For example, the RepeaterItem in the Repeater control derives from Control , and the DataListItem in the DataList control derives from WebControl . For variety, we derive the template container from the Panel class. While we have not defined a style for the container, you can create a style for the template container and expose it as a top-level style from your parent control, that is, the control that exposes template properties.

If you repeatedly instantiate the template in your control, your template container should implement the INamingContainer interface so that the child controls represented by the template are created within a new naming scope. In our example, ContactPanel does not need to implement INamingContainer because it is not repeatedly instantiated . However, we have implemented this interface as a reminder that you must implement it in the more common case of a templated data-bound control.

If you want to support data binding within the template, your template container should expose one or more properties that represent data to bind to. For example, in the Repeater control, the RepeaterItem exposes a DataItem property that the page developer binds to the property of a control specified within the template. To illustrate this feature, we have exposed the Caption and Info properties from the ContactPanel template container, which allow the template content to contain data-binding expressions such as Container.Caption and Container.Info .

Listing 12-9 contains the code for the DefaultContactTemplate class, which serves as the default template when the ContactTemplate property has not been assigned a template by the page developer. We implemented DefaultContactTemplate as a private inner class in the ContactInfo control because it is not meaningful outside the ContactInfo control. A default template is not essential; you can render the default UI by other means. We have provided a default template in this example mainly to illustrate how to implement the ITemplate interface and thus to create dynamic templates.

Listing 12-9 The default template for the ContactInfo control, implemented as a private inner class
 privatesealedclassDefaultContactTemplate:ITemplate{ voidITemplate.InstantiateIn(Controlcontainer){ LabelcaptionLabel=newLabel(); captionLabel.DataBinding+= newEventHandler(CaptionLabel_DataBinding); LiteralControllinebreak=newLiteralControl("<br>"); LabelinfoLabel=newLabel(); infoLabel.DataBinding+= newEventHandler(InfoLabel_DataBinding); container.Controls.Add(captionLabel); container.Controls.Add(lt); container.Controls.Add(infoLabel); } privatestaticvoidCaptionLabel_DataBinding(objectsender, EventArgse){ ControltargetControl=(Control)sender; ContactPanelcontainer= (ContactPanel)(targetControl.NamingContainer); ((Label)targetControl).Text=container.Caption; } privatestaticvoidInfoLabel_DataBinding(objectsender, EventArgse){ ControltargetControl=(Control)sender; ContactPanelcontainer= (ContactPanel)(targetControl.NamingContainer); ((Label)targetControl).Text=container.Info; } } 

The DefaultContactTemplate class implements the ITemplate interface, which has only one method, InstantiateIn , which we described in the previous section. In this method, DefaultContactTemplate creates two Label controls that are separated by a LiteralControl that represents a line break. The class then adds the two Label controls to the Controls collection of the control that is passed into the InstantiateIn method.

If you want data-binding functionality in a dynamic template, you must provide event handlers that you wire up to the DataBinding events of child controls defined in the template. In the event handlers, you assign values to properties of the child controls by using data (values of properties) present on the controls' NamingContainer . For example, in the event handlers that we have defined, we assign the values of the Caption and Info properties of the ContactPanel instance to the Text properties of the Label controls.

Controls can repeatedly instantiate a template (in different container instances), as they do in common data-binding scenarios. Therefore, a template should not contain any control instances as member variables because these would be overwritten with new values each time the template is instantiated. To emphasize that our template does not have any control instances, we have declared the event handlers by using the static modifier.



Developing Microsoft ASP. NET Server Controls and Components
Developing Microsoft ASP.NET Server Controls and Components (Pro-Developer)
ISBN: 0735615829
EAN: 2147483647
Year: 2005
Pages: 183

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net