Custom Controls

 
Chapter 16 - User Controls and Custom Controls
bySimon Robinsonet al.
Wrox Press 2002
  

Custom controls go a step beyond user controls in that they are entirely self-contained in C# assemblies, requiring no separate ASP.NET code. This means that we don't need to go through the process of assembling a UI in an .ascx file. Instead, we have complete control over what is written to the output stream, that is, the exact HTML generated by our control.

In general, it will take longer to develop custom controls than user controls, because the syntax is more complex and we often have to write significantly more code to get results. A user control may be as simple as a few other controls grouped together as we've seen, whereas a custom control can do just about anything short of making you a cup of coffee.

To get the most customizable behavior for our custom controls we can derive a class from System.Web.UI.WebControls.WebControl . If we do this then we are creating a full custom control. Alternatively, we can extend the functionality of an existing control, creating a derived custom control. Finally, we can group existing controls together, much like we did in the last section but with a more logical structure, to create a composite custom control.

Whatever we create can be used in ASP.NET pages in pretty much the same way. All we need to do is to place the generated assembly somewhere where the web application that will use it can find it, and register the element names to use with the < %@ Register % > directive. I say 'somewhere' because there are two options: we can either put the assembly in the bin directory of the web application, or place it in the GAC if we want all web applications on the server to have access to it.

The < %@ Register % > directive takes a slightly different syntax for custom controls:

   <%@ Register TagPrefix="PCS" Namespace="PCSCustomWebControls"     Assembly="PCSCustomWebControls"%>   

We use the TagPrefix option in the same way as before, but we don't use the TagName or Src attributes. This is because the custom control assembly we use may contain several custom controls, and each of these will be named by its class, so TagName is redundant. In addition, since we can use the dynamic discovery capabilities of the .NET Framework to find our assembly we simply have to name it and the namespace in it that contains our controls.

In the example line of code above, we are saying that we want to use an assembly called PCSCustomWebControls.dll with controls in the PCSCustomWebControls namespace, and use the tag prefix PCS . If we have a control called Control1 in this namespace we could use it with the ASP.NET code:

   <PCS:Control1 Runat="server" ID="MyControl1"/>   

With custom controls it is also possible to reproduce some of the control nesting behavior such as we see in list controls:

   <asp:DropDownList ID="roomList" Runat="server" Width="160px">     <asp:ListItem Value="1">The Happy Room</asp:ListItem>     <asp:ListItem Value="2">The Angry Room</asp:ListItem>     <asp:ListItem Value="3">The Depressing Room</asp:ListItem>     <asp:ListItem Value="4">The Funked Out Room</asp:ListItem>     </asp:DropDownList>   

We can create controls that should be interpreted as being children of other controls in a very similar way to this. We'll see how to do this later in this section.

Custom Control Project Configuration

Let's start putting some of this theory into practice. We'll use a single assembly to hold all of the example custom controls in this chapter for simplicity, which we can create in Visual Studio .NET by choosing a new project of type Web Control Library . We'll call our library PCSCustomWebControls :

click to expand

Here I have created the project in C :\ ProCSharp\CustomControls . There is no need for the project to be created on the web server as with web applications, since it doesn't need to be externally accessible in the same way. Of course, we can create web control libraries anywhere , as long as we remember to copy the generated assembly somewhere where the web application that uses it can find it!

One technique we can use to facilitate testing is to add a web application project to the same solution. We'll call this application PCSCustomWebControlsTestApp . For now, this is the only application that will use our custom control library, so to speed things up a little we can make the output assembly for our library be created in the correct bin directory (this means that we don't have to copy the file across every time we recompile). We can do this through the property pages for the PCSCustomWebControls project:

click to expand

Note that we have changed the Configuration dropdown to All Configurations , so debug and release build will be placed in the same place. The Output Path has been changed to C:\Inetpub\ wwwroot \PCSCustomWebControlsTestApp\bin . To make debugging easier we can also change the Start URL option on the Debugging property page to http://localhost/PCSCustomWebControlsTestApp/WebForm1.aspx and the Debug Mode to URL , so we can just execute our project in debug mode to see our results.

