Section 14.2. Custom Controls

14.2. Custom Controls

In addition to creating user controls, which are essentially reusable portions of web pages, you can create your own compiled custom controls. As noted earlier, there are three ways to create custom controls: derive from an existing control, create a control that combines existing controls, or create a new control from scratch.

Composite controls are most similar to user controls. The key difference is that composite controls are compiled into a dll and used as you would any server control.

To get started, you'll create a Web Control Library in which you'll create the various custom controls for this chapter. Open VS2005 and choose New Project. In the New Project dialog box, select Visual C# Projects, then Windows, and create a Web Control Library called CustomControls , as shown in Figure 14-6.

For inexplicable reasons, the Web Control Library is listed under Windows and not directly under Visual C#, nor under New Web Site.


Visual Studio has created a complete custom control named WebCustomControl1. Build the application.

If you try to run this application, you will get a message telling you that a project with an output type of Class Library cannot be started directly.


Before examining this control in detail, open a second instance of VS2005 and create a web site named CustomControlWebPages . This will act as your consuming application or your client . Both of these phrases mean the same thing: CustomControlWebPages will use the custom controls you'll put in your controls library. Right-click on the

Figure 14-6. Create CustomControls in Web Control Library

solution and add a reference to the CustomControls.dll you built, as shown in Figure 14-7.

Figure 14-7. Add Reference to CustomControls.dll

The solution explorer will reflect the new reference, as shown in Figure 14-8.

Figure 14-8. Custom Controls DLL in Solution Explorer

You are ready to add the custom control to your web page. This is done in two steps: register the control and use it (much like you did with User Controls). First, add the Register directive to the top of the content page of the consuming web page (i.e., CustomControlWebPages\Default.aspx ).

 <%@ Register TagPrefix="OReilly"        Namespace="CustomControls"        Assembly="CustomControls" %> 

Once again you use the @Register tag and provide a tag prefix ( OReilly ). Rather than providing a Tagname and src , however, you provide a Namespace and Assembly , which uniquely identify the control and the DLLthat the page must use.

You can add the control to the page. The two attributes you must set are the Runat attribute, which is needed for all server-side controls, and the Text attribute, which dictates how the control is displayed at runtime. The complete content file, listed in Example 14-7, incorporates the registration and the control declaration:

Example 14-7. default.aspx for CustomControlWebPages
 <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs"    Inherits="_Default" %>  <%@ Register TagPrefix="OReilly" Namespace="CustomControls"    Assembly="CustomControls" %>  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>Untitled Page</title> </head> <body>     <form id="form1" runat="server">     <div>  <OReilly:WebCustomControl1 ID="wcc1" runat="server"                                    Text="Hello Custom Control!" />  </div>     </form> </body> </html> 

When you view this page, the text you passed in is displayed, as shown Figure 14-9.

For your convenience, Example 14-8 shows the complete custom control source code provided by VS2005.

Figure 14-9. Custom Control running

Example 14-8. Custom Control provided by Visual Studio 2005
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControls {     [DefaultProperty("Text")]     [ToolboxData("<{0}:WebCustomControl1 runat=server></{0}:WebCustomControl1>")]     public class WebCustomControl1 : WebControl     {         private string text;         [Bindable(true)]         [Category("Appearance")]         [DefaultValue("")]         public string Text         {             get             {                 return text;             }             set             {                 text = value;             }         }         protected override void Render(HtmlTextWriter output)         {             output.Write(Text);         }     } } 

This control contains a single property, Text , backed by a private string variable, text .

Attributes are provided for the property and the class. These attributes are used by VS2005 and are not required when creating custom controls . The most commonly used attributes for custom controls are shown in Table 14-2.

Table 14-2. Common attributes for custom controls

Attribute

Description

Bindable

Boolean. true indicates that VS.NET will display this control in the data bindings dialog box.

Browsable

Boolean. Determines if the property is displayed in the designer.

Category

Determines in which category this control will be displayed when the Properties dialog is sorted by category.

DefaultProperty

The default property of the class.

DefaultValue

The default value.

Description

