Implementing a Composite Control ”The CompositeLogin ExampleWe'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.
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.csusingSystem; 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:
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> </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 ControlsAs 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:
In addition, Control provides two members that are used for state management of child controls:
View State and Child ControlsTo 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 BubblingThe 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 ExampleTo 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
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.csusingSystem; 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 OverviewA 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%>'/> <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 ExampleWe'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
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"/> <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.csusingSystem; 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
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:
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:
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.csusingSystem; 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 classprivatesealedclassDefaultContactTemplate: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. |