8.1 Fundamentals


Before we delve into the details of control creation, it is important to understand what a control is and how the existing controls in the framework function. When an .aspx page is parsed, an instance of the System.Web.UI.Page -derived class is constructed . This instance in turn contains controls that render the contents of the page. It may contain members of the HtmlControls hierarchy, which mirror their respective HTML elements. It may also contain WebControls , which are higher-level controls with a more uniform interface than native HTML elements, but which render HTML when the page is rendered as well. It is also likely to contain instances of the LiteralControl class, which simply renders the literal HTML it stores when requested . For example, consider the .aspx page shown in Listing 8-1.

Listing 8-1 A Simple .aspx Page
 <! File: SimplePage.aspx > <%@ Page language='C#' trace='true' %> <html> <head> <script runat='server'> protected void OnEnterName(object src, EventArgs e) {   _message.Text = string.Format("Hi, {0}, how are you?", _name.Text); } </script> </head> <body> <form runat='server'> <h2>Enter your name:     <asp:TextBox id='_name' runat='server'/> </h2> <asp:Button Text='Enter'             OnClick='OnEnterName' runat='server'/> <br/> <asp:Label id='_message' runat='server'/> </form> </body> </html> 

When this page is accessed, it is compiled into a Page -derived class with a collection of child controls. The literal HTML is placed into instances of the LiteralControl class, which when asked to render, simply regurgitates whatever text it was assigned. Any controls marked with runat=server are turned into server-side controls and then added to the controls collection of their immediate parent control. The Page class serves as the root control, and it has three immediate child controls: two literal controls to generate the beginning and end of the page text, and a server-side HtmlForm control. Because all the remaining server-side controls are within the server-side form, they are added as child controls to the HtmlForm control. Note that the order in which controls are added to their parent's Controls collection is important because it determines the rendering order of the controls. Figure 8-1 shows the control hierarchy that is generated for this page.

Figure 8-1. Control Hierarchy for a Parsed .aspx Page

graphics/08fig01.gif

Once this hierarchy is constructed, the page can be rendered by invoking the top-level Render method (this happens implicitly when the IHttpHandler.ProcessRequest method of the Page class is invoked). The effect of calling Render on the Page class is to perform a depth-first traversal of the control tree, asking each control to add its contribution to the Response buffer in turn. As the user interacts with the page, controls take on new state and render themselves differently. For example, when the user performs a post-back in our sample page by pressing the Enter button, the value of the Label control is a string greeting the user with his name.

8.1.1 Writing Custom Controls

All the ASP.NET controls, including the Page class, derive from the common base class System.Web.UI.Control , which provides the necessary methods , events, and fields to define a control's behavior. To create your own custom control, you must create a new class that derives from System.Web.UI.Control , and typically you override the virtual Render() method to generate whatever HTML is necessary to display the contents of your control. Any public methods, properties, or events added to your Control -derived class become part of the interface to the control as well. Listing 8-2 shows a simple control called NameControl that renders its string Name property as either an h1 or h2 tag, depending on the public IsImportant property.

Listing 8-2 The Name Control
 // File: name.cs using System; using System.Web; using System.Web.UI; namespace EssentialAspDotNet.CustomControls {   public class NameControl : Control   {      private string _name;      private bool   _isImportant;      public string Name      {        get { return _name;  }        set { _name = value; }      }      public bool IsImportant      {        get { return _isImportant;  }        set { _isImportant = value; }      }      protected override void Render(HtmlTextWriter writer)      {        string txt;        if (_isImportant)           txt = string.Format("<h1>{0}</h1>", _name);        else           txt = string.Format("<h2>{0}</h2>", _name);        writer.Write(txt);      }   } } 

To deploy this control, we would first compile it into an assembly and then deploy that assembly into the /bin directory of the ASP.NET application in which it was to be used. Alternatively, we could sign the compiled assembly with a public/private key pair and deploy it in the global assembly cache so that it could easily be accessed on a machine-wide basis. A sample showing how to compile and deploy this from the command prompt is shown in Listing 8-3.

Listing 8-3 Command-Line Compilation of NameControl
 csc /t:library /out:bin\Name.dll Name.cs 

8.1.2 Using Custom Controls

One of the most appealing features of custom controls is that their usage is syntactically identical to the intrinsic controls built into ASP.NET. Once you include a Register directive at the top of your .aspx file to reference the custom control, you can then use the TagPrefix string assigned in the Register directive to refer to any controls in the referenced namespace. For example, Listing 8-4 shows a sample .aspx client file to our NameControl described earlier, with two instances of the custom control.

Listing 8-4 Client Page Using NameControl
 <%@ Page Language='C#' %> <%@ Register Namespace='EssentialAspDotNet.CustomControls'              TagPrefix='eadn' Assembly='name' %> <html> <body> <eadn:NameControl runat='server' Name='Fred'              IsImportant='false'/> <eadn:NameControl runat='server' Name='George'                   IsImportant='true' /> </body> </html> 