The text you provide is displayed in the description box in the Properties panel.

ToolboxData

Used by VS2005 to provide the tag when the object is dragged from the toolbox.


14.2.1. Properties

Custom controls can expose properties as any other class can. You access these properties programmatically (in code-behind) or declaratively by setting attributes of the custom control as you did in CustomControlWebPages and as shown here:

 <OReilly:WebCustomControl1 ID="wcc1" runat="server"  Text="Hello Custom Control!" />  

The Text property of the control is accessed through the Text attribute in the custom control declaration on the web page.

In the case of the Text property and the Text attribute, the mapping between the attribute and the underlying property is straightforward because both are strings. ASP.NET will provide intelligent conversion of other types, however. For example, if the underlying type is an integer or a long, the attribute will be converted to the appropriate value type. If the value is an enumeration, ASP.NET will match the string value against the evaluation name and set the correct enumeration value. If the value is a Boolean, ASP.NET will match the string value against the Boolean value, i.e., it will match the string "True" to the Boolean value TRue .

14.2.2. The Render Method

The key method of the custom control is Render . This method is declared in the base class and must be overridden in your derived class if you wish to take control of rendering to the page. The Render method uses the HtmlTextWriter object passed in as a parameter. In the case of the boilerplate custom control code provided by VS2005, this is used to write the string held in the Text property.

The HtmlTextWriter class derives from TextWriter and provides rich formatting capabilities. HtmlTextWriter will ensure the elements produced are well- formed , and it will manage the attributes, including style attributes, such as the following:

 output.AddStyleAttribute("color", "fuchsia"); 

This style attribute needs to be attached to a tag. To do this, replace the contents of the Render method in WebCustomControl1.cs with the following code snippet:

 output.AddStyleAttribute("color", "fuchsia");     output.RenderBeginTag("p");     output.Write(Text);     output.RenderEndTag(  ); 

The style attribute is attached to an opening <p> tag, the text is written, and the RenderEndTag renders the end tag for the most recent begin tag.

The result is that when the text is output, the correct tags are created, as shown in Figure 14-10. (The source output that illustrates the HTML rendered by the HtmlTextWriter is circled.)

Figure 14-10. Render tag

14.2.3. Updating the Control

Each time you modify the control in the custom control library, you must get the rebuilt DLL into the bin directory of your web site. You can do that by deleting and recreating the reference or by copying the .dll file by hand.

An alternative is to teach the Custom Controls library to build its DLL right into the bin directory of the test web site. To do this, open the Custom Controls library in VS2005 and click on Project Custom Controls Properties. Open the Build tab and set the Output path to the path for your web site, as shown in Figure 14-11.

Figure 14-11. Set the Output path

Each time you make a change in the custom controls, rebuild the project and the DLL will be placed in the correct directory for your web application.

14.2.4. Maintaining State

In the next example, you'll add a button to the custom control to increase the size of the text. To accomplish this, right-click on the CustomControls project and choose Add New Item. In the New Item dialog choose Web Custom Control and use the default name WebCustomControl2 , as shown in Figure 14-12.

Figure 14-12. Add a second custom control

Once again, VS2005 creates a default custom control. This time you'll render the text while setting the font size based on a Size property you'll add to the class.

Replace the existing line in the Render method with the highlighted line from the following code snippet:

 protected override void Render(HtmlTextWriter output)     {  output.Write("<font size = " + Size + ">" + Text + "</font>");  } 

This new line wraps the output text with an HTML <font> element and incorporates a property called Size , which you will define shortly.

First, open the consuming web application, CustomControlWebPages , and right-click on the web site folder in the Solution Explorer. Choose Add New Item. Add a new Web Form, accepting the default name Default2.aspx .

Drag a Button onto the new form and set its ID to btnSize and its Text to Increase Size. In Design view, double-click on the button to create the skeleton of an event handler for the Click event in the code-behind file. The Button declaration in the content file will look something like the following:

 <asp:Button ID="btnSize" runat="server"        OnClick="btnSize_Click"        Text="Increase Size" /> 