We can make sure that this is all working by testing out the control that is supplied by default in the .cs file for our custom control library, WebCustomControl1 . We just need to make the following changes to the code in WebForm1.aspx , which simply reference the newly-created control library and embed the default control in this library into the page body:

 <%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false"          Inherits="PCSCustomWebControlsTestApp.WebForm1" %>   <%@ Register TagPrefix="PCS" Namespace="PCSCustomWebControls"     Assembly="PCSCustomWebControls"%>   <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML>    <HEAD>       <title>WebForm1</title>       <meta name="GENERATOR"content="Microsoft Visual Studio 7.0" >       <meta name="CODE_LANGUAGE"content="C#" >       <meta name="vs_defaultClientScript"content="JavaScript" >       <meta name="vs_targetSchema"             content="http://schemas.microsoft.com/intellisense/ie5" >    </HEAD>    <body MS_POSITIONING="GridLayout">       <form id="Form1" method="post" runat="server">   <PCS:WebCustomControl1 ID="testControl" Runat="server"     Text="Testing again..."/>   </form>    </body> </html> 

In fact, there is an even better way of doing this once the control library project is compiled. Add a new tab to the Toolbox called Custom Controls , right-click it and choose the Customize Toolbox... menu option. Next choose the .NET Framework Components tab and browse to the PCSCustomWebControls assembly. Once this is loaded you can choose controls from it in the list:

click to expand

Select WebCustomControl1 as shown above and the new control will appear in the toolbox ready for adding to our form:

click to expand

Also, add a project reference to PCSCustomWebControls in the test app:

click to expand

Now add a using statement to our PCSCustomWebControlsTestApp namespace in WebForm1.aspx.cs :

   using PCSCustomWebControls;   

This will enable us to use our custom controls from the code behind the form without full name qualification.

The nice thing about this is that if we add the control from the toolbox the < %@ Register % > directive is added automatically - although the tag prefix is assigned automatically. I got cc1 for mine when I tried it, which is fine although doing this ourselves gives us greater flexibility that could improve code readability.

Now, as long as we have the PCSCustomWebControls library configured as our startup application we can hit the Debug button to see our results:

click to expand

Basic Custom Controls

As can be inferred from the results in the last section, the sample control generated by default is simply a version of the standard < asp:Label > control. The code generated in the .cs file for the project, WebCustomControl1.cs , is as follows (omitting the standard and XML documentation comments):

   using System;     using System.Web.UI;     using System.Web.UI.WebControls;     using System.ComponentModel;     using PCSCustomWebControls;     namespace PCSCustomWebControls     {     [DefaultProperty("Text"),     ToolboxData("<{0}:WebCustomControl1 runat=server>                 </{0}:WebCustomControl1>")]     public class WebCustomControl1 : System.Web.UI.WebControls.WebControl     {     private string text;     [Bindable(true), Category("Appearance"), DefaultValue("")]     public string Text     {     get     {     return text;     }     set     {     text = value;     }     }     protected override void Render(HtmlTextWriter output)     {     output.Write(Text);     }     }     }   

The single class defined here is the WebCustomControl1 class (note how the class name mapped straight onto an ASP.NET element in the simple example we saw before), which is derived from the WebControl class as discussed earlier. Two attributes are provided for this class: DefaultProperty and ToolboxData . The DefaultProperty attribute specifies what the default property for the control will be if used in languages that support this functionality. The ToolboxData attribute specifies exactly what HTML will be added to an .aspx page if this control is added using the Visual Studio toolbox (as we saw above, once the project is compiled we can add the control to the toolbox by configuring the toolbox to use the assembly created). Note that a {0} placeholder is used to specify where the tag prefix will be placed.