Notice that the tag attributes of your control are implicitly mapped to the public properties in the target control. Also note that the system implicitly converts the text-based attributes of your custom control tag into the type expected by the control's property. In our case, the IsImportant attribute must be converted from a string to a Boolean, and the Name property requires no conversion. This implicit conversion works for all standard built-in types and can be customized by providing your own converter, as we discuss in the section Designer Integration later in this chapter.

It is a testament to the simplicity and elegance of the custom control model that we have covered the details of both control creation and usage in only a few pages. The remainder of this chapter covers individual pieces of the control architecture in more detail, but the core architecture has been presented in its entirety.

8.1.3 System.Web.UI.Control

Every custom control that you create derives either directly or indirectly from System.Web.UI.Control , so it is important to understand the features provided by this class in detail. We have so far looked only at the virtual Render method, but there are several other methods, properties, and events you should be familiar with as you start building controls. Listing 8-5 shows the core methods, properties, and events of this class.

Listing 8-5 Core Methods, Properties, and Events of System.Web.UI.Control
 public class Control : IComponent,                        IDisposable,                        IParserAccessor,                        IDataBindingsAccessor {   // Rendering   protected virtual void Render(HtmlTextWriter);   public            void RenderControl(HtmlTextWriter);   protected virtual void RenderChildren(HtmlTextWriter);   public    virtual bool Visible {get; set;}   // Child control management   protected virtual void    CreateChildControls();   protected virtual void    EnsureChildControls();   public    virtual Control FindControl(string);   public    virtual bool    HasControls();   public    virtual ControlCollection Controls {get;}   public    virtual Control NamingContainer    {get;}   protected         bool  ChildControlsCreated {get; set;}   // Identification   public virtual string  ClientID           {get;}   public virtual string  ID                 {get; set;}   public virtual string  UniqueID           {get;}   // Accessors   public    virtual Page        Page        {get; set;}   public    virtual Control     Parent      {get;}   protected virtual HttpContext Context     {get;}   // State management   public    virtual bool        EnableViewState {get; set;}   protected virtual StateBag    ViewState           {get;}   protected         bool        HasChildViewState   {get;}   protected         bool        IsTrackingViewState {get;}   // Events   public event EventHandler DataBinding;   public event EventHandler Init;   public event EventHandler Load;   public event EventHandler PreRender;   public event EventHandler Unload;   // Misc   public virtual void   DataBind();   public         string ResolveUrl(string); } 

The first function you typically override in classes derived from this class is the Render method. Render is called whenever a control is asked to render its content into the response buffer. It takes as a parameter a reference to an HtmlTextWriter object, which contains helper methods for writing HTML tags to the response buffer. Notice that the Render method is a protected method and cannot be called outside the Control class. To explicitly ask a control to render its contents, you call the RenderControl function, which internally invokes the Render method. This gives the base class an opportunity to pre- and postprocess render requests if needed (currently it is used to generate additional trace information when tracing is enabled).

The other method related to rendering is the RenderChildren method, whose default implementation iterates across the Controls collection, invoking RenderControl on each child control. The base class implementation of Render calls RenderChildren as a default behavior that is useful for composite controls, as we will see later. Keep in mind that if you override the Render method, the base class version is not called unless you explicitly call base.Render() . Thus, if your control contains other controls as child controls, you should decide whether it makes sense to render those child controls in your Render implementation and call base.Render() as appropriate.

Finally, note that if you set the Visible property of your control to false , the base class implementation of RenderControl does not dispatch the call to your Render method at all.

Control authors should understand how controls are identified, both internally and when they are rendered to the client. Three identifiers are associated with a control: ClientID , ID , and UniqueID . Most of the time, these identifiers will be the same. If your control is labeled with an ID attribute when it is used, the value of that attribute will be your control's ID , and if not, ASP.NET synthesizes an ID for your control when it is rendered. If, however, your control is a child control of another control that is a naming container (such as a Repeater ), these three values will be different. In this scenario, the ID will still be whatever string was placed in the ID attribute for your control, but the UniqueID will be that ID prefixed with the ID of the containing control (such as _ctrl0:myid ), and the ClientID will be the same as the UniqueID but with the ":" character replaced with the "_" character (such as _ctrl0_my_id ). This ensures that repeated instances of a control marked with the same ID are scoped by its container control, avoiding ID clashes in the client. When rendering your control, if you need a unique ID in your generated HTML, you should use the ClientID, because it will always be an HTML-compatible ID. The convention used by the WebControls and HtmlControls is to use the UniqueID for the name attribute of rendered HTML controls, and ClientID for the ID attribute.

Finally, it is useful to know what accessors are available when you are authoring controls. You have direct access to your containing Page class through the Page property and to the current HttpContext through the Context property. The Parent property can be used to query your immediate parent control. The remaining elements of the Control class are discussed as we explore control implementation techniques in more detail next .

8.1.4 HtmlTextWriter

Although you can completely render your control by using only the Write method of the HtmlTextWriter class, many other methods are available that can both simplify rendering and make the output more browser generic. Internally, all the Web controls defined in the System.Web.UI hierarchy use the HTML-rendering methods of the HtmlTextWriter class to generate their output. This class can be used in three ways to generate HTML output. First, you can use the overridden versions of Write and WriteLine to generate whatever text you need into the response buffer. Second, you can use the non-stack-based convenience methods, such as WriteBeginTag and WriteEndTag , to generate each HTML element programmatically instead of generating string literals. The third, and most appealing, way is to use the stack-based methods, such as RenderBeginTag and RenderEndTag , for easily constructing blocks of nested HTML elements. Listing 8-6 shows the core methods of the HtmlTextWriter class, grouped into methods you would use for each of the three rendering techniques supported by the class.

Listing 8-6 Core Methods of HtmlTextWriter
 public class HtmlTextWriter : TextWriter {   // Convenience, stack-based methods   public virtual void          AddAttribute(HtmlTextWriterAttribute, string);   public virtual void          AddStyleAttribute(HtmlTextWriterStyle, string);   public virtual void RenderBeginTag(HtmlTextWriterTag);   public virtual void RenderEndTag();   // Convenience, non-stack-based methods   public virtual void WriteAttribute(string, string);   public virtual void WriteBeginTag(string);   public virtual void WriteEndTag(string);   public virtual void WriteFullBeginTag(string);   public virtual void WriteStyleAttribute(string, string);   // Do-it-yourself methods   public override void Write(string);   public override void WriteLine(string);   // Indent controls level of indentation in output   public int Indent { get; set; }   // Newline character   public override string NewLine {get; set;}   // Note - many of these methods have overloads to   // support alternate types not shown here } 

The stack-based methods for rendering keep track of tags written using the RenderBeginTag method, and when a corresponding call to the RenderEndTag method is made, the appropriate terminating tag is generated into the output stream. The AddAttribute and AddStyleAttribute methods are a way of decorating the next call to RenderBeginTag with standard or style attributes, and once the call to RenderBeginTag has been made, the attributes are flushed. The indentation of the rendered HTML is also performed implicitly when you use the stack-based method of rendering. For example, consider the Render method of a control, shown in Listing 8-7, that generates a table using only the Write method, and manually adjusts the indentation level.

Listing 8-7 Sample Control Render Method Using Write/WriteLine
 protected override void Render(HtmlTextWriter output) {   output.WriteLine("<table width='50%' border='1'>");   output.Indent++;   output.WriteLine("<tr>");   output.Indent++;   output.Write("<td align='left'");   output.Write(" style='font-size:medium;color:blue;'>");   output.WriteLine("This is row 0 column 0");   output.WriteLine("</td>");   output.Write("<td align='right' ");   output.WriteLine("style='color:green;'>");   output.WriteLine("This is row 0 column 1");   output.WriteLine("</td>");   output.Indent;   output.WriteLine("</tr>");   output.Indent;   output.WriteLine("</table>"); } 

There are two problems with rendering a control this way. First, it is very difficult to reuse portions of this rendering, because each piece must be placed specifically in order in the output string. Second, because we are writing out literal strings, there is no opportunity for ASP.NET classes to alter the output based on the capabilities of the current browser. For example, in our generated table definition, we are using the style attribute to alter the appearance of the column text. If this were rendered for Netscape 4.0, this style attribute would have no effect, because it is not supported in that browser type. The alternative to generating raw text is to use the stack-based HTML-rendering methods of the HtmlTextWriter class, as shown in Listing 8-8.

Listing 8-8 Sample Control Render Method Using Stack-Based Methods
 protected override void Render(HtmlTextWriter output) {   output.AddAttribute(HtmlTextWriterAttribute.Width,                       "50%");   output.AddAttribute(HtmlTextWriterAttribute.Border, "1");   output.RenderBeginTag(HtmlTextWriterTag.Table); //<table>   output.RenderBeginTag(HtmlTextWriterTag.Tr); // <tr>   output.AddAttribute(HtmlTextWriterAttribute.Align,                       "left");   output.AddStyleAttribute(HtmlTextWriterStyle.FontSize,                            "medium");   output.AddStyleAttribute(HtmlTextWriterStyle.Color,                            "blue");   output.RenderBeginTag(HtmlTextWriterTag.Td); // <td>   output.Write("This is row 0 column 0");   output.RenderEndTag(); // </td>   output.AddAttribute(HtmlTextWriterAttribute.Align,                       "right");   output.AddStyleAttribute(HtmlTextWriterStyle.Color,                            "green");   output.RenderBeginTag(HtmlTextWriterTag.Td); // <td>   output.Write("This is row 0 column 1");   output.RenderEndTag(); // </td>   output.RenderEndTag(); // </tr>   output.RenderEndTag(); // </table> } 

Although a bit longer-winded, this version of our table rendering is easier to modify without breaking the syntax in the rendered HTML. Most importantly, it will render the table differently based on the client's browser capabilities. If accessed by a browser that can render only HTML 3.2 (instead of the more current HTML 4.0), this rendering method, instead of passing a reference to HtmlTextWriter , will pass a reference to an Html32TextWriter object, and it will render HTML 3.2 “compliant tags. For example, it converts HTML 4.0 “style attributes into the equivalent tags and attributes in HTML 3.2. Html32TextWriter will also standardize the propagation of attributes such as colors and fonts, which tend to vary in behavior in earlier browsers, by using HTML tables.

Adding an attribute to any element is simply a matter of adding an AddAttribute call before the RenderBeginTag that generates that element. Tables 8-1, 8-2, and 8-3 show all the possible values for the HtmlTextWriterAttribute , HtmlTextWriterStyle , and HtmlTextWriterTag enumerations, for use with these stack-based rendering functions.

Table 8-1. HtmlTextWriterAttribute Enumeration Values

Values

Accesskey

Align

Alt

Background

Bgcolor

Border

Bordercolor

Cellpadding

Cellspacing

Checked

Class

Cols

Colspan

Disabled

For

Height

Href

  Id  

Maxlength

Multiple

Name

Nowrap

Onchange

Onclick

ReadOnly

Rows

Rowspan

Rules

Selected

Size

Src

Style

Tabindex

Target

Title

Type

Valign

Value

Width

Wrap

   
Table 8-2. HtmlTextWriterStyle Enumeration Values

Values

BackgroundColor

BackgroundImage

BorderCollapse

BorderColor

BorderStyle

BorderWidth

Color

FontFamily

FontSize

FontStyle

FontWeight

Height

TextDecoration

Width

 
Table 8-3. HtmlTextWriterTag Enumeration Values

Values

  A  

Acronym

Address

Area

  B  

Base

Basefont

Bdo

Bgsound

Big

Blockquote

Body

  Br  

Button

Caption

Center

Cite

Code

Col

Colgroup

  Dd  

Del

Dfn

Dir

Div

  Dl  
  Dt  
  Em  

Embed

Fieldset

Font

Form

Frame

Frameset

  H1  
  H2  
  H3  
  H4  
  H5  
  H6  

Head

  Hr  

Html

  I  

Iframe

Img

Input

Ins

Isindex

Kbd

Label

Legend

  Li  

Link

Map

Marquee

Menu

Meta

Nobr

Noframes

Noscript

Object

  Ol  

Option

  P  

Param

Pre

  Q  
  Rt  

Ruby

  S  

Samp

Script

Select

Small

Span

Strike

Strong

Style

Sub

Sup

Table

Tbody

  Td  

Textarea

Tfoot

  Th  

Thead

Title

  Tr  
  Tt  
  U  
  Ul  

Unknown

Var

Wbr

Xml

   

The final technique for implementing the Render method of a custom control is to use the non-stack-based helper methods to generate the output. This involves using the Write methods of HtmlTextWriter class, such as WriteBeginTag and WriteEndTag . While the stack-based rendering with the HtmlTextWriter class should be the technique of choice in most circumstances, these Write methods can be used where you want more precise control over the rendered HTML, but you don't want to resort to writing string literals. These methods can also be used when you are writing out nonstandard HTML elements or even, perhaps, generic XML elements, with specific formatting requirements that must be met. These methods are not stack based, so you must take responsibility for rendering each begin and end tag separately. Also, these methods do not take into consideration the current indentation level, so you must explicitly add tab or space characters to indent your output. Listing 8-9 shows the same table we have been rendering, using the non-stack-based methods for rendering. In this example, several literal characters are used in Write method invocations. The complete list of the literal characters defined in the HtmlText Writer class is shown in Table 8-4.

Listing 8-9 Sample Control Render Method Using Non-Stack-Based Methods
 protected override void Render(HtmlTextWriter output) {   output.WriteBeginTag("table");   output.WriteAttribute("width", "50%");   output.WriteAttribute("border", "1");   output.Write(HtmlTextWriter.TagRightChar);   output.Write(output.NewLine);   output.WriteFullBeginTag("tr");   output.Write(output.NewLine);   output.WriteBeginTag("td");   output.WriteAttribute("align", "left");   output.Write(HtmlTextWriter.SpaceChar);   output.Write("style");   output.Write(HtmlTextWriter.EqualsDoubleQuoteString);   output.WriteStyleAttribute("font-size", "medium");   output.WriteStyleAttribute("color", "blue");   output.Write(HtmlTextWriter.DoubleQuoteChar);   output.Write(HtmlTextWriter.TagRightChar);   output.Write("This is row 0 column 0");   output.WriteEndTag("td");   output.Write(output.NewLine);   output.WriteBeginTag("td");   output.WriteAttribute("align", "right");   output.Write(HtmlTextWriter.SpaceChar);   output.Write("style");   output.Write(HtmlTextWriter.EqualsDoubleQuoteString);   output.WriteStyleAttribute("color", "green");   output.Write(HtmlTextWriter.DoubleQuoteChar);   output.Write(HtmlTextWriter.TagRightChar);   output.Write("This is row 0 column 1");   output.WriteEndTag("td");   output.Write(output.NewLine);   output.WriteEndTag("tr");   output.Write(output.NewLine);   output.WriteEndTag("table"); } 
Table 8-4. HtmlTextWriter Static Character Properties

Property

Value

DefaultTabString

<tab character>

DoubleQuoteChar

  "  

EndTagLeftChars

  </  

EqualsChar

  =  

EqualsDoubleQuoteString

  ="  

SelfClosingChars

  /  

SelfClosingTagEnd

  />  

SemicolonChar

  ;  

SingleQuoteChar

  '  

SlashChar

  /  

SpaceChar

<space character>

StyleEqualsChar

  :  

TagLeftChar

  <  

TagRightChar

  >  

8.1.5 Browser Independence

One of the most appealing features of server-side controls is the potential for browser-independent, reusable Web components. Several of the controls that ship with ASP.NET generate different HTML based on the client browser. For example, several of the IE Web controls use HTML Components (HTCs) to define DHTML behaviors if the client browser supports them (IE5 or higher), and if not, the controls render the appropriate JavaScript to provide equivalent behavior.

You can build controls that conditionally generate different HTML as well by querying the client browser capabilities through the Browser property of the Request object of the Page . The Browser is an instance of the HttpBrowserCapabilities class, which contains information about the capabilities of the client browser. Listing 8-10 shows the properties of the HttpBrowserCapabilities class, and Listing 8-11 shows a simple control that changes its rendering based on whether the client browser supports JavaScript or not.

Listing 8-10 The HttpBrowserCapabilities Class
 class HttpBrowserCapabilities : HttpCapabilitiesBase {   public bool           ActiveXControls          {get;}   public bool           AOL                      {get;}   public bool           BackgroundSounds         {get;}   public bool           Beta                     {get;}   public bool           Browser                  {get;}   public bool           CDF                      {get;}   public Version        ClrVersion               {get;}   public bool           Cookies                  {get;}   public bool           Crawler                  {get;}   public Version        EcmaScriptVersion        {get;}   public bool           Frames                   {get;}   public bool           JavaApplets              {get;}   public bool           JavaScript               {get;}   public int            MajorVersion             {get;}   public double         MinorVersion             {get;}   public Version        MSDomVersion             {get;}   public string         Platform                 {get;}   public bool           Tables                   {get;}   public string         Type                     {get;}   public bool           VBScript                 {get;}   public string         Version                  {get;}   public Version        W3CDomVersion            {get;}   public bool           Win16                    {get;}   public bool           Win32                    {get;}   //... } 
Listing 8-11 Example of a Control That Changes Its Rendering Based on Browser Capabilities
 public class BrowserIndependentControl : Control {   protected override void Render(HtmlTextWriter output)   {      if (Page.Request.Browser.JavaScript)   output.Write("<h3 onclick=\"alert('Hi there')\">click me!</h3>");     else        output.Write("<h3>Don't bother</h3>");   } } 

The information used to populate the HttpBrowserCapabilities class for any given request is drawn from the <browserCaps> element of the systemwide machine.config file. This element contains regular expression matches for browser headers and assigns the values in the HttpBrowserCapabilities class based on the current capabilities of that browser and version. This section may be augmented if browsers that are not handled may be used to access your pages. In the machine.config file there is a note that this section can be updated by retrieving the latest browser capabilities information from http://www.cyscape.com/browsercaps. As of this writing, however, this site has no such update.

It can be useful, for testing, to change the browser type, even if you are using the same browser for testing. You can do this by using the ClientTarget attribute of the Page directive in your .aspx file. There are four predefined client targets ” ie4 , ie5 , uplevel , and downlevel ”and you can define additional targets by using the clientTarget element in your web.config file. By setting the ClientTarget to downlevel , as shown in Listing 8-12, you can easily test to see how your page will behave when accessed by an unknown browser type.

Listing 8-12 Setting the ClientTarget for a Page
 <%@ Page Language='C#' ClientTarget='downlevel' %> 

This ClientTarget value is accessible through the Page class as a read/write property. You should be aware, however, that the value is set only if you explicitly set it yourself either programmatically in your Page -derived class or as an attribute in your Page directive. You should never use the ClientTarget property to test for browser capabilities. Instead, you should always use the HttpBrowserCapabilities class, which will be filled in properly based either on the headers of the HTTP request or on the value of the ClientTarget attribute (which takes precedence). To add your own browser alias for testing to the ClientTarget attribute list, add a clientTarget element to your web.config file with an embedded add element containing the alias string you want to use and the userAgent string that would be submitted by the browser you want to test against. Listing 8-13 shows an example of defining an additional ClientTarget type in a web.config file for testing to see how a page will render in Netscape 6.0.

Listing 8-13 Adding a ClientTarget Alias to web.config
 <! web.config file > <configuration>   <system.web>   <clientTarget>     <add alias='nn6'          userAgent='Mozilla/5.0 (en-US;) Netscape6/6.0' />   </clientTarget>   </system.web> </configuration> 

8.1.6 Subproperties

In addition to exposing properties on a custom control, it is also possible to expose subproperties. Subproperties are a convenient way of gathering a set of common properties under a single, top-level property, with the potential for reuse in other controls as well. The syntax for setting subproperties is identical to the syntax used in traditional HTML, which means that it will be familiar to users of your control. For example, suppose we wanted to expose font properties used by our control for rendering text as subproperties, so that a client could set font attributes, such as Color and Size , through our single Font property, as shown in Listing 8-14.

Listing 8-14 Client Syntax for Setting Subproperties on a Custom Control
 <%@ Page Language='C#' %> <%@ Register Namespace='EssentialAspDotNet.CustomControls'              TagPrefix='eadn' Assembly='SubProperties' %> <html> <body> <eadn:SubPropertyControl runat='server' Name='Test'                          Font-Color='red' Font-Size='24'/> </body> </html> 

The mechanism for exposing subproperties in a custom control is to define a helper class that exposes all the properties you want to include as subproperties on your control, and then to add an instance of that class to your control class and expose it as a read-only property. Listing 8-15 shows a sample subproperty class that exposes two properties for controlling font rendering: Size and Color .

Listing 8-15 Sample Subproperty Class Exposing Font Properties
 public class FontFormat {   private int   m_size;   private Color m_color;   public FontFormat(int size, Color clr)   {     m_size  = size;     m_color = clr;   }   public int Size   {     get { return m_size;  }     set { m_size = value; }   }   public Color Color   {     get { return m_color;  }     set { m_color = value; }   } } 

Listing 8-16 shows how this class could be used to expose the properties of the FontFormat class as subproperties of a control, achieving the subproperty access syntax shown initially in Listing 8-14.

Listing 8-16 Exposing Subproperties in a Custom Control
 public class SubPropertyControl : Control {   private string m_Name;   private FontFormat m_Font =                      new FontFormat(3, Color.Black);   public string Name   {     get { return m_Name;  }     set { m_Name = value; }   }   public FontFormat Font   {     get { return m_Font; }   }   protected override void Render(HtmlTextWriter writer)   {     writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize, m_Font.Size.ToString());     writer.AddStyleAttribute(HtmlTextWriterStyle.Color,                              m_Font.Color.Name);     writer.RenderBeginTag(HtmlTextWriterTag.Span);     writer.Write(m_Name);     writer.RenderEndTag();   } } 

8.1.7 Inner Content

Although properties can be used to initialize all of the state in a control, it sometimes makes more syntactic sense to have the user specify control state through the contents of a control tag, as shown in Listing 8-17.

Listing 8-17 Specifying Control State through Using Inner Content
 <%@ Page Language='C#' %> <%@ Register Namespace='EssentialAspDotNet.CustomControls'              TagPrefix='eadn' Assembly='InnerContent' %> <html> <body>   <eadn:InnerContentControl runat='server'>     Inner content goes here   </eadn:InnerContentControl> </body> </html> 

To retrieve this inner content from within a custom control class, you need to understand what the ASP.NET page parser does when it encounters content inside the tag of a control. Looking back at Figure 8-1, you can see that the parser turns the contents of any control tag into child controls and adds them to the Controls collection for that control. If the contents of the tag are simple text or client-side HTML tags, an instance of a LiteralControl class is created to represent it on the server. Thus, in our case of building a custom control that wants to use its inner content as part of its rendering, we need to access the LiteralControl class in our Controls array and extract its text content.

An example of a control that uses its inner content as part of its rendering is shown in Listing 8-18. Note that it is important to determine whether there actually is any inner content before trying to access it. The HasControls() method returns true only if one or more controls are in your Controls collection. Furthermore, note that we check to see that the first control in the Controls array is in fact a LiteralControl before accessing its Text field.

Listing 8-18 Custom Control That Accesses Inner Content
 public class InnerContentControl : Control {   protected override void Render(HtmlTextWriter output)   {     if (HasControls())     {       output.RenderBeginTag(HtmlTextWriterTag.H1);       LiteralControl lc = Controls[0] as LiteralControl;       if (lc != null)         output.Write(lc.Text);       output.RenderEndTag();     }   } } 

8.1.8 Generating Client-Side Script

Often a control may want to generate client-side script in addition to static HTML as part of its rendering process. If the client browser supports DHTML, it is often possible to shift some of the behavior of a control to the client and thus avoid unnecessary round-trips. For example, suppose we wanted to build a control that rendered a tic-tac-toe game board, which allows the user to click in a square to add an X or an O in alternating fashion, as shown in Figure 8-2.

Figure 8-2. Tic-tac-toe Board Rendered with DHTML

graphics/08fig02.gif

A na ve approach to building a control that would render this board might be to add a client script block with a handler function to toggle the Xs and Os for the Onclick event, followed by the HTML table and cells that would generate the client-side events. This approach is shown in Listing 8-19.

Listing 8-19 Na ve Client Script Generation
 public class TicTacToe : Control {   protected override void Render(HtmlTextWriter output)   {     output.WriteLine("<script language=javascript>    ");     output.WriteLine("var g_bXWentLast;               ");     output.WriteLine("function OnClickCell(cell) {    ");     output.WriteLine("  if (cell.innerText == ' ') {  ");     output.WriteLine("   if (g_bXWentLast)            ");     output.WriteLine("     cell.innerText = 'O';      ");     output.WriteLine("   else                         ");     output.WriteLine("     cell.innerText = 'X';      ");     output.WriteLine("   g_bXWentLast = !g_bXWentLast;");     output.WriteLine(" }                              ");     output.WriteLine(" else                           ");     output.WriteLine("   cell.innerText = ' ';        ");     output.WriteLine(" } </script>                    ");     /* additional style attributes not shown */     output.RenderBeginTag(HtmlTextWriterTag.Table);     for (int row=0; row<3; row++)     {       output.RenderBeginTag(HtmlTextWriterTag.Tr);       for (int col=0; col<3; col++)       {         output.AddAttribute(HtmlTextWriterAttribute.Onclick,                    "OnClickCell(this)");         output.RenderBeginTag(HtmlTextWriterTag.Td);         output.Write("&nbsp;");         output.RenderEndTag();       }       output.RenderEndTag();     }     output.RenderEndTag();   } } 

This control will work well as long as it is never used more than once on a single page. Placing multiple instances of this control on a page, however, causes two problems. The first problem is that the script block will be rendered twice to the page, which can cause undetermined behavior, especially since we are declaring a global script variable to keep track of whether an X or an O should be placed in the next square. The second problem is that our single global script variable, g_bXWentLast , will not have the correct value if a user clicks in one instance of our control and then another (it is easy to generate two Xs in a row on one board, for example). These two problems are indicative of problems that most controls encounter when they try to add client-side script as part of their rendering.

The first problem can easily be solved by using the helper function RegisterClientScriptBlock in the Page class. This function takes a pair of strings as parameters; the first string is a name for the script block, so that if a script block is ever registered multiple times on a given page, it will still be rendered only once. The second string is the actual script block you want included in the page. The best place to call this function is in a handler for the Init event of your control, as shown in Listing 8-20.

Listing 8-20 Calling RegisterClientScriptBlock
 protected override void OnInit(EventArgs e) {   Page.RegisterClientScriptBlock("MyScriptBlock",      "<script language=javascript>/*code here*/</script>"); } 

The second problem is maintaining client-side state on behalf of each instance of our control. This is a common problem for controls rendering client-side script and relying on a single set of functions to perform some client-side logic. It is important to plan for the scenario that someone places multiple instances of your control on a page, and not have the behavior become undefined. Fortunately, as covered earlier in this chapter, there is a unique identifier associated with each control that will always be a valid JavaScript identifier: ClientID . Thus, the solution to our tic-tac-toe control problem is to keep an associative array of Booleans, indexed by the ClientID of our control, thereby guaranteeing unique state for each instance of our control. The complete and correct implementation of our tic-tac-toe control is shown in Listing 8-21.

Listing 8-21 Correct Client-Side Script Generation
 public class TicTacToe : Control {   protected override void OnInit(EventArgs e)   {     string sCode = @"<script language=javascript>        var g_rgbXWentLast = new Object();        function OnClickCell(cell, idx) {          if (cell.innerText == \"" \"") {           if (g_rgbXWentLast[idx])              cell.innerText = 'O';           else              cell.innerText = 'X';           g_rgbXWentLast[idx] = !g_rgbXWentLast[idx];         }         else           cell.innerText = ' ';       }";     Page.RegisterClientScriptBlock("CellCode", sCode);   }   protected override void Render(HtmlTextWriter output)   {     output.RenderBeginTag(HtmlTextWriterTag.Table);     for (int row=0; row<3; row++)     {       output.RenderBeginTag(HtmlTextWriterTag.Tr);       for (int col=0; col<3; col++)       {         string clk = string.Format("OnClickCell(this, '{0}')", ClientID);         output.AddAttribute(HtmlTextWriterAttribute.Onclick, clk);         output.RenderBeginTag(HtmlTextWriterTag.Td);         output.Write("&nbsp;");         output.RenderEndTag();       }       output.RenderEndTag();     }     output.RenderEndTag();   } } 

8.1.9 System.Web.UI.WebControls.WebControl

Another class that derives from Control can be used as the base class for custom controls: WebControl . The WebControl class contains several additional fields, properties, and methods that are used commonly by all the controls in the WebControls namespace (such as Button and ListBox ). The most significant difference between deriving from Control and WebControl is that WebControl adds several properties to your class. Listing 8-22 shows the additional properties you will inherit if you use WebControl as your base class.

Listing 8-22 Additional Properties Defined in the WebControl Class
 public class WebControl : Control {   public virtual string      AccessKey   {get; set;}   public virtual Color       BackColor   {get; set;}   public virtual Color       BorderColor {get; set;}   public virtual BorderStyle BorderStyle {get; set;}   public virtual Unit        BorderWidth {get; set;}   public virtual string      CssClass    {get; set;}   public virtual bool        Enabled     {get; set;}   public virtual FontInfo    Font        {get;     }   public virtual Color       ForeColor   {get; set;}   public virtual Unit        Height      {get; set;}   public virtual short       TabIndex    {get; set;}   public virtual string      ToolTip     {get; set;}   public virtual Unit        Width       {get; set;}   //... } 

On the other hand, most controls will probably support only a subset of the additional properties defined by WebControl and may be better off defining the common properties explicitly rather than deriving from WebControl . If you are using Visual Studio.NET to build your custom control, it automatically derives your control from WebControl instead of Control . You should be aware of this, and either change the base class to Control if you do not plan on supporting most of the additional properties, or make sure you do support these additional properties (or explicitly disable them in your class).

For an example of a control that derives from WebControl and implements all the additional properties, consider the NameWebControl class shown in Listing 8-23. This class has a single custom property, Text , used to store a string to be rendered as a <span> element. This control is very similar to the Label control defined in the WebControls namespace.

Listing 8-23 Sample Control Deriving from WebControl
 public class NameWebControl : WebControl {   private string m_Text;   public string Text   {     get { return m_Text;  }     set { m_Text = value; }   }   protected override void Render(HtmlTextWriter writer)   {     writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, BorderColor.Name);     writer.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, BackColor.Name);     writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle,       BorderStyle.ToString());     writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth,       BorderWidth.ToString());     writer.AddStyleAttribute(HtmlTextWriterStyle.FontFamily, Font.Name);     writer.AddStyleAttribute(HtmlTextWriterStyle.FontSize,       Font.Size.ToString());     if (Font.Bold)       writer.AddStyleAttribute(HtmlTextWriterStyle.FontWeight, "bold");     if (Font.Italic)       writer.AddStyleAttribute(HtmlTextWriterStyle.FontStyle, "italic");     string decoration = "";     if (Font.Underline)       decoration += "underline";     if (Font.Overline)       decoration += " overline";     if (Font.Strikeout)       decoration += " line-through";     if (decoration.Length > 0)       writer.AddStyleAttribute(HtmlTextWriterStyle.TextDecoration, decoration);     writer.AddStyleAttribute(HtmlTextWriterStyle.Color,        ForeColor.Name);     writer.AddStyleAttribute(HtmlTextWriterStyle.Height,        Height.ToString());     writer.AddStyleAttribute(HtmlTextWriterStyle.Width,        Width.ToString());     writer.AddAttribute(HtmlTextWriterAttribute.Class,        CssClass);     writer.AddAttribute(HtmlTextWriterAttribute.Title,        ToolTip);     if (!Enabled)       writer.AddAttribute(HtmlTextWriterAttribute.Disabled,                           "true");     writer.RenderBeginTag(HtmlTextWriterTag.Span);     writer.Write(m_Text);     writer.RenderEndTag();   } } 