Add the Register directive for this custom control to the page file, and create an instance of the custom control within your page. The complete content file for the second web page, default2.aspx, is shown in Example 14-9.

Example 14-9. default2.aspx to test CustomControl2
 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs"    Inherits="Default2" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">  <%@ Register TagPrefix="OReilly"    Namespace="CustomControls"    Assembly="CustomControls" %>  <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>Untitled Page</title> </head> <body>     <form id="form1" runat="server">     <div>         <asp:Button ID="btnSize" runat="server"            OnClick="btnSize_Click"            Text="Increase Size" />  <OReilly:WebCustomControl2 ID="wc2" runat="server"            Text="Hello Custom Control!" />  </div>     </form> </body> </html> 

The registration statement is identical to that used in Default.aspx even though you are using a second control. Since they are both in the same control library, you only have to register the library and not each individual control.

Switch back to your new Custom control and add a Size property to WebCustomControl2 that stores its value in ViewState :

 public int Size     {         get { return Convert.ToInt32(ViewState["Size"]); }         set { ViewState["Size"] = value; }     } 

The property get method retrieves the value from ViewState and converts the value to an integer. The property set method stashes a string representing the size into ViewState .

To ensure a valid value is in ViewState to start with, you'll add a constructor to initialize the value:

 public WebCustomControl2(  )     {         ViewState["Size"] = 1;     } 

The constructor initializes the value held in ViewState to 1 . Each press of the button will update the Size property. To make that work, return to the web site and implement the btnSize_Click method in Default2.aspx.cs . Update the size in the btnSize_Click method:

 protected void btnSize_Click(object sender, EventArgs e)     {  wc2.Size += 1;  } 

To illustrate the effect of clicking the button, we created two instances of the program in Figure 14-13, and in the second instance we pressed the button three times. Before running the application, remember to rebuild the custom control project.

Figure 14-13. Maintaining state

Each time the button is clicked, the state variable Size is incremented. When the page is drawn, the state variable is retrieved and used to set the size of the text.

14.2.5. Creating Derived Controls

There are times when it is not necessary to create your own control from scratch. You may want to extend the behavior of an existing control type. You can derive from an existing control as you might derive from any class.

Imagine, for example, that you would like a button to maintain a count of the number of times it has been clicked. Such a button might be useful in any number of applications; unfortunately , the ASP.NET Button control does not provide this functionality.

To overcome this limitation of the Button class, you'll derive a new custom control from System.Web.UI.WebControls.Button , as shown in Example 14-10.