The class contains one property: Text . This is a very simple text property much like those we've seen before. The only point to note here is the three attributes:

  • Bindable ( whether the property can be bound to data

  • Category ( where the property will be displayed in the property pages

  • DefaultValue ( the default value for the property

Exposing properties in this way works in exactly the same way as it did for custom controls, and is definitely preferable to exposing public fields.

The remainder of the class consists of the Render() method. This is the single most important method to implement when designing custom controls, as it is where we have access to the output stream to display our control content. There are only two cases where we don't need to implement this method:

  • Where we are designing a control that has no visual representation (usually known as a component )

  • Where we are deriving from an existing control and don't need to change its display characteristics

Custom controls may also expose custom methods , raise custom events, and respond to child controls (if any exist). We'll look at all of this in the remainder of this chapter, where we'll see how to:

  • Create a derived control

  • Create a composite control

  • Create a more advanced control

The final example will be a straw poll control, capable of allowing the user to vote for one of several candidates, and displaying voting progress graphically. Options will be defined using nested child controls, in the manner described earlier.

We'll start simply, though, and create a simple derived control.

The RainbowLabel Derived Control

For this first example we'll derive a control from a Label control and override its Render() method to output multicolored text. To keep the code for example controls in this chapter separate we'll create new source files as necessary, so for this control add a new .cs code file called RainbowLabel.cs to the PCSCustomWebControls project and add the following code:

   using System;     using System.Web.UI;     using System.Web.UI.WebControls;     using System.ComponentModel;     using System.Drawing;     namespace PCSCustomWebControls     {     public class RainbowLabel : System.Web.UI.WebControls.Label     {     private Color[] colors = new Color[] {Color.Red, Color.Orange,     Color.Yellow,     Color.GreenYellow,     Color.Blue, Color.Indigo,     Color.Violet};     protected override void Render(HtmlTextWriter output)     {     string text = Text;     for (int pos=0; pos < text.Length; pos++)     {     int rgb = colors[pos % colors.Length].ToArgb() & 0xFFFFFF;     output.Write("<font color='#" + rgb.ToString("X6") + "'>"     + text[pos] + "</font>");     }     }     }     }   

This class derives from the existing Label control ( System.Web.UI.WebControls.Label ) and doesn't require any additional properties as the inherited Text one will do fine. We have added a new private field, colors[] , which contains an array of colors that we'll cycle through when we output text.

The main functionality of the control is in Render() , which we have overridden as we want to change the HTML output. Here we get the string to display from the Text property and display each character in a color from the colors[] array.

To test this control we need to add it to the form in PCSCustomWebControlsTestApp :

 <form method="post" runat="server" ID="Form1">   <PCS:RainbowLabel Runat="server" Text="Multicolored label!"     ID="rainbowLabel1"/>   </form> 

This gives us:

Maintaining State in Custom Controls

Each time a control is created on the server in response to a server request it is created from scratch. This means that any simple field of the control will be reinitialized. In order for controls to maintain state between requests they must use the ViewState maintained on the client, which means we need to write controls with this in mind.

To illustrate this, we'll add an additional capability to the RainbowLabel control. We'll add a method called Cycle() that cycles through the colors available, which will make use of a stored offset field to determine which color should be used for the first letter in the string displayed. This field will need to make use of the ViewState of the control in order to be persisted between requests.

We'll show the code for both with and without ViewState storage cases to illustrate the trap that is all too easy to fall into. First we'll look at code that fails to make use of the ViewState :

 public class RainbowLabel : System.Web.UI.WebControls.Label    {       private Color[] colors = new Color[] {Color.Red, Color.Orange,                                             Color.Yellow,                                             Color.GreenYellow,                                             Color.Blue, Color.Indigo,                                             Color.Violet};   private int offset = 0;   protected override void Render(HtmlTextWriter output)       {          string text = Text;          for (int pos=0; pos < text.Length; pos++)          {   int rgb = colors[(pos + offset) % colors.Length].ToArgb()     & 0xFFFFFF;   output.Write("<font color='#" + rgb.ToString("X6") + "'>"                          + text[pos] + "</font>");             }       }       public void Cycle()   {     offset = ++offset;     }   } 

Here we initialize the offset field to zero, then allow the Cycle() method to increment it, using the % operator to ensure that it wraps round to 0 if it reaches 7 (the number of colors in the colors array).

To test this we need a way of calling cycle() , and the simplest way to do that is to add a button to our form:

 <form method="post" runat="server" ID="Form1">          <PCS:RainbowLabel Runat="server" Text="Multicolored label!"                            ID="rainbowLabel1"/>   <asp:Button Runat="server" ID="cycleButton"     Text="Cycle colors"/>   </form> 

Add an event handler by double-clicking on the button in design view and add the following code (you'll need to change the protection level to protected ):

   protected void cycleButton_Click(object sender, System.EventArgs e)   {   this.rainbowLabel1.Cycle();   } 

If you run this code you'll find that the colors change the first time you click the button, but further clicks will leave the colors as they are.

If this control persisted itself on the server between requests then it would work adequately, as the offset field would maintain its state without us having to worry about it. However, this technique wouldn't make sense for a web application, with thousands of users potentially using it at the same time. Creating a separate instance for each user would be counterproductive.

In any case, the solution is quite simple. We have to use the ViewState property bag of our control to store and retrieve data. We don't have to worry about how this is serialized, recreated, or anything else, we just put things in and take things out, safe in the knowledge that state will be maintained between requests in the standard ASP.NET way.

To place the offset field into the ViewState we simply use:

   ViewState["_offset"] = offset;   

ViewState consists of name-value pairs, and here we are using one called _offset . We don't have to declare this anywhere, it will be created the first time this code is used.

Similarly, to retrieve state we use:

   offset = (int)ViewState["_offset"];   

If we do this when nothing is stored in the ViewState under that name we will get a null value. Casting a null value in code such as the above will throw an exception, so we can either test for this or check whether the object type retrieved from ViewState is null before we cast it, which is what we'll do in our code.

In fact, we can update our code in a very simple way - simply by replacing the existing offset member with a private offset property that makes use of viewstate, with code as follows:

 public class RainbowLabel : System.Web.UI.WebControls.Label    {       ...   private int offset     {     get     {     object rawOffset = ViewState["_offset"];     if (rawOffset != null)     {     return (int)rawOffset;     }     else     {     ViewState["_offset"] = 0;     return 0;     }     }     set     {     ViewState["_offset"] = value;     }     }   ...    } 

This time, the control allows the Cycle() method to work each time.

In general, we might see ViewState being used for simple public properties, for example:

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

One further point about using the ViewState concerns child controls. If our control has children and is used more than once on a page, then we have the problem that the children will share their ViewState by default. In almost every case this isn't the behavior we'd like to see, and luckily we have a simple solution. By implementing INamingContainer on the parent control we force child controls to use qualified storage in the ViewState , such that child controls will not share their ViewState with similar child controls with a different parent.

Using this interface doesn't require any property or method implementation, we just need to say that we are using it, as if it were simply a marker for interpretation by the ASP.NET server. We'll need to do this in the next section.

Creating a Composite Custom Control

As a simple example of a composite custom control, we can combine the control from the last section with the cycle button we had in the test form.

We'll call this composite control RainbowLabel2 , and place it in a new file, RainbowLabel2.cs . This control needs to:

  • Inherit from WebControl (not Label this time)

  • Support INamingContainer

  • Possess two fields to hold its child controls

The code for these three things requires the following modifications to the code obtained by generating a new class file:

 using System;   using System.Web.UI;     using System.Web.UI.WebControls;     using System.ComponentModel;   namespace PCSCustomWebControls {   public class RainbowLabel2 : System.Web.UI.WebControls.WebControl,     INamingContainer   {   private RainbowLabel rainbowLabel = new RainbowLabel();     private Button cycleButton = new Button();   ... 

In order to configure a composite control we need to ensure that any child controls are added to the Controls collection and properly initialized . We do this by overriding the CreateChildControls() method and placing the required code there (here we should call the base CreateChildControls() implementation, which won't affect our class but may prevent unexpected surprises ):

   protected override void CreateChildControls()     {     cycleButton.Text = "Cycle colors.";     cycleButton.Click += new System.EventHandler(cycleButton_Click);     Controls.Add(cycleButton);     Controls.Add(rainbowLabel);     base.CreateChildControls();     }   

Here we just use the Add() method of Controls to get things set up correctly. We've also added an event handler for the button so that we can make it cycle colors, which is achieved in exactly the same way as for other events. The handler is the now familiar:

   protected void cycleButton_Click(object sender, System.EventArgs e)     {     rainbowLabel.Cycle();     }   

This call simply makes the label colors cycle.

To give users of our composite control access to the text in the rainbowLabel child we can add a property that maps to the Text property of the child:

   public string Text     {     get     {     return rainbowLabel.Text;     }     set     {     rainbowLabel.Text = value;     }     }   

The last thing to do is to implement Render() . The base implementation of this method takes each control in the Controls collection of the class and tells it to render itself. Since Render() is a protected method it doesn't call Render() for each of these controls; instead it calls the public method RenderControl() . This has the same effect, because RenderControl() calls Render() , so we don't have to change any more code in the RainbowLabel class. To get more control over this rendering (for example in composite controls that output HTML around that generated by child controls) we can call this method ourselves:

   protected override void Render(HtmlTextWriter output)     {     rainbowLabel.RenderControl(output);     cycleButton.RenderControl(output);     }   

We just pass the HtmlTextWriter instance we receive to the RenderControl() method for a child, and the HTML normally generated by that child will be rendered.

We can use this control in much the same way as RainbowLabel :

 <form method="post" runat="server" ID="Form1">   <PCS:RainbowLabel2 Runat="server"     Text="Multicolored label composite"     ID="rainbowLabel2"/>   </form> 

This time a button to cycle the colors is included.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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