Because the WebControl class defines all these additional properties, using it as a base class for your control means that your control should respond if a client changes one of these properties. For our simple text control that rendered as a span, this was pretty straightforward because the <span> element supports all these properties natively. Applying all these properties to a more complex control (like our tic-tac-toe control) would require more thought, and in all likelihood , several of the properties would not make sense for our control. This is important to keep in mind, because when someone uses your control within Visual Studio .NET, they will be presented with a complete list of your properties and will probably expect them to have some effect on the appearance of your control. An example of this property list for our NameWebControl is shown in Figure 8-3.

Figure 8-3. Property Page for Web Control

graphics/08fig03.gif

If it does not make sense for your control to implement all the additional properties defined by WebControl , you have two options. First, you can choose to derive your control from the Control base class instead of WebControl , and define your own properties as needed. Alternatively, you can choose the subset of properties inherited from WebControl that make sense for your control to implement, and for the remainder of the properties, provide a virtual override that sets the Browsable attribute to false to hide them in the designer. We will discuss designer integration in more detail later in this chapter, but for now, Listing 8-24 shows an example of a control that derives from WebControl but has set the Browsable attribute to false for several of the inherited properties.

Listing 8-24 WebControl-Derived Class Disabling Some Inherited Properties
 public class PartialWebControl : WebControl {   [Browsable(false)]   public override Color BorderColor   { get {return base.BorderColor;} }   [Browsable(false)]   public override BorderStyle BorderStyle   { get {return base.BorderStyle;} }   [Browsable(false)]   public override Unit BorderWidth   { get {return base.BorderWidth;} }   // ... } 


Essential ASP.NET With Examples in C#
Essential ASP.NET With Examples in C#
ISBN: 0201760401
EAN: 2147483647
Year: 2003
Pages: 94
Authors: Fritz Onion

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