Example 14-10. CountedButton.cs
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControls {     [DefaultProperty("Text")]     [ToolboxData("<{0}:CountedButton runat=server></{0}:CountedButton>")]     public class CountedButton : System.Web.UI.WebControls.Button     {         // constructor initializes view state value         public CountedButton(  )         {             this.Text = "Click me";             ViewState["Count"] = 0;         }         // count as property maintained in view state         public int Count         {             get             {                 return (int)ViewState["Count"];             }             set             {                 ViewState["Count"] = value;             }         }         // override the OnClick to increment the count,         // update the button text and then invoke the base method         protected override void OnClick(EventArgs e)         {             ViewState["Count"] = ((int)ViewState["Count"]) + 1;             this.Text = ViewState["Count"] + " clicks";             base.OnClick(e);         }     } } 

Right-click on the CustomControls project and add a new Web Custom Control item named CountedButton.cs . Delete everything in the file and replace it with the code shown in Example 14-10.

You begin by deriving your new class from the existing Button type:

 public class CountedButton : System.Web.UI.WebControls.Button 

The work of this class is to maintain its state: how many times the button has been clicked. You provide a public property, Count , which is backed not by a private member variable but rather by a value stored in ViewState . This is necessary because the Button will post the page; otherwise , the state would be lost.

 public int Count     {        get        {           return (int) ViewState["Count"];        }        set        {           ViewState["Count"] = value;        }     } 

To retrieve the value " Count " from ViewState , you use the string Count as an offset into the ViewState collection. What is returned is an object that you cast to an int .

To ensure the property will return a valid value, you initialize the Count property in the constructor, where you set the initial text for the button.

 public CountedButton(  )     {        this.Text = "Click me";        ViewState["Count"] = 0;     } 

Because CountedButton derives from Button , it is easy to override the behavior of a Click event. In this case, when the user clicks the button, you will increment the Count value held in ViewState and update the text on the button to reflect the new count. You will then call the base class's OnClick method to carry on with the normal processing of the Click event.

 protected override void OnClick(EventArgs e)     {        ViewState["Count"] =  ((int)ViewState["Count"]) + 1;        this.Text = ViewState["Count"] + " clicks";        base.OnClick(e);     } 

Remember to build the CustomControls project.

Create a new page ( Default3.aspx ) in CustomControlWebPages , the consuming web application, and add the Register directive and the highlighted control declaration:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default3.aspx.cs"        Inherits="Default3" %>  <%@ Register TagPrefix="OReilly"        Namespace="CustomControls"        Assembly="CustomControls" %>  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"        "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">     <html xmlns="http://www.w3.org/1999/xhtml" >     <head runat="server">         <title>Counted Button</title>     </head>     <body>         <form id="form1" runat="server">         <div>  <OReilly:CountedButton runat="server" ID="cb1" />  </div>         </form>     </body>     </html> 

When you click the button four times, the button reflects the current count of clicks, as shown in Figure 14-14.

Figure 14-14. Counted Button

14.2.6. Creating Composite Controls

The third way to create a custom control is to combine two or more existing controls. In the next example, you will act as a contract programmer, and we will act as the client. We'd like you to build a more complex control that we might use to keep track of the number of inquiries we receive about our books.

As your potential client, we might ask you to write a control that lets us put in one or more books; each time we click on a book, the control will keep track of the number of clicks for that book, as shown in Figure 14-15.

Figure 14-15. Book counter

To begin, create a new web page in the CustomControlsWebPages web site named BookCounter.aspx . The complete listing for the content file is shown in Example 14-11.

Example 14-11. BookCounter.aspx
 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="BookCounter.aspx.cs" Inherits="BookCounter" %> <%@ Register TagPrefix="OReilly"    Namespace="CustomControls"    Assembly="CustomControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/ xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server">     <title>Book Counter</title> </head> <body>     <form id="form1" runat="server">     <div>        <OReilly:BookInquiryList ID="bookInquiry1" Runat="Server">           <OReilly:BookCounter ID="Bookcounter1" Runat="server"                                BookName="Programming ASP.NET" />           <OReilly:BookCounter ID="Bookcounter2" Runat="server"                                BookName="Programming C#" />           <OReilly:BookCounter ID="Bookcounter3" Runat="server"                                BookName="Programming Visual Basic 2005" />           <OReilly:BookCounter ID="Bookcounter4" Runat="server"                                BookName="Visual C#: A Developers Notebook" />           <OReilly:BookCounter ID="BookCounter5" Runat="server"                                BookName="Teach Yourself C++ 21 Days" />           <OReilly:BookCounter ID="Bookcounter6"  Runat="server"                                BookName="Teach Yourself C++ 24 Hours" />           <OReilly:BookCounter ID="Bookcounter7" Runat="server"                                BookName="Clouds To Code" />        </OReilly:BookInquiryList>     </div>     </form> </body> </html> 

The key thing to note in this code is that the BookInquiryList component contains a number of BookCounter elements. There is one BookCounter element for each book we want to track in the control. The control is quite flexible. We can track one, seven (as shown here), or any arbitrary number of books. Each BookCounter element has a BookName attribute used to display the name of the book being tracked.

You can see in Figure 14-15 that each book is tracked using a CountedButton custom control, but you do not see a declaration of the CountedButton in the .aspx file. The CountedButton control is entirely encapsulated within the BookCounter custom control.

The entire architecture, therefore, is as follows :

  1. The BookInquiryList composite control derives from WebControl and implements INamingContainer (described shortly).

  2. The BookInquiryList control has a Controls property it inherits from the Control class (through WebControl ) and that returns a collection of child controls.

  3. Within this Controls collection is an arbitrary number of BookCounter controls.

  4. BookCounter is a composite control that derives from WebControl and that implements INamingContainer .

  5. Each instance of BookContainer has two properties, BookName and Count :



    Name

    Backed by ViewState and is initialized through the BookName in the . aspx file



    Count

    Delegates to a private CountedButton object, which is instantiated in BookContainer.CreateChildControls( )

The BookInquiryList object has two purposes: it acts as a container for the BookCounter objects, and it is responsible for rendering itself and ensuring that its contained BookCounter objects render themselves on demand.

The best way to see how all this works is to work your way through the code from the inside out. The most contained object is the CountedButton .

14.2.6.1. Modifying the CountedButton derived control

CountedButton needs minor modification. To keep the code clear, create a new user control named CountedButton2 and have it derive from Button , as shown in Example 14-12.

Example 14-12. CountedButton2.cs
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControls {     [DefaultProperty("Text")]     [ToolboxData("<{0}:CountedButton2      runat=server></{0}:CountedButton2>")]     public class CountedButton2 : System.Web.UI.WebControls.Button     {  private string displayString;  // default constructor       public CountedButton2(  )       {  displayString = "clicks";  InitValues(  );       }       // overloaded, takes string to display (e.g., 5 books)  public CountedButton2(string displayString)       {          this.displayString = displayString;          InitValues(  );       }  // called by constructors       private void InitValues(  )       {          if (ViewState["Count"] == null)             ViewState["Count"] = 0;          this.Text = "Click me";       }       // count as property maintained in view state       public int Count       {          get          {             return (int) ViewState["Count"];          }          set          {             ViewState["Count"] = value;          }       }       // override the OnClick to increment the count,       // update the button text and then invoke the base method       protected override void OnClick(EventArgs e)       {          ViewState["Count"] =  ((int)ViewState["Count"]) + 1;          this.Text = ViewState["Count"] + " " + displayString;          base.OnClick(e);       }     } } 

Because you want the button to be able to display the string 5 Inquiries rather than five clicks, you must change the line within the OnClick method that sets the button's text:

 this.Text = ViewState["Count"] + " " + displayString; 

Rather than hard-wiring the string, you'll use a private member variable, displayString , to store a value passed in to the constructor:

 private string displayString; 

You must set this string in the constructor. To protect client code that uses the default constructor (with no parameters), you'll overload the constructor, adding a version that takes a string:

 public CountedButton2(string displayString)      {         this.displayString = displayString;         InitValues(  );      } 

You can now modify the default constructor to set the displayString member variable to a reasonable default value:

 public CountedButton2(  )     {        displayString = "clicks";        InitValues(  );     } 

The code common to both constructors has been factored out to the private helper method InitValues , which ensures that the Count property is initialized to zero and sets the initial text for the button:

 private void InitValues(  )     {        if (ViewState["Count"] == null)           ViewState["Count"] = 0;        this.Text = "Click me";     } 

With these changes, the CountedButton is ready to be used in the first composite control, BookCounter .

14.2.6.2. Creating the BookCounter composite control

The BookCounter composite control is responsible for keeping track of and displaying the number of inquiries about an individual book. Create a new control in CustomControls named BookCounter and modify it, as shown in Example 14-13.

Example 14-13. BookCounter.cs composite control
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControls {     [ToolboxData("<{0}:BookCounter runat=server></{0}:BookCounter>")]  public class BookCounter : WebControl, INamingContainer  {         // intialize the counted button member         CountedButton2 btn = new CountedButton2("inquiries");         public string BookName         {             get             {                 return (string)ViewState["BookName"];             }             set             {                 ViewState["BookName"] = value;             }         }         public int Count         {             get             {                 return btn.Count;             }             set             {                 btn.Count = value;             }         }         public void Reset(  )         {             btn.Count = 0;         }         protected override void CreateChildControls(  )         {             Controls.Add(btn);         }     } } 

14.2.6.2.1. INamingContainer

The BookCounter class implements the INamingContainer interface. This is a "marker" interface that has no methods . This interface identifies a container control that creates a new ID namespace, guaranteeing that all child controls have IDs that are unique to the page.

14.2.6.2.2. Containing CountedButton2

The BookCounter class contains an instance of CountedBut-ton2 :

 CountedButton2 btn = new CountedButton2("inquiries"); 

The btn member is instantiated in the CreateChildControls method inherited from System.Control :

 protected override void CreateChildControls(  )     {        Controls.Add(btn);     } 

CreateChildControls is called in preparation for rendering and offers the BookCounter class the opportunity to add the btn object as a contained control.

The complete control life cycle is presented in Chapter 6. That same life cycle applies to custom controls as to ASP.NET Web controls.


There is no need for BookCounter to override the Render method; the only thing it must render is the CountedButton , which can render itself. The default behavior of Render is to render all the child controls, so you need not do anything special to make this work.

BookCounter has two properties: BookName and Count . BookName is a string to be displayed in the control and is managed through ViewState :

 public string BookName     {         get         {             return (string) ViewState["BookName"];         }         set         {             ViewState["BookName"] = value;         }     } 

Count is the count of inquires about this particular book; responsibility for keeping track of this value is delegated to the CountedButton2 :

 public int Count     {        get        {           return btn.Count;        }        set        {           btn.Count = value;        }     } 

There is no need to place the value in ViewState since the button is responsible for its own data.

14.2.6.3. Creating the BookInquiryList composite control

Each of the BookCounter objects is contained within the Controls collection of the BookInquiryList . Create another control in the CustomControls project named BookInquiryList and modify it, as shown in Example 14-14. BookInquiryList has no properties or state. Its only method is Render .

Example 14-14. BookInquiryList.cs
 using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Web.UI; using System.Web.UI.WebControls; namespace CustomControls {     [ControlBuilderAttribute(typeof(BookCounterBuilder)),     ParseChildren(false)]     public class BookInquiryList : WebControl, INamingContainer     {         protected override void Render(HtmlTextWriter output)         {             int totalInquiries = 0;             BookCounter current;             // Write the header             output.Write("<Table border='1' width='90%' cellpadding='1'" +                "cellspacing='1' align = 'center' >");             output.Write("<TR><TD colspan = '2' align='center'>");             output.Write("<B> Inquiries </B></TD></TR>");             // if you have no contained controls, write the default msg.             if (Controls.Count == 0)             {                 output.Write("<TR><TD colspan='2' align='center'>");                 output.Write("<B> No books listed </B></TD></TR>");             }             // otherwise render each of the contained controls             else             {                 // iterate over the controls colelction and                 // display the book name for each                 // then tell each contained control to render itself                 for (int i = 0; i < Controls.Count; i++)                 {                     current = (BookCounter)Controls[i];                     totalInquiries += current.Count;                     output.Write("<TR><TD align='left'>" +                        current.BookName + "</TD>");                     output.RenderBeginTag("TD");                     current.RenderControl(output);                     output.RenderEndTag(  );  // end td                     output.Write("</tr>");                 }                 output.Write("<TR><TD colspan='2' align='center'> " +                    " Total Inquiries: " +                    totalInquiries + "</TD></TR>");             }             output.Write("</TABLE>");         }     } } 

14.2.6.3.1. ControlBuilder and ParseChildren attributes

The BookCounter class must be associated with the BookInquiryClass so ASP.NET can translate the elements in the .aspx page into the appropriate code. This is accomplished by adding the ControlBuilderAttribute attribute to the BookInquiryList class declaration:

 [DefaultProperty("Text")]        [ToolboxData("<{0}:BookInquiryList              runat=server></{0}:BookInquiryList>")]  [ControlBuilderAttribute(typeof(BookCounterBuilder)),              ParseChildren(false)]  public class BookInquiryList : WebControl, INamingContainer        { 

The first argument to the ControlBuilderAttribute attribute is a Type object that you obtain by passing in BookCounterBuilder , a class you will define to return the type of the BookCounter class given a tag named BookCounter . BookCounterBuilder is defined in the following code, added to BookInquiryList.cs inside the CustomControls namespace:

 internal class BookCounterBuilder : ControlBuilder     {        public override Type GetChildControlType(           string tagName, System.Collections.IDictionary attributes)        {  if (tagName == "BookCounter")              return typeof(BookCounter);  else              return null;        }        public override void AppendLiteralString(string s)        {        }     } 

ASP.NET will use this BookCounterBuilder class, which derives from ControlBuilder , to determine the type of the object indicated by the BookCounter tag. Through this association, each of the BookCounter objects will be instantiated and added to the Controls collection of the BookInquiryClass .

The BookCounterBuilder 's method GetChildControlType uses the classic (non-generic) IDictionary interface.


The second argument of the ControlBuilderAttribute attribute, ParseChildren , must be set to false to tell ASP.NET you have handled the children attributes and no further parsing is required. A value of false indicates the nested child attributes are not properties of the outer object, but rather are child controls.

14.2.6.3.2. Render

The only method of the BookInquiryList class is the override of Render . The purpose of Render is to draw the table shown earlier in Figure 14-15, using the data managed by each of the BookCounter child controls.

The BookInquiryList class provides a count of the total number of inquiries, as shown in Figure 14-16.

Figure 14-16. Total inquiries displayed

The code tallies inquiries by initializing an integer variable, totalInquiries , to and then iterating over each control in turn , asking the control for its Count property:

 totalInquiries += current.Count; 

The Count property of the control delegates to the CountedButton 's Count property.

14.2.6.3.3. Rendering the output

That same loop in the overridden Render method of the BookInquiryList class renders each of the child controls by iterating over each of the controls, building up the output HTML:

 for (int i = 0; i < Controls.Count; i++)     {        current = (BookCounter) Controls[i];        totalInquiries += current.Count;        output.Write("<TR><TD align='left'>" +           current.BookName + "</TD>");        output.RenderBeginTag("TD");        current.RenderControl(output);        output.RenderEndTag(  );  // end td        output.Write("</tr>");     } 

The local BookCounter object, current , is assigned to each object in the Controls collection in succession:

 for (int i = 0; i < Controls.Count; i++)     {        current = (BookCounter) Controls[i]; 

With that object, you are able to get the Count , as described previously:

 totalInquiries += current.Count; 

Then you proceed to render the object. The HTMLTextWriter is used first to create a row and to display the name of the book, using the BookName property of the current BookCounter object:

 output.Write("<TR><TD align='left'>" +        current.BookName + "</TD>"); 

You then render a td tag, and within that tag, you tell the BookCounter object to render itself. Finally, you render an ending td tag using RenderEndTag and an ending row tag using the Write method of the HTMLTextWriter :

 output.RenderBeginTag("TD");      current.RenderControl(output);      output.RenderEndTag(  );  // end td      output.Write("</tr>"); 

You tell the contained control to render itself:

 current.RenderControl(output); 

When you do this, the; method of BookCounter is called. Since you have not overridden this method, the Render method of the base class is called, which tells each contained object to render itself. The only contained object is CountedButton . Since you have not overridden Render in CountedButton , the base Render method in Button is called, and the button is rendered.

Assignment of Responsibilities

This example of a composite control is interesting because the various responsibilities are spread among the participating objects. The BookInquiryList object assumes all responsibility for laying out the control, creating the table, and deciding what will be rendered where. However, it delegates responsibility for rendering the Button object to the individual contained controls.

Similarly, the BookInquiryList is responsible for the total number of inquiriesbecause that information transcends what any individual BookCounter object might know. However, the responsibility for the count held by each BookCounter is delegated to the BookCounter itself. As far as the BookInquiryList is concerned , it gets that information directly from the BookCounter 's Count property. It turns out, however, that BookCounter delegates that responsibility to the CountedButton .


14.2.6.3.4. Rendering the summary

Once all of the child controls have been rendered, the BookInquiryList creates a new row to display the total inquiries:

 output.Write("<TR><TD colspan='2' align='center'> " +        " Total Inquiries: " +        totalInquiries + "</TD></TR>"); 



Programming ASP. NET
Programming ASP.NET 3.5
ISBN: 0596529562
EAN: 2147483647
Year: 2003
Pages: 173

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