Chapter 8. Custom Controls
At its core, ASP.NET is a control-based architecture, defined in the following way:
-
A Page is a control.
-
Any control may contain child controls.
-
A control is rendered by rendering its contents and then iteratively rendering its children.
This architecture is analogous to the window architecture in the Win32 API. The desktop itself is a window (like the Page), and any window may contain child windows. Each window is rendered by first rendering its contents and then rendering the contents of its children. The rendering of a window on the desktop involves drawing pixels to a display, whereas the rendering of an ASP.NET control involves generating HTML to populate a portion of the response to an HTTP request. ASP.NET's control architecture is a completely extensible framework, and in this chapter, we look at techniques for building custom controls to create truly reusable Web components that encapsulate details of presentation and server interaction.
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
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朿ompliant tags. For example, it converts HTML 4.0杝tyle 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.
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 |
Values | ||
---|---|---|
BackgroundColor |
BackgroundImage |
BorderCollapse |
BorderColor |
BorderStyle |
BorderWidth |
Color |
FontFamily |
FontSize |
FontStyle |
FontWeight |
Height |
TextDecoration |
Width |
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"); }
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梐nd 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
A nave 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(" "); 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(" "); 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
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;} } // ... }8.2 State Management
As with most aspects of Web application development, building custom controls involves thinking carefully about state management. In this section, we explore two mechanisms for maintaining state in custom controls: using view state and explicitly handling post-back data.
8.2.1 ViewState
All the Web controls in the base class libraries retain their state across post-backs, and thus users will expect all controls they use to work this way. This means that any controls you develop should support state retention. Fortunately, ASP.NET helps you by providing a collection of name/value pairs accessible by any control through its ViewState property that is an instance of System.Web.UI.StateBag. Any name/value pair placed in this collection before the rendering of a page is stored in the hidden __VIEWSTATE input field, and when the page is next accessed via a POST request, the contents of the hidden __VIEWSTATE input field are parsed and used to reconstitute the ViewState collection.
As an example of retaining state using the ViewState collection, Listing 8-25 shows the Name control shown earlier rewritten to save and restore its two properties from the ViewState collection. This version of the control no longer maintains local fields to store the property values but relies instead on the ViewState collection to always have the current values for all its properties.
This technique of using the ViewState collection as the state repository for properties works well for any simple property. More complex properties (such as lists or arrays of data) need to be saved to and restored from ViewState more carefully, as we will see shortly. It is important to note that you should always initialize any elements your control depends on in your control's constructor to guarantee that they will be initialized to some default values if they are accessed before they are set. Alternatively, you could implement the get method of each property to conditionally check that there is a valid entry in the ViewState collection before returning that value, and if there is none, initialize it with some default value.
Listing 8-25 Name Control Rewritten to Use ViewState
public class NameControl : Control { public NameControl() { ViewState["IsImportant"] = true; ViewState["Name"] = ""; } public string Name { get { return (string)ViewState["Name"]; } set { ViewState["Name"] = value; } } public bool IsImportant { get { return (bool)ViewState["IsImportant"]; } set { ViewState["IsImportant"] = value; } } protected override void Render(HtmlTextWriter writer) { if (IsImportant) writer.RenderBeginTag(HtmlTextWriterTag.H1); else writer.RenderBeginTag(HtmlTextWriterTag.H2); writer.Write(Name); writer.RenderEndTag(); } }The effect of retaining our state across post-backs is that clients can now rely on our control retaining its state like all the other controls. Keep in mind that if the client explicitly disables ViewState at either the control or the Page level, the ViewState collection will be empty at the beginning of each request, and your control properties will always have their default values.
For an example of a client that depends on our control retaining its state across post-backs, consider the page shown in Listing 8-26. Notice that when the page is first accessed (when IsPostBack is false), the Name and IsImportant properties of the Name controls are initialized, but on subsequent post-backs to the same page (when IsPostBack is true), the control properties are not touched. Because our control is now saving its properties in the ViewState collection, the values of these properties will be properly restored.
Listing 8-26 Name Control Client Page Relying on Post-Back State Retention
<%@ Page Language='C#' %> <%@ Register Namespace='EssentialAspDotNet.CustomControls' TagPrefix='eadn' Assembly='name' %> <script runat='server'> protected void DoSubmit(object src, EventArgs e) { /* do something */ } protected void Page_Load(object src, EventArgs e) { if (!IsPostBack) // populate custom controls { m_nc1.Name = "Foo"; m_nc1.IsImportant = true; m_nc2.Name = "Bar"; m_nc2.IsImportant = false; } // if not IsPostBack, no need to repopulate // controls because their state will have been // retained } </script> <html><body> <form runat='server'> <eadn:NameControl id='m_nc1' runat='server' /> <eadn:NameControl id='m_nc2' runat='server' /> <br/> <asp:Button runat='server' text='submit' Onclick='DoSubmit'/> </form> </body></html>Using the ViewState collection works well for primitive types and for state that maps directly to properties. If you have more complex state in your control that can reasonably be represented only by using local data structures, it will be cumbersome to figure out how to use the ViewState to cache all your data structure state. Instead, you can override a pair of virtual methods defined in the Control base class, and manually populate and retrieve state from the ViewState stream. Any object that is serializable can be persisted to the ViewState stream, which includes all the standard collection classes in the base class libraries (and any of your own classes that are marked with the Serializable attribute).
For an example of a control that performs more sophisticated view state management, consider the BarGraph control shown in Listing 8-27. This control maintains three pieces of data internally: a list of strings, a list of doubles, and a single double instance. Its rendering involves showing a table consisting of color-filled span elements displaying the current values of the two lists, as shown in Figure 8-4. The values that populate these two lists are added when a client programmatically invokes the AddValue() method of the control, so there is no simple mapping between the state of this control and the ViewState property inherited from the Control class. Instead, the BarGraphControl class overrides the SaveViewState and LoadViewState functions to manually populate the object array to be serialized into the __VIEWSTATE field. The SaveViewState function is called when a control is asked to add what it wants to into the outgoing __VIEWSTATE field, and should return an array of objects to be serialized into the __VIEWSTATE field. In almost all cases, you will want to invoke the base class's version of SaveViewState and add the result into your locally constructed object array, as shown in the BarGraphControl's implementation. This will ensure that any state managed by the base class will also be saved. In the BarGraphControl's function, the three other pieces of state in the class are added to a locally constructed object array, and because each of the types added to the array is serializable, they will be successfully added to the view state stream. The LoadViewState function does the opposite梚t is called just before the Load event is fired, and it receives an object array as input used to rehydrate any local control state from the view state stream. Again, most implementations will want to call the base class version of LoadViewState with the first element of the array. In our BarGraphControl example, once the base class has been called, we load the state from each element of the array into our local fields.
Listing 8-27 BarGraph Control Performing Manual View State Management
public class BarGraphControl : Control { private ArrayList _dataDescriptions; private ArrayList _dataValues; private double _max = 0; protected override void LoadViewState(object savedState) { if (savedState != null) { // Load State from the array of objects that // was saved in SaveViewState object[] vState = (object[])savedState; if (vState[0] != null) base.LoadViewState(vState[0]); if (vState[1] != null) _dataDescriptions = (ArrayList)vState[1]; if (vState[2] != null) _dataValues = (ArrayList)vState[2]; if (vState[3] != null) _max = (double)vState[3]; } } protected override object SaveViewState() { object[] vState = new object[4]; vState[0] = base.SaveViewState(); vState[1] = _dataDescriptions; vState[2] = _dataValues; vState[3] = _max; return vState; } public BarGraphControl() { _dataDescriptions = new ArrayList(); _dataValues = new ArrayList(); } public void AddValue(string name, double val) { _dataDescriptions.Add(name); _dataValues.Add(val); if (val > _max) _max = val; } protected override void Render(HtmlTextWriter output) { output.RenderBeginTag(HtmlTextWriterTag.Table); foreach (object elem in _dataValues) { // rendering details omitted (see sample) } output.RenderEndTag(); //</table> } }Figure 8-4. BarGraph Control Rendering
8.2.2 Explicit Post-Back Data Handling
Using view state to retain control state across post-backs is necessary for controls that render themselves as HTML elements whose contents are not sent as part of a POST request (such as tables and spans). If you are building a control that renders itself using HTML elements whose contents are sent through a POST request, you can load the state of the control directly from the POST variable collection instead of further burdening the hidden __VIEWSTATE field.
For example, suppose you were building a control that rendered itself as an INPUT tag in HTML. The contents of INPUT tags within a FORM element are always sent back as part of a POST request, so instead of saving and loading your control's state to and from view state, you could tap directly into the POST body for its latest value. To do this, your control must implement the IPostBackDataHandler interface, shown in Listing 8-28.
Listing 8-28 IPostBackDataHandler Interface Definition
public interface IPostBackDataHandler { bool LoadPostData(string postDataKey, NameValueCollection postCollection); void RaisePostDataChangedEvent(); }For controls that implement this interface, the LoadPostData method will be invoked just before the Load event fires, and will contain the entire contents of the POST body in the NameValueCollection. The postDataKey string passed in will contain the unique identifier associated with your control, which can be used to index the postCollection to find the current value of your control within the POST variable collection. The result of this method should be true if you change the value of the control's state, false otherwise.
If your control wants to propagate change notifications through server-side events, you can use a method of IPostBackDataHandler called RaisePostDataChangedEvent that is called whenever you return true from your implementation of LoadPostData. To fire this event when your control's data has changed, you need to keep track of the last value of your control in addition to the current value. The easiest way to do this is by using view state to store the current value of your control, and then checking the value stored in view state against the value sent back as part of the POST request. If they are different, you know that the control was changed between the last request and the current request, and you should return true from LoadPostData to indicate so.
Listing 8-29 shows a custom control called CustomTextBox that implements IPostBackDataHandler. It renders itself as an INPUT tag and can therefore extract its value from the POST body whenever a request is processed. It also exposes a public event called TextChanged that is fired whenever the RaisePostDataChangedEvent is invoked. To ensure that this event is fired only when the contents of the INPUT control have changed, it stores its Text property in ViewState, and when it loads a new value in from the POST body, it checks to see whether it has changed and returns true or false accordingly.
Listing 8-29 A Control That Performs Explicit Post-Back Data Handling
public class CustomTextBox: Control, IPostBackDataHandler { public string Text { get { return (string) ViewState["Text"]; } set { ViewState["Text"] = value; } } public event EventHandler TextChanged; public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) { string presentValue = Text; string postedValue = postCollection[postDataKey]; if (presentValue == null || !presentValue.Equals(postedValue)) { Text = postedValue; return true; } return false; } public virtual void RaisePostDataChangedEvent() { if (TextChanged != null) TextChanged(this,e); } protected override void Render(HtmlTextWriter output) { output.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID); output.AddAttribute(HtmlTextWriterAttribute.Value, Text); output.RenderBeginTag(HtmlTextWriterTag.Input); output.RenderEndTag(); } }Many controls in the base class libraries implement IPostBackDataHandler, including TextBox, HtmlInputText, CheckBox, HtmlSelect, DropDownList, and so on. Because all the primitive HTML elements whose contents are propagated back within the body of a POST request already have control classes defined, it is unlikely that you will want to create your own control classes that implement IPostBackDataHandler very often. If you want to customize the behavior of one of these primitive control classes, it is often more convenient to create a new class that derives from the base class control implementation. The one other case where you may consider implementing this interface is for a composite control that contains several HTML controls in its rendering (such as a collection of INPUT controls). Controls that contain other controls, however, can usually be more conveniently implemented as composite controls, which are server controls that contain other server controls and which we discuss next.
8.3 Composite Controls
Many controls contain other controls as part of their definition梩hese are termed composite controls. By embedding other controls within them, custom controls can be used to define "chunks" of forms that can potentially be reused from many different pages, complete with their own properties, events, and methods. Composite controls are built by creating child controls and adding them to the Controls collection of the parent control. All creation of child controls should take place in an override of the virtual function CreateChildControls. This function is inherited from the Control base class and is called at the correct time to create child controls (after the Load event, just before rendering).
8.3.1 Creating Child Controls
Listing 8-30 shows a simple composite control that implements a calculator by using three TextBox controls, a Button control, and some LiteralControls intermingled. A LiteralControl is a simple control that renders its Text property and is especially useful when building composite controls for properly laying out the child controls. This control also shows an example of hooking up an event handler to a child control.
Listing 8-30 Composite Control Example
public class CalcComposite : Control, INamingContainer { TextBox _operand1; TextBox _operand2; TextBox _result; private void OnCalculate(Object sender, EventArgs e) { int res = Convert.ToInt32(_operand1.Text) + Convert.ToInt32(_operand2.Text); _result.Text = res.ToString(); } public int Result { get { return Convert.ToInt32(_result.Text); } } protected override void CreateChildControls() { _operand1 = new TextBox(); _operand2 = new TextBox(); _result = new TextBox(); Controls.Add(_operand1); Controls.Add(new LiteralControl(" + ")); Controls.Add(_operand2); Controls.Add(new LiteralControl(" = ")); Controls.Add(_result); Controls.Add(new LiteralControl("<br/>")); Button calculate = new Button(); calculate.Text = "Calculate"; calculate.Click += new EventHandler(this.OnCalculate); Controls.Add(calculate); } }
Notice also that the control shown in Listing 8-30 implements the INamingContainer interface. This is a marker interface (one with no methods) that indicates to the containing page that this control has child controls that may need to be in a separate namespace. If there is more than one instance of a composite control on a given page, it is important that the child controls of each composite control not have ID clashes. Anytime you have child controls in a custom control, you should be sure to add support for INamingContainer to avoid ID clashes in the rendered HTML.
You may find that in your composite control, you would like to manipulate some of your child controls during the Load event of your control. The CreateChildControls method is not called until after the Load event has fired, however, so if you attempt to access any child controls within a Load handler, you will find none. To guarantee that your child controls have been created, you can always call EnsureChildControls(). This function checks to see if the CreateChildControls function has been called yet (by checking the ChildControlsCreated Boolean), and if not, calls it for you. For example, the composite control shown in Listing 8-31 explicitly calls EnsureChildControls from within its Load handler before it tries to set one of the children's values.
Listing 8-31 Calling EnsureChildControls
public class CalcComposite : Control, INamingContainer { TextBox _operand1; TextBox _operand2; TextBox _result; protected void Page_Load(object src, EventArgs e) { EnsureChildControls(); _operand1.Text = "42"; } protected override void CreateChildControls() { _operand1 = new TextBox(); _operand2 = new TextBox(); _result = new TextBox(); //... } }
With a composite control, it may also be useful to retrieve a child control dynamically. This is easily done with the FindControl method, which takes the string identifier of the control and returns a reference to the control.
8.3.2 Custom Events
For a custom control to truly provide all the attributes of standard Web controls, it must be able to define and propagate events. You define events in a custom control by adding a public event data member of delegate type EventHandler. A client then attaches a method to the event handler, and it is up to the control to invoke that method whenever the event logically occurs. Listing 8-32 shows a modified version of our CalcComposite control with a custom event added. Note that it declares a public EventHandler member called MagicNumber to which clients can hook delegates. In our example, the EventHandler is invoked whenever the user calculates the value 42 with the calculator. Listing 8-33 shows a sample .aspx page that traps the MagicNumber event and populates a label in response.
Listing 8-32 Control with Custom Event Example
public class CalcComposite : Control, INamingContainer { // other members not shown public event EventHandler MagicNumber; // public event private void OnCalculate(Object sender, EventArgs e) { int res = Convert.ToInt32(_operand1.Text) + Convert.ToInt32(_operand2.Text); _result.Text = res.ToString(); if ((res == 42) && (MagicNumber != null)) MagicNumber(this, EventArgs.Empty); // Trigger event! } protected override void CreateChildControls() { // Other control creation not shown... Button calculate = new Button(); calculate.Text = "Calculate"; calculate.Click += new EventHandler(this.OnCalculate); this.Controls.Add(calculate); } }
Listing 8-33 Sample Custom Event Client
<%@ Page Language="C#" %> <%@ Register TagPrefix="eadn" Namespace="EssentialAspDotNet.CustomControls" Assembly="CalcComposite" %> <html> <script language="C#" runat=server> private void MyCtrl_OnMagicNumber(Object src, EventArgs e) { MagicNumberLabel.Text = "Magic number calculated!!"; } </script> <body> <form runat=server> <asp:Label id=MagicNumberLabel runat=server /> <eadn:SimpleComposite id="MyCtrl" OnMagicNumber="MyCtrl_OnMagicNumber" runat=server /> </form> </body> </html>8.4 User Controls
Like pages in ASP.NET, controls can also be defined mostly in code, mostly as tags on a page, or somewhere in between. So far, we have looked at defining controls entirely in code, but you can define something called a "user control" by using tags on a page as well. This is very convenient for composite controls, because it allows you to lay out your control's HTML tags on a page rather than programmatically.
To define a user control, you create a page with an .ascx extension and use the @Control directive where you would normally use an @Page directive in a standard .aspx page. The @Control directive takes the same attributes as the @Page directive. You then lay out the controls you want to appear in your user control, making sure not to include html, body, or form tags, because these will be supplied by the client. You can also add properties and events in a server-side script block. Listing 8-34 shows our calculator control rewritten as a user control. Notice that properties and methods are declared within the server-side script block as if we were inside our control class definition. This .ascx page will be compiled into a distinct control class when referenced by a client .aspx page.
Listing 8-34 A User Control
<%@ Control Description="A simple calculator" %> <asp:TextBox ID="Op1" runat=server/> + <asp:TextBox ID="Op2" runat=server/> = <asp:TextBox ID="Res" runat=server/> <br/> <asp:Button Text="Calculate" OnClick="OnCalculate" runat=server/> <script language="C#" runat=server> private void OnCalculate(Object sender, EventArgs e) { int res = Convert.ToInt32(Op1.Text) + Convert.ToInt32(Op2.Text); Res.Text = res.ToString(); } public int Result { get { return Convert.ToInt32(Res.Text); } } </script>User controls can be accessed from any .aspx page much like other custom controls. The one major difference is that the .ascx file must be accessible by the client page, and it must be referenced using the Src attribute of the @Register directive. If you need to load a user control dynamically, you can use the LoadControl method of the Page class, which takes the file name of the user control. When a user control is loaded programmatically, the name of the new control class is the name of the file containing the user control, replacing the "." with an underscore (_). For example, a user control written in file Foo.ascx would generate a new control class of type Foo_ascx when loaded with LoadControl. Listing 8-35 shows an example of an .aspx page that references two user controls. The first one, user1, is loaded using the @Register directive. The second, user2, is loaded programmatically using the LoadControl method of the Page class.
Listing 8-35 A Sample Client to a User Control
<%@ Page Language="C#" %> <%@ Register TagPrefix="uc1" TagName="UserControl1" Src="UserControl1.ascx" %> <html> <script runat=server> void Page_Load(Object sender, EventArgs e) { Control uc2 = LoadControl("UserControl2.ascx"); Page.Controls.Add(uc2); } </script> <body> <form runat=server> <uc1:UserControl1 id="UC1" runat=server/> </form> </body> </html>8.5 Validation and Data Binding
Depending on the features of your custom controls, it may make sense to add support for either validation or data binding. This section describes how to build controls that support these two capabilities.
8.5.1 Supporting Validation
You can add validation support for custom controls that collect information from the user. For a control to support validation, it must be annotated with the ValidationProperty attribute, indicating which of its public properties should be tested with the validation algorithm, and when rendered in HTML, the value attribute of the rendered HTML must equate to the value that is to be validated (for client-side validation). Listing 8-36 shows a sample control to which validation controls could be applied, and Listing 8-37 shows a sample client page applying a validation control to our CustomTextBox control.
Listing 8-36 A Control That Supports Validation
[ValidationProperty("Text")] public class CustomTextBox : Control { public CustomTextBox() { ViewState["Text"] = ""; } public string Text { get { return (string) ViewState["Text"]; } set { ViewState["Text"] = value; } } protected override void Render(HtmlTextWriter output) { output.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID); output.AddAttribute(HtmlTextWriterAttribute.Value, Text); output.RenderBeginTag(HtmlTextWriterTag.Input); output.RenderEndTag(); } }Listing 8-37 Client to Custom Control Supporting Validation
<%@ Page Language='C#' %> <%@ Register TagPrefix='eadn' Namespace='EssentialAspDotNet.CustomControls' Assembly='Validation' %> <html> <body> <form runat=server> <eadn:ValidatableControl id='ctb' runat='server' /> <asp:RequiredFieldValidator runat='server' ControlToValidate='ctb'>* </asp:RequiredFieldValidator> <input type='submit' value='submit' /> </form> </body> </html>8.5.2 Data-Bound Controls
Many controls in the WebControls hierarchy support the concept of data binding. You can add data binding support to your custom controls as well by exposing a DataSource property. When your control renders itself, it pulls data from the data source and uses it as part of its rendering.
To begin with, it is important to understand how clients expect data-bound controls to work. Typically, a client prepares a connection to a data source in the Load event of a Page class and connects that data source to the DataSource property that all data-bound controls provide. The data source could be a simple collection, such as an ArrayList; a forward-only data reader, such as an IDataReader; or a fully cached data source, such as a DataSet, DataView, or DataTable. As an author of data-bound controls, you need to accommodate all these possibilities. Because all the data-bound controls in the base class hierarchy retain their state across post-backs, clients expect any data-bound control you write to do the same, so that a control need be populated only the first time a page is accessed. Once the client has attached a data source to the control, she may need to set the DataTextField and DataValueField properties of the control to indicate which field from a tabular data source should be used. Note that if your control can render tabular data (rows with multiple columns), there is no need to support these properties. Finally, once the data source, DataTextField, and DataValueField properties have been correctly populated, the client calls DataBind(), at which point your control's OnDataBinding method is called. Your control should iterate over the attached data source, saving the data locally within itself so that when your Render() method is called, you have the data to display. This interaction between a client page and a data-bound control is shown in Figure 8-5.
Figure 8-5. Interaction between a Page and a Data-Bound Control
8.5.3 Implementing a Data-Bound Control
The first step in creating a data-bound control is to define a DataSource property. This involves adding a public property called DataSource to your class of type object. Next you need to override the virtual function OnDataBinding, inherited from the Control base class. Your implementation of OnDataBinding should iterate across the data source that was assigned through your control's property, saving the data to a local data structure for future rendering. Note that it is not safe to assume that the data source will be around during your Render method, so you must take this step to ensure that you save the data that your control needs to be able to render itself during the OnDataBinding method call.
If your control expects only a list of items, not a tabular set of data, to be bound to it, you should expose another property, called DataTextField. This field will contain the index into a rowset that the client expects you to dereference when you perform the data binding. If you have the capability of associating values with the data items, you can also expose a property called DataValueField (most of the data-bound list controls in the base class hierarchy expose both of these fields). You should also be as accommodating as possible in what your control supports for data sources. All the data binding controls in the base class hierarchy support binding to any class that supports the IEnumerable or IList interface, plus they support binding directly to DataSets or DataTables by locating the default Data Table within a DataSet and the default DataView within a DataTable.
Clients of data-bound controls expect the controls to retain their state across post-backs. This lets page authors populate a control once if IsPostBack is false, and avoid additional round-trips to the database if the page is posted to again. As the author of a data bound control, you are responsible for making sure that your control's state is retained across a post-back. Typically, you do this by using the ViewState mechanism described earlier in this chapter. Because data-bound controls usually need to persist collections of data into view state, it is typically most efficient to override the LoadViewState and SaveViewState methods in your control to explicitly populate and retrieve collections of data from view state. Keep in mind that clients can always disable view state on your control, so even if you suspect it will be inefficient for clients to rely on state retention for your control, you should leave that decision to the client and support it nonetheless for consistency.
Listing 8-38 shows a sample control that supports data binding to all the different data source types with state retention. It renders itself as an item list and caches the contents of the data source in an ArrayList. This control defines two helper functions that should be useful for any implementer of data-bound controls. The first is GetResolvedDataSource, which takes the data source as an object and returns a reference to an IEnumerable interface. This function accounts for the fact that the data source may be a collection class, an IDataReader, a DataView, a DataTable, or a DataSet, and returns the enumeration interface on the correct element of the data source. The second helper function is GetDataItem, which takes an item pointed to by an enumerator and indexes it with the m_DataTextField value if is a rowset, or simply returns the object as a string if not. This is necessary to accommodate items stored in simple collections and items stored in tabular data sets.
Listing 8-38 A Data-Bound Control
public class DataBoundControl : Control { private ArrayList _cachedData = new ArrayList(); private object _dataSource; private string _dataTextField; private string _dataValueField; public string DataTextField { get { return _dataTextField; } set { _dataTextField = value; } } public string DataValueField { get { return _dataValueField; } set { _dataValueField = value; } } public override object DataSource { get {return _dataSource;} set {_dataSource = value;} } public IEnumerable GetResolvedDataSource(object ds) { if (ds is IEnumerable) return (IEnumerable)ds; else if (ds is DataTable) return (IEnumerable)(((DataTable)ds).DefaultView); else if (ds is DataSet) { DataView dv = ((DataSet)ds).Tables[0].DefaultView; return (IEnumerable)dv; } else if (ds is IList) return (IEnumerable)((IList)ds); else return null; } protected string GetDataItem(object item) { string ret; if (item is DataRowView) { DataRowView drv = (DataRowView)item; ret = drv[_dataValueField].ToString(); } else if (item is DbDataRecord) { DbDataRecord ddr = (DbDataRecord)item; ret = ddr[_dataValueField].ToString(); } else ret = item.ToString(); return ret; } protected override void OnDataBinding(EventArgs e) { base.OnDataBinding(e); if (DataSource != null) { IEnumerable ds = GetResolvedDataSource(_dataSource); IEnumerator dataEnum = ds.GetEnumerator(); while (dataEnum.MoveNext()) _cachedData.Add(GetDataItem(dataEnum.Current)); } } protected override void Render(HtmlTextWriter htw) { htw.RenderBeginTag(HtmlTextWriterTag.Ul); // <ul> foreach (string s in _cachedData) { htw.RenderBeginTag(HtmlTextWriterTag.Li); // <li> htw.Write(s); htw.RenderEndTag(); // </li> } htw.RenderEndTag(); // </ul> } protected override void LoadViewState(object savedState) { if (savedState != null) { // Load State from the array of objects that // was saved in SaveViewState object[] vState = (object[])savedState; if (vState[0] != null) base.LoadViewState(vState[0]); if (vState[1] != null) _cachedData = (ArrayList)vState[1]; } } protected override object SaveViewState() { object[] vState = new object[2]; vState[0] = base.SaveViewState(); vState[1] = _cachedData; return vState; } }8.6 Designer Integration
Many controls are created with reuse in mind, whether it is across multiple applications within one company or perhaps for sale and distribution. Reusable controls should take advantage of designer integration so that they integrate into the Visual Studio .NET design environment. With designer integration you can control
How your control appears in the Toolbox
The appearance of the control's tag on the page
The appearance of the control's @Register directive on the page
How properties are categorized in the designer
Editable string formats for any complex property types
Custom GUIs for editing properties, if necessary
The designer view of your control
Adding designer integration to your control is the final, critical step in making your control accessible to developers who want to use it.
8.6.1 Properties and Appearance
When a control you build is used in Visual Studio .NET, the first thing users of your control will see is the cool 16-by-16 graphic that represents your control in the Toolbox梠r the lack thereof. To associate a Toolbox image with your control, you must create a bitmap that is 16 by 16 pixels. The lower left pixel of the bitmap is used as the transparent color when the control is rendered in the Toolbox. The file name of the bitmap must be the full class name of the control class with which it is associated. For example, if you have defined a control class named Control1 in the MyControls namespace, the bitmap file for that control must be named MyControls.Control1.bmp. Finally, the bitmap needs to be compiled into the control assembly as an embedded resource. You can do this in a Visual Studio .NET project by setting the Build Action of the bitmap file to Embedded Resource. Be aware that Visual Studio .NET prepends the default namespace associated with your project to any embedded resources, so you must either take that into consideration when naming your bitmap file to ensure that it matches the control class name, or remove the default namespace from your project altogether (accessible through the project properties dialog). In a command-line compilation, you can embed a bitmap as a resource by using the /resource switch referencing the bitmap file. The entire process of associating a Toolbox bitmap with your control is shown in Figure 8-6.
Figure 8-6. Setting the Toolbox Bitmap for Your Control
The second thing users of your control will notice is the tag that is placed into the .aspx page. You can control what this tag looks like through the class-level attribute ToolboxData. This attribute takes a string as a parameter that is the tag you would like rendered for your control, using the placeholder {0} wherever a tag prefix is used (which is also controllable through an assembly-level attribute). Listing 8-39 shows a custom control that specifies the tag that should be used through the TooboxData attribute. Note that you can specify any initial attributes or content you would like the tags for your control to have when they are first placed on a form.
Listing 8-39 Specifying a Client Tag for a Custom Control
[ToolboxData("<{0}:BarGraphControl runat='server'/>")] public class BarGraphControl : WebControl { /*...*/ }
In addition to the tag for your control, you can specify how the @Register directive should appear for your control when users place an instance of your control on their form for the first time. To do this, you specify an assembly-level attribute called TagPrefix, which takes two strings as parameters: the namespace of your control and the tag prefix to use when placing tags for your control on a form. Listing 8-40 shows an example of using the TagPrefix attribute to customize the @Register directive generated by Visual Studio .NET for a custom control.
Listing 8-40 Customizing the @Register Directive for a Custom Control
[assembly: TagPrefix("EssentialAspDotNet.CustomControls", "eadn")]
A number of attributes can be applied to properties of a control to influence their appearance and usage from within a designer. Table 8-5 shows a complete list of the attributes that apply to control properties. The two that should most often be applied are the Category and Browsable attributes, which control under which category a property should appear and whether it should appear at all.
Attribute | Values | Description |
---|---|---|
Bindable | BindableSupport. [Yes, No, Default] | Should this property be displayed in the DataBindings dialog? |
Category | Any string | Property category this property should appear under in the designer (Appearance, Behavior, ...) |
DefaultValue | Any value | The default value this property should take when the control is first created |
PersistenceMode | PersistenceMode. [Attribute, Default, EncodedInner DefaultProperty, InnerDefault Property] | Whether changes made to the value of this property are persisted (and how) |
Browsable | true, false | Whether this property is displayed in the designer |
TypeConverter | Type of a class that derives from TypeConverter | Class to use to convert the string in the designer to the type required by this property |
Editor | Type of a class that derives from UITypeEditor | Class to provide a custom interface for editing this property |
8.6.2 Type Converters
You can also define custom type converters if your control has complex properties that may need special UI support in the designer. Because properties for controls can be specified as attributes (by default) within a control tag on a form, all property types need a way to be converted to and from a string representation. For many properties, this happens automatically because there are several built-in converter classes, as shown in Table 8-6.
Converter Class |
---|
ArrayConverter |
BooleanConverter |
ByteConverter |
CharConverter |
CollectionConverter |
ComponentConverter |
CultureInfoConverter |
DateTimeConverter |
DecimalConverter |
DoubleConverter |
EnumConverter |
Int16Converter |
Int32Converter |
Int64Converter |
ReferenceConverter |
UInt16Converter |
UInt32Converter |
UInt64Converter |
ColorConverter |
WebColorConverter |
FontConverter |
For other property types, however, you need to provide a custom converter to allow clients to specify property values within a control tag. To provide a custom converter for a property in your control, you must create a new class deriving from System.ComponentModel.TypeConverter to perform the conversions, and you must associate that type converter class with your property by using the TypeConverter attribute. For example, suppose we built a control that exposed a composite property to set the attributes of a Dog, including its name, age, and breed. These attributes could be encapsulated into a structure and exposed as subproperties, as shown in Listing 8-41.
Listing 8-41 The Dog Structure
public enum eBreed { Dalmation, Labrador, GoldenRetriever, Mutt, BlackLabradorRetriever } public struct Dog { private string _name; private eBreed _breed; private int _age; public Dog(string name, eBreed breed, int age) { _name = name; _breed = breed; _age = age; } public string Name { get { return _name; } set { _name = value; } } public eBreed Breed { get { return _breed; } set { _breed = value; } } public int Age { get { return _age; } set { _age = value; } } }
If a custom control exposed the Dog structure as a property without adding a TypeConverter, users of this control would only be able to set the value of the dog property programmatically. For users to be able to manipulate the subproperties of the Dog structure through the designer, we must provide a way to convert the structure to and from a string representation. Listing 8-42 shows the implementation of DogConverter, a class that provides conversion between the Dog data type and its equivalent string representation (which, in this case, we have chosen to be "name, breed, age").
Listing 8-42 A Custom Type Converter for Dog
public class DogConverter : TypeConverter { public override bool CanConvertFrom( ITypeDescriptorContext context, Type destType) { if (destType == typeof(string)) return true; else return base.CanConvertFrom(context, destType); } public override bool CanConvertTo( ITypeDescriptorContext context, Type destType) { if (destType == typeof(InstanceDescriptor) || destType == typeof(string)) return true; else return base.CanConvertTo(context, destType); } public override object ConvertFrom( ITypeDescriptorContext context, CultureInfo culture, object value) { // Parse the string format, which is: name,breed,age string sValue = value as string; if (sValue != null) { string[] v = sValue.Split(new char[] {','}); return new Dog(v[0], (eBreed)Enum.Parse(typeof(eBreed), v[1]), Int32.Parse(v[2])); } else return base.ConvertFrom(context, culture, value); } public override object ConvertTo( ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { Dog dg = (Dog)value; if (destinationType == typeof(InstanceDescriptor)) { Type[] parms = new Type[]{typeof(string), typeof(eBreed), typeof(int)}; object[] vals = new object[] {dg.Name, dg.Breed, dg.Age}; return new InstanceDescriptor( typeof(Dog).GetConstructor(parms), vals); } else if (destinationType == typeof(string)) { return string.Format("{0},{1},{2}", dg.Name, dg.Breed, dg.Age); } return base.ConvertTo(context, culture, value, destinationType); } }
Note that four core methods must be overridden in a type converter. First, the CanConvertFrom and CanConvertTo methods indicate what types this class can convert from and to. In our case, the purpose of providing this class is to convert from a Dog to a string and back again. Second, the ConvertFrom method requests that the class perform a conversion from a given type to the real type, which in our case will always be from a string to a Dog. Finally, the ConvertTo method involves taking a reference to the real type and returning a conversion of it into the target type, which in our case will always be a string. The one additional requirement of the designer is that it be able to generate code to create an instance of your type. To provide this information, we must also support conversion to an InstanceDescriptor, which is a class that stores information on how to create an instance of a particular class so that the designer can correctly rehydrate your class from its persistent store. In our case, we need to provide an InstanceDescriptor that describes how to invoke the three-parameter constructor of our Dog structure, shown in our implementation of ConvertTo.
The last step is to apply this custom type converter to our Dog structure, which we do by using the TypeConverter attribute, as shown in Listing 8-43.
Listing 8-43 Applying a Type Converter to a Type
[TypeConverter(typeof(DogConverter))] public struct Dog { //... }
Once a property has a valid converter associated with it, the designer lets the user modify the property value through the property window, taking whatever string is passed in, running it through the converter, and assigning it to the control's property. If you would like the user to be able to edit the subproperties of your type individually, you can derive your type converter from a special derivative of TypeConverter called ExpandableObjectTypeConverter. Figure 8-7 shows what the property editor looks like when the DogConverter class is derived from ExpandableObjectTypeConverter.
Figure 8-7. Expandable Properties
8.6.3 Property Editors
Some control authors may want to take this one step further and provide their own custom editors for users to edit properties with. Instead of having users type a formatted string in the property editor, they can associate an editor with a property that can launch a form or dialog to edit the property. Several built-in editors are available, as shown in Table 8-7.
Editor Class |
---|
DataBindingCollectionEditor |
ImageUrlEditor |
UrlEditor |
XmlFileEditor |
XmlUrlEditor |
XslUrlEditor |
For an example of using a custom editor, consider a class that maintains a URL property. If would be nice if instead of having users type in any random string for this property, there were a selection dialog that helped them construct a proper URL. By using the Editor attribute on the string property used to access and set the URL, the control builder can associate the built-in UrlEditor class to do just this, as shown in Figure 8-8. Note that the Editor takes two parameters, the type of the editor class and the base class from which it inherits, which currently should always be System.Drawing.Design.UITypeEditor.
Figure 8-8. Associating a Custom Editor with a Control Property
To create your own custom editor for a property, you derive a new class from System.Drawing.Design.UITypeEditor and override the EditValue and GetEditStyle methods. The GetEditStyle returns an enumeration indicating what type of user interface the editor is going to use: modal dialog, drop-down box from within the property list, or none. EditValue takes a reference to the object it is editing and returns a new object with new values. What happens inside EditValue is completely up to you, but most often it invokes a modal dialog box with controls on it to edit the values for the object. Listing 8-44 shows an example of a custom editor for editing the Dog structure shown earlier and how to hook it up to the Dog structure using the Editor attribute.
Listing 8-44 A Custom Editor Example
public class DogEditor : UITypeEditor { public override object EditValue( ITypeDescriptorContext tdc, IServiceProvider sp, object obj) { Dog dg = (Dog)obj; // DogEditorDlg class now shown... DogEditorDlg dlg = new DogEditorDlg(); dlg.DogName.Text = dg.Name; dlg.DogAge.Text = dg.Age.ToString(); dlg.DogBreed.SelectedIndex = dlg.DogBreed.Items.IndexOf(dg.Breed.ToString()); if (dlg.ShowDialog() == DialogResult.OK) { return new Dog(dlg.DogName.Text, (eBreed)Enum.Parse(typeof(eBreed), dlg.DogBreed.SelectedItem.ToString()), Int32.Parse(dlg.DogAge.Text)); } else return obj; // no change } public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext tdc) { return UITypeEditorEditStyle.Modal; } } // To hook up this editor to the Dog structure: [Editor(typeof(DogEditor), typeof(UITypeEditor))] public struct Dog { /*...*/ }
8.6.4 Designers
For each control you create, you can create an accompanying designer class that determines how that control appears and behaves within the designer. For many controls, this is unnecessary because the Visual Studio .NET designer creates an instance of your control when it is placed on a form and asks it to render itself. For controls whose rendering depends on runtime data population or that have no visual presence on a form, however, it makes sense to have something show up when such controls are dropped on a form in the designer. Whether this means artificially populating a control with initial data or providing a "stub" rendering indicating that it is a placeholder depends on the type of control.
Creating a designer involves creating a new class derived from System.Web.UI.Design.ControlDesigner and overriding its GetDesignTimeHtml method. This method should return the HTML you want to represent your control at design time. The last step is to associate the designer with your control class by using the Designer attribute. Listing 8-45 shows a custom control class with an associated designer. This listing also demonstrates the common practice of placing the designer in a new namespace called Design within the control class's namespace.
Listing 8-45 A Custom Control with an Associated Designer Class
namespace Design { public class MyControlDesigner : ControlDesigner { public override string GetDesignTimeHtml() { return "<h3>MyControl in design mode!</h3>"; } } } [Designer(typeof(Design.MyControlDesigner))] public class MyControl : Control { /*...*/ }
Providing a designer for a control does not change the fact that an instance of your control is created in design mode; it merely changes how that control renders itself in a designer. For controls that want to use the standard control rendering in their designer, it is possible to reference the control instance from within the designer via the Component property of the ControlDesigner class. A good example of when this would be appropriate is in the BarGraph control presented earlier in this chapter. This control renders a bar graph with data populated at runtime, so at design time there is nothing to render. To give it a visual presence in the designer, we can access the allocated instance of the control, populate it with some artificial data, and return the result of invoking the control's render function (by calling the base class implementation of GetDesignTimeHtml). This technique is shown in Listing 8-46.
Listing 8-46 Using a Designer to Artificially Populate the BarGraph Control
public class BarGraphDesigner : ControlDesigner { bool bGetDesignTimeHtmlCalled = false; public override string GetDesignTimeHtml() { // populate with data the first time only if (!bGetDesignTimeHtmlCalled) { BarGraphControl ctrl = (BarGraphControl)Component; // add some artifical data ctrl.AddValue("Value 1", 10.0); ctrl.AddValue("Value 2", 20.0); ctrl.AddValue("Value 3", 30.0); ctrl.AddValue("Value 4", 40.0); bGetDesignTimeHtmlCalled = true; } return base.GetDesignTimeHtml(); } }
The other type of controls that require designers are those that have no runtime rendering or whose rendering depends on too many runtime elements, making rendering in design mode impossible. For these controls, a helper function called CreatePlaceHolderDesignTimeHtml creates a standard placeholder for a control. This method takes a string and renders a gray box with the string displayed at design time, and is used by controls such as the Xml control and the Repeater control in the base class libraries. Listing 8-47 shows a sample designer that renders a placeholder.
Listing 8-47 Rendering a Placeholder with a Designer
public class MyControlDesigner : ControlDesigner { public override string GetDesignTimeHtml() { return CreatePlaceHolderDesignTimeHtml("see me later"); } }
SUMMARY
ASP.NET defines an architecture for extending its existing controls with custom controls. You can write controls to encapsulate portions of your Web applications into truly reusable components by deriving from System.Web.UI.Control and overriding the virtual Render method. An instance of the HtmlTextWriter class is passed into Render and provides a collection of helper routines for generating HTML to the response buffer. Inside your control's Render implementation, you are free to query the capabilities of the current client and render your control's state differently for different browsers. You can also render client-side JavaScript to clients that support it as part of your control's rendering, shifting some work to the client where possible.
Developers of custom controls must be very aware of how the state of their control is managed. Control users expect controls to retain their state across post-backs, which typically means that a control must use the ViewState mechanism to save and restore its state. It is also possible to manually parse the contents of the POST body if your control's state is propagated that way.
Composite controls are controls that contain other controls as children and are often used to build pieces of a form. You create composite controls by overriding the virtual CreateChildControls method and implementing the tagging interface INamingContainer. Even more useful are user controls, which create composite control definitions from .ascx files, letting you lay out your composite control with a designer.
The last step in building a user-friendly control is to ensure that it integrates cleanly with the Visual Studio .NET designer. Controls can specify what their Toolbox bitmap should look like, how their tags should appear when placed on a page, how to edit their properties, and what the control should look like in design mode.