Writing a TextBox Control


To understand some of the more advanced development aspects of server control, it is useful to actually write a simple control like a textbox. Although this isn't the most exciting control to write, especially since it already exists, it clearly demonstrates some important aspects that control authors need to understand including:

  • Interacting with postback

  • Using viewstate

  • Raising events from a control

To render a textbox, the server control needs to output an HTML input element with a type attribute containing the value "text" . Using the WebControl as our control's base class makes this first task simple. We have to perform several main tasks :

  • Create a new class that derives from the WebControl class.

  • Implement a public constructor that calls the base class constructor specifying that our server control should output an input element.

  • Override the AddAttributesToRender method. This is called to allow derived classes to add attributes to the root element ( input ).

  • Add the type attribute with a value of text .

  • Add a name attribute whose value is derived from the UniqueID property. This property is used by ASP.NET to hold the unique id of each control. We have to output the name property containing this value since an HTML form will not postback the value entered into an input field without a name.

Here is the sourcecode that implements these steps:

  using System;   using System.Web;   using System.Web.UI;   using System.Web.UI.WebControls;   namespace WroxControls   {   public class MyTextBox : WebControl   {   public MyTextBox() : base("input")   {   }   protected override void AddAttributesToRender(HtmlTextWriter writer)   {   base.AddAttributesToRender(writer);   writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);   writer.AddAttribute(HtmlTextWriterAttribute.Type, "input");   }   }   }  

The implementation of the AddAttributesToRender method calls the base-class implementation. This is called since the base-class implementation will add various other attributes, such as style attributes, to your element depending on other properties that have been set in the base class.

Once compiled, you can use the following ASP.NET page to reference your control ( assuming the control is called MyTextBox and is compiled to the MyTextBox.dll assembly):

  <%@ Register TagPrefix="Wrox" Namespace="WroxControls"   Assembly="MyTextBox" %>   <html>   <body>   <form runat="server">   <H3>TextBox Control</H3>   <P>Enter a value: <Wrox:MyTextBox runat="server" />   <BR>   <ASP:Button Text="Postback" runat="server" />   </form>   </body>   </html>  

In this ASP.NET page we've declared an instance of the MyTextBox control, and a button within a form element so that we can cause a postback to occur. When viewed in the browser, this page initially renders as shown in Figure 18-18:

click to expand
Figure 18-18:

Enter some text into the textbox and click the Postback button. You will find that after the postback occurs, the textbox is blank again. This demonstrates that as a control developer, you have to do some work for a control to become intelligent .

For an intelligent textbox, you need to access the postback data submitted as part of a form. If data is present for the control, add a value attribute to the input element. Assuming you can populate a member variable _text with the postback for the textbox, you could add the following code to output a value attribute:

  public MyTextBox() : base ("input")   {   }   string _value;   protected override void AddAttributesToRender(HtmlTextWriter writer)   {   base.AddAttributesToRender(writer);   writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID);   writer.AddAttribute(HtmlTextWriterAttribute.Type, "input");   if (_value != null)   writer.AddAttribute("value", _value);   }  

To access postback data, a server control has to implement the IPostBackDataHandler interface.

The IPostBackDataHandler Interface

The IPostBackDataHandler interface has two methods :

 bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection); void IPostBackDataHandler.RaisePostDataChangedEvent() 

The LoadPostData method is called when postback occurs and a control has postback data. The method is called in sequence for all controls on a page that need to access postback data. If a control does not have any postback data (for example, if the control was disabled), the method is not called. A control can explicitly ask for this method to be called even if does not have postback, by calling the RegisterRequiresPostBack method of the Page class.

The LoadPostData call passes all of the postback data submitted in a form using a NameValueCollection . A control can access any of the data in this collection. To access the specific postback data item associated with a control, the postDataKey variable is used as the key into the collection. The value of this key field is the unique name assigned to the control either automatically by ASP.NET, or the value assigned by a user if an id attribute was specified in the control declaration.

The LoadPostData method returns a Boolean value. If true is returned, the RaisePostDataChangedEvent method will be called after the LoadPostData method has been called for all other server controls on a page with postback data. If a false value is returned, the RaisePostDataChangedMethod will not be called.

A control should return true in the LoadPostData method to raise an event as the result of certain data being present, or changes to data caused as a result of postback data. Events must be raised in the RaisePostDataChangedEvent method since raising events in LoadPostData will cause unpredictable results. Any event handler that caught and processed an event raised in LoadPostData would not be able to depend on consistent state being present in other controls, since such controls may not have initialized or updated their state based upon postback.

To make your textbox control intelligent, implement the IPostBackDataHandler interface as follows :

  bool IPostBackDataHandler.LoadPostData(string postDataKey,   NameValueCollection postCollection)   {   _value = postCollection[postDataKey];   return false;   }     void IPostBackDataHandler.RaisePostDataChangedEvent()   {   }  
Note

The NameValueCollection class is defined in the System.Collections.Specialized namespace, and so it has to be imported into any class files before this code will compile.

The LoadPostData method uses the postDataKey variable to copy the postback data for the control into the _value variable. Since this variable is already being used to render a value, if present, the textbox should now appear intelligent. We return false from LoadPostData since we're not yet interested in raising any events. For the same reason, the implementation of the RaisePostDataChangedEvent is also empty.

With these changes in place, enter a value into the textbox, and hit the Postback button to cause a roundtrip to the server. Any value entered will be remembered , since the value attribute will be emitted by our control, as seen in Figure 18-19:

click to expand
Figure 18-19:

If you look at the HTML output for this page, you'll see the value attribute, which is why the value redisplays correctly:

  <html>   <body>     <form name="_ctl0" method="post" action="mytextbox.aspx" id="_ctl0">   <input type="hidden" name="__VIEWSTATE"   value="dDwtMzM4NzUxNTczOzs+YFp06wUt7ZoMN2kDeJMiO1YBg9U=" />     <H3>TextBox Control (VB)</H3>   <P>Enter a value: <input id="name" name="name" type="input" />   <BR>     <input type="submit" name="_ctl1" value="Postback" />     <P>   <span id="status"></span>     </form>     </body>   </html>  

If you declared a couple more of MyTextBox controls in the ASP.NET page, entered values, and used the button to cause postback, you would see that they all remember their state, just as with other standard server controls in earlier chapters. Each declared server control has a unique name assigned to it, so each manages its own postback and renders itself correctly, without interfering with other controls. All control behavior added to the textbox is encapsulated and reusable. This makes server controls very powerful, and makes complex page development much simpler. Once individual controls are written, they take care of all the plumbing code to remember their state.

Raising Events from Controls

ASP.NET provides a powerful server-side event model. As a page is being created, server controls can fire events that are caused either by aspects of a client-side postback, or by controls responding to page code that is calling their methods or changing properties. These events can be captured in an ASP.NET page, or can be caught by other server controls.

ASP.NET server controls support events using delegates, just like any other .NET class does. With ASP.NET, the EventHandler delegate is used when defining most events. The standard definition for this delegate defines a method signature with two parameters “the first parameter of type object , and the second of type EventArgs “that you will have seen many times in event handlers like Page_Load . When a server control raises an event, the first parameter of the event method will contain a reference to the server control. Supplying a reference to the control that raised the event allows event methods to handle events from multiple controls.Depending on the parameters you need your controls to raise, you can replace the EventHandler delegate with a custom delegate.

To demonstrate events, we'll add an event to our textbox control that is raised when its value changes.

Events in Action

To support an event in the textbox control, first define a public event of the type EventHandler called TextChanged . The code in C# will look as follows:

  public event EventHandler TextChanged;  

In VB:

  Event TextChanged As EventHandler  

Next , change the LoadPostData implementation to always return true :

 bool IPostBackDataHandler.LoadPostData(string postDataKey,                                        NameValueCollection postCollection) {    _value = postCollection[postDataKey];  return true;  } 

This will cause the RaisePostDataChangedEvent to be called, from which it is safe to raise events caused by postback.

In this method, we use the delegate to raise a TextChanged event, assuming there are listeners (for example, the delegate is not null ):

  void IPostBackDataHandler.RaisePostDataChangedEvent()   {   if (TextChanged != null)   TextChanged(this, EventArgs.Empty);   }  

You're probably wondering if these changes will always raise a TextChanged event when postback occurs, even if the text has not changed. As it stands, this is precisely what will happen. We'll shortly refine the event handler to only call the event when the value actually changes. But for now, let's just see how events are raised.

To use the new event, declare an event handler called OnNamedChanged in the ASP.NET page:

  <script runat="server" language="C#">   private void OnNameChanged(object sender, EventArgs e)   {   status.Text = "Value changed to " + name.Text;   }   </script>  

To wire the event handler up to the text changed event, add an OnTextChanged attribute and specify the name of the method to be called when the event is fired (in this case, OnNameChanged ):

  <P>Enter a value: <Wrox:MyTextBox id="name" runat="server"   OnTextChanged="OnNameChanged" />  

Prefix an event name with 'On' to associate event handlers with server-control events in server control declarations. If your control had an additional event called TextInvalid , the event name would be OnTextInvalid .

The OnNameChanged event handler displays the value of text in a label field when the event is fired:

  <asp:Label runat="server" id="status" />  

For your event handler code to work, you have to declare a Text property on your server control that exposes the held value so it can be displayed:

  public string Text   {   get { return _value; }   }  

With all these changes in place, entering a value of Events in ASP.NET in the textbox and calling the Postback button will cause a message to appear below the Postback button, as shown in Figure 18-20:

click to expand
Figure 18-20:

The following code shows how to implement a server-side control that supports events in VB:

  Imports System   Imports System.Web   Imports System.Web.UI   Imports System.Web.UI.WebControls   Imports System.Collections.Specialized     Namespace WroxControls   Public Class MyTextBoxVB   Inherits WebControl   Implements IPostBackDataHandler     Public Sub New()   MyBase.New("input")   End Sub 'New     Public Event TextChanged As EventHandler   Private _value As String   Public ReadOnly Property Text As String   Get   Return _value   End Get   End Property     Protected Overrides Sub AddAttributesToRender(writer As HtmlTextWriter)   MyBase.AddAttributesToRender(writer)   writer.AddAttribute(HtmlTextWriterAttribute.Name, UniqueID)   writer.AddAttribute(HtmlTextWriterAttribute.Type, "input")   If Not (_value Is Nothing) Then   writer.AddAttribute("value", _value)   End If   End Sub     Function LoadPostData(postDataKey As String, _   postCollection As NameValueCollection) As Boolean   Implements IPostBackDataHandler.LoadPostData   _value = postCollection(postDataKey)   Return True   End Function     Sub RaisePostDataChangedEvent()   Implements IPostBackDataHandler.RaisePostDataChangedEvent   RaiseEvent TextChanged(Me, EventArgs.Empty)   End Sub   End Class   End Namespace  

The Framework Event Pattern

When supporting events, classes in the .NET Framework define a protected method called OnEventName , which actually raises the event. The reason for this is that it enables derived classes to perform event handling by overriding a method instead of attaching a delegate. This is simpler and more efficient. To follow this pattern for the TextChange event, you could implement the IPostBackDataHandler.RaisePostDataChangedEvent method as follows:

 void IPostBackDataHandler.RaisePostDataChangedEvent() {  OnTextChanged(EventArgs.Empty);  }  protected void OnTextChanged (EventArgs e)   {   if (TextChanged != null)   TextChanged (this, e);   }  

In this code, the RaisePostDataChangedEvent method calls the OnTextChanged method to raise the event. Derived classes that want to raise events can simply override the OnTextChanged method to catch the event, and optionally call the base-class implementation if they want other listeners to receive the event.

Causing Postback from Any Element “IPostBackEventHandler

In HTML, only the button and imagebutton elements can actually cause a postback to occur. When designing controls, you may want other elements of a control's user interface such as an anchor element to cause postback. You may also want a control's user interface to be able to raise different types of postback events, which the control can process to manipulate its user interface. For example, a calendar control may want to have previous-month and next-month events.

To show how to support postback events, we'll write a simple counter control. The control's user interface displays a counter (starting at 50) and provides two hyperlinks that enable you to increase or decrease the value (see Figure 18-21):

click to expand
Figure 18-21:

If you click Increase Number twice, the count would increase to 52, as shown in Figure 18-22:

click to expand
Figure 18-22:

If you click Decrease Number four times, the number goes down to 48 (see Figure 18-23):

click to expand
Figure 18-23:

This control is raising postback events to itself. To achieve this, a control must do two things:

  • Derive from the IPostBackEventHandler interface and implement the RaisePostBackEvent method.

  • Call the Page class' GetPostBackEventReference method to create some script code that can be rendered into a page to force a postback to occur. This of course means the browser must support JavaScript for this feature.

The IPostBackEventHandler class's RaisePostBackEvent method is called when a postback is caused by script code calling Page class's GetPostBackEventReference . The RaisePostBackEvent method accepts a single string parameter that can be used to determine what event has been raised. In the case of the counter control, we use the value inc to signal our counter should be increased, and dec to signal it should be decreased:

  public void RaisePostBackEvent(string eventArgument)   {   if (eventArgument == "inc")   Number = Number + 1;   if (eventArgument == "dec")   Number = Number  1;   }  

When a control renders its user interface, it calls the Page class's GetPostBackEventReference method passing itself as a parameter. The return value is a string containing JavaScript code that will cause a postback event to be raised when it is called. In our control, this JavaScript is placed into an anchor element, so when the user clicks the anchor, a postback occurs.

The following code shows how this method was used to generate the inc postback event:

  writer.Write("<a href=\"javascript:" +   Page.GetPostBackEventReference(this,"inc") +   "\"'>Increase Number</a>");  

Here is the complete sourcecode for the counter control:

  using System;   using System.Web;   using System.Web.UI;   namespace WroxControls   {   public class MyFirstControl : Control,   IPostBackEventHandler   {   public int Number   {   get   {   if (ViewState["Number"] != null)   return (int) ViewState["Number"];   return 50;   }   set   {   ViewState["Number"] = value;   }   }     public void RaisePostBackEvent(string eventArgument)   {   if (eventArgument == "inc")   Number = Number + 1;   if (eventArgument == "dec")   Number = Number  1;   }     protected override void Render(HtmlTextWriter writer)   {   writer.Write("The Number is " + Number.ToString() + " (");   writer.Write("<a href=\"javascript:" +   Page.GetPostBackEventReference(this,"inc") +   "\"'>Increase Number</a>");   writer.Write(" or ");   writer.Write("<a href=\"javascript:" +   Page.GetPostBackEventReference(this,"dec") +   "\">Decrease Number)</a>");   }   }   }  

This control demonstrates how to:

  • Derive and implement the IPostBackEventHandler

  • Use the Page.GetPostBackEventReference to raise two events

The next section in the chapter covers viewstate, so for now ignore the implementation of the Number property.

The control renders the following HTML:

  <html>   <body>     <form name="_ctl0" method="post" action="myfirstcontrol.aspx" id="_ctl0">   <input type="hidden" name="__VIEWSTATE"   value="dDwxMjc4NDMyNjExO3Q8O2w8aTwxPjs+O2w8dDw7bDxpPDE+Oz47bDx0PHA8bDxOdW1iZXI   7PjtsPGk8NDg+Oz4+Ozs+Oz4+Oz4+Oz6XT9F3HCi1voCIwAsaNszynaGM2w==" />     The Number is 48 (<a href="javascript:__doPostBack('_ctl1','inc')">Increase   Number</a> or <a href="javascript:__doPostBack('_ctl1','dec')">Decrease   Number)</a>     <input type="hidden" name="__EVENTTARGET" value="" />   <input type="hidden" name="__EVENTARGUMENT" value="" />   <script language="javascript">   <!--   function __doPostBack(eventTarget, eventArgument) {   var theform;   if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {   theform = document.forms["_ctl0"];   }   else {   theform = document._ctl0;   }   theform.__EVENTTARGET.value = eventTarget.split("$").join(":");   theform.__EVENTARGUMENT.value = eventArgument;   theform.submit();   }   // -->   </script>   </form>     </body>   </html>  

Notice how the following script block containing the __doPostBack function is automatically rendered into the output stream when an ASP.NET server control calls the Page.GetPostBackEventReference reference:

 <script language="javascript"> <!--    function __doPostBack(eventTarget, eventArgument) {       var theform;       if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {          theform = document.forms["_ctl0"];       }       else {          theform = document._ctl0;       }       theform.__EVENTTARGET.value = eventTarget.split("$").join(":");       theform.__EVENTARGUMENT.value = eventArgument;       theform.submit();    } // --> </script> 

This function is called by the script code returned from Page.GetPostBackEventReference :

 <a href="javascript:__doPostBack('_ctl1','inc')"> 

Now that we have covered handling postback and events, let's look at how a control can persist state during postback using viewstate.

Using ViewState

After an ASP.NET page is rendered, the page object, which created the page and all of its server controls, is destroyed . When a postback occurs, a new page and server-control objects are created.

When writing a server control you often need to store and manage state. Since a control is created and destroyed with each page request, any state held in object member variables will be lost. If a control needs to maintain state, it has to do so using another technique. As you have seen with the textbox control, one way of managing state is to use postback. When a postback occurs, any postback data associated with a control is made available to it via the IPostBackData interface. A control can therefore repopulate its class variables, making the control appear to be stateful.

Using postback data to manage the state of a control is a good technique when it can be used, but there are some drawbacks. The most obvious one is that only certain HTML elements, such as input , can use postback. If you had a label control that needed to remember its value, you couldn't use postback.

Also, postback is only really designed to contain a single item of data. For example, our textbox control needs to remember its last value so it can raise a TextChanged event when the value changes. To maintain this additional state, one option would be to use hidden fields. When a control renders its output, it could also output hidden fields with other values that need to be remembered. When a postback occurs, these values would be retrieved into the LoadPostData method. This approach would work for a single control, but could be problematic in cases where many instances of the same control were on a page (for example, what would you call the hidden fields? How could you ensure the names do not clash with names a page developer may have used?)

To resolve the problems of managing state ASP.NET has a feature called viewstate. In a nutshell , viewstate is a hidden input field that can contain state for any number of server controls. This hidden field is automatically managed for you, and as a control author you never need to access it directly.

Introducing the StateBag

All server controls have a ViewState property. This is defined in the Control class as the StateBag type, and allows server controls to store and retrieve values that are automatically round-tripped and recreated during a postback.

During the save state stage of a page, the ASP.NET Framework enumerates all server controls within a page and persists their combined state into a hidden field called __VIEWSTATE . If you view any rendered ASP.NET containing a form element, you will see this field:

  <input type="hidden" name="__VIEWSTATE" value="dDwtMTcxOTc0MTI5NDs7Pg==" />  

When a postback occurs, ASP.NET decodes the __VIEWSTATE hidden field and automatically repopulates the viewstate for each server control as they are created. This reloading of state occurs during the load state stage of a page for controls that are declared on an ASP.NET page.

If a control is dynamically created, either on a page or within another composite control, the state will be loaded at the point of creation. ASP.NET keeps track of what viewstate hasn't been processed, and when a new control is added to the Controls property of a Control (remember a page is a control), it checks to see if it has any viewstate for the control. If it has, it is loaded into the control at that point.

To see viewstate in action, change your textbox control to store its current value in viewstate, rather than the _value field. By doing this, when LoadPostData is called to enable the textbox control to retrieve its new value, you can compare it with the old value held in viewstate. Return true if the values are different. This will cause a TextChanged event to be raised in RaisePostDataChangedEvent . If the values are the same, return false , so that RaisePostDataChangedEvent is not called, and no event is raised.

The StateBag class implements the IDictionary interface, and for the most part is used just like the Hashtable class with a string key. All items stored are of the System.Object type, and thus, any type can be held in the viewstate, and casting is required for retrieving an item.

In the earlier textbox control, we used a _value string member variable to hold the current value of the textbox. Delete that variable and rewrite the property to use viewstate:

  public string Text   {   get   {   if (ViewState["value"] == null)   return String.Empty;   return (string) ViewState["value"];   }   set   {   ViewState["value"] = value;   }   }  

Since you've deleted the _value member variable and replaced it with this property, you need to change all references to it, with the Text property. You could directly reference the ViewState where you previously used _value , but it's good practice to use properties to encapsulate your usage of viewstate, making the code cleaner and more maintainable (for example, if you changed the viewstate key name used for the text value, you'd only have to do it in one place).

With this new property in place, you can revise the LoadPostData to perform the check against the existing value as discussed:

  bool IPostBackDataHandler.LoadPostData(string postDataKey,   NameValueCollection postCollection)   {   bool raiseEvent = false;   if (Text != postCollection[postDataKey])   raiseEvent = true;   Text = postCollection[postDataKey];   return raiseEvent;   }  

Before testing this code to prove that the TextChanged event is now only raised when the text changes, you need to make a small change to the ASP.NET page. As you'll recall from earlier, we have an event handler that sets the contents of a label to reflect our textbox value when TextChanged is raised:

  <script runat="server" language="C#">   private void OnNameChanged(object sender, EventArgs e)   {   status.Text = "Value changed to " + name.Text;   }   </script>  

The label control uses viewstate to remember its value. When a postback occurs, even if this event is not raised, the label will still display the text from the previous postback, making it look like an event was raised. So, to know if an event really was raised, reset the value of the label during each postback. You could do this within the page init or load events, but since the label uses viewstate to retain its value, you can simply disable viewstate for the control using the EnableViewState attribute as follows:

  <ASP:Label runat="server" EnableViewState="false" id="status" />  

During the save state stage of a page, the ASP.NET page framework will not persist viewstate for the controls with an EnableViewState property of false . This change to the page will therefore make the label forget its value during each postback.

Note

Setting EnableViewState to false does not prevent a control from remembering state using postback, as the state is rendered to the browser as the Text property of the control, and resubmitted during postback As such, should you need to reset the value of a textbox, you'd have to clear the Text property in a page's init or load event.

With all these changes made, if you enter a value of Wrox Press and press the Postback button, you will see that during the first postback our event is fired, and our label control displays the value (see Figure 18-24):

click to expand
Figure 18-24:

If you click the postback button again, the textbox control will use its viewstate to determine that the postback value has not changed, and it will not fire its TextChanged event. Since the label control does not remember its state, as viewstate was disabled for it, the value-changed message will not appear during the second postback since the label will default back to its original blank value (see Figure 18-25):

click to expand
Figure 18-25:

Our textbox control is now pretty functional for a simple control “it can remember its value during postback, can raise events when its text changes, and can have style properties applied in the same way as other Web controls using the various style attributes:

  <Wrox:MyTextBox id="name" runat="server"   BackColor="Green"   ForeColor="Yellow"   BorderColor="Red"   OnTextChanged="OnNameChanged" />  

More on Events

Any server control that derives from the Control base classes automatically inherits several built-in events that page developers can also handle:

  • Init : Called when a control has to be constructed and its properties have been set

  • Load :Called when a control's viewstate is available

  • DataBinding :Called when a control bound to a data source should enumerate its data source and build its control tree

  • PreRender :Called just before the UI of a control is rendered

  • Unload : Called when a control has been rendered

  • Disposed :Called when a control is destroyed by its container

These events behave just like any other event. For example, you could catch the PreRender event of the TextBox and restrict its length to seven characters , by adding an OnPreRender attribute to the control declaration:

  <P>Enter a value: <Wrox:MyTextBox id="name" runat="server"   BackColor="Green"   ForeColor="Yellow"   BorderColor="Red"   OnTextChanged="OnNameChanged"   OnPreRender="OnPreRender" />  

And an event handler that restricts the size of the TextBox value if it exceeds seven characters:

  private void OnPreRender(object sender, EventArgs e)   {   if (name.Text.Length > 7)   name.Text = name.Text.Substring(0,7);   }  

As a control author, you can also catch these standard events within your controls. Do this by either wiring up the necessary event wire-up code, or, as you've seen already, overriding one of these methods:

  • OnInit(EventArgs e)

  • OnLoad(EventArgs e)

  • OnDataBinding(EventArgs e)

  • OnPreRender(EventArgs e)

  • OnUnload(EventArgs e)

  • Disposed()

The default implementation of each of these methods raises the associated events listed earlier. For example, OnInit fires the Init event, and OnPreRender fires the PreRender event. When overriding one of these methods, you should call the base-class implementation of the method so that events are still raised, assuming that is the behavior you want:

 protected override void OnInit(EventArgs e) {  base.OnInit(e);  if (_text == null) _text = "Here is some default text"; } 

Event Optimization in C# Using the EventHandlerList

When an event is declared within a class definition, additional memory must be allocated for an object instance at runtime for the field containing the event. As the number of events a class supports increases, the memory consumed by each and every object instance increases . Assuming that a control supports ten events (six built-in and four custom events), and assuming an event declaration requires roughly 16 bytes of memory, each object instance will require 160 bytes of memory. If nobody is interested in any of these events, this is a lot of overhead for a single control.

To only consume memory for events that are in use, ASP.NET controls can use the EventHandlerList class. The EventHandlerList is an optimized list class designed to hold delegates. The list can hold any number of delegates, and each delegate is associated with a key. The Control class has an Events property that returns a reference to an instance of the EventHandlerList . This instantiates the class on demand, so if no event handlers are in use, there is almost no overhead:

  protected EventHandlerList Events   {   get   {   if (_events == null)   _events = new EventHandlerList();   }   return _events;   }  

The EventHandlerList class has two main methods:

  void AddHandler(object key, Delegate handler);  

And:

  void RemoveHandler(object key, Delegate handler);  

AddHandler is used to associate a delegate (event handler) with a given key. If the method is called with a key for which a delegate already exists, the two delegates will be combined and both will be called when an event is raised. RemoveHandler simply performs the reverse of AddHandler .

Using the Events property, a server control should implement support for an event using a property declared as the type event:

  private static readonly object _textChanged = new object();   public event EventHandler TextChanged   {   add { Events.AddHandler(EventPreRender, value); }   remove { Events.RemoveHandler(EventPreRender, value); }   }  

Since this property is declared as an event, you have to use the add and remove property accessor declarations, rather than get and set . When add or remove are called, the value is equal to the delegate being added or removed, so we use this value when calling AddHandler or RemoveHandler .

Note

As Visual Basic .NET does not support the add / remove accessor, you can't use optimized event handlers in Visual Basic .NET.

To create a unique key for your events, which will not clash with any events defined in your base classes, define a static, read-only member variable called _textChanged , and instantiate it with an object reference. You could use other techniques for creating the key, but this approach adds no overhead for each instance of the server control, and is also the technique used by the built-in ASP.NET server controls. By making the key value static, there is no per-object overhead.

Checking and raising an event using the Events property is done by determining if a delegate exists for the key associated with an event. If it does, you can raise it to notify one or more subscribed listeners:

  void IPostBackDataHandler.RaisePostDataChangedEvent()   {   EventHandler handler = (EventHandler) Events[_textChanged];   if (handler != null)   handler(this, EventArgs.Empty);   }  

Using the EventHandler technique, a control can implement many events without causing excessive overhead for controls that do not have any event listeners associated with them. Since the Control class already implements most of the work for you, it makes sense to always implement your events in this way.

Tracking ViewState

When adding and removing items from viewstate, they are only persisted by a control if its viewstate is being tracked. This tracking only occurs after the initialization phase of a page is completed. This means if a server control makes any changes to itself or to another control before this phase, and the OnInit event has been raised, the changes will not be saved.

Types and ViewState

We mentioned earlier that the StateBag method used for implementing viewstate allows any type to be saved and retrieved from it. While this is true, this does not mean that you can use any type with it. Only types that can be safely persisted can be used. As such, types that maintain resources such as database connections or file handles should not be used.

ViewState is optimized and designed to work with the following types:

  • Int32 , Boolean , String , and other primitive types

  • Arrays of Int32 , Boolean , String , and other primitive types

  • ArrayList , Hashtable .

  • Types that have a type converter . A type converter is a class derived from System.ComponentModel.TypeConverter that can convert one type into another. For example, the type converter for the Color class can convert the string red into the enumeration value for red. ASP.NET requires a type converter that can convert a type to and from a string.

  • Types that are serializable ( marked with the serializable attribute, or support the serialization interfaces).

  • Pair , Triplet (defined in System.Web.UI , and respectively hold two or three of the other types listed).

ViewState is converted from these types into a string by the Limited Object Serialization ( LOS ) formatter class ( System.Web.UI.LosFormatter ).

The LOS formatter used by ASP.NET encodes a hash code into viewstate when a page is generated. This hash code is used during postback to determine if the static control declarations in an ASP.NET page have changed (for example, the number and ordering of server controls declared within an ASP.NET page). If a change is detected , all viewstate is discarded, since viewstate cannot be reliably processed if the structure of a page has changed. This limitation stems from the fact that ASP.NET automatically assigns unique identifiers to controls, and uses these identifiers to associate viewstate with individual given controls. If a page structure changes, so do the unique identifiers assigned to controls, and therefore the viewstate-control relationship is meaningless. In case you're wondering, yes, this is one technical reason why ASP.NET only allows a page to postback to itself.

More on Object Properties and Template UI

Earlier, we discussed how the default control builder of a server control would automatically map sub- elements defined within a server-control declaration to public properties of that control. For example, consider the following server-control declaration:

 <Wrox:ICollectionLister id="SessionList" runat="server">    <HeadingStyle ForeColor="Blue">       <Font Size="18"/>    </HeadingStyle>    <ItemStyle ForeColor="Green" Font-Size="12"/> </Wrox:ICollectionLister> 

The control builder of the ICollectionLister control shown here would try to initialize the HeadingStyle and ItemStyle object properties, determining the type of the object properties by examining the meta data of the ICollectionLister class using reflection. As the HeadingStyle element in this example has a Font sub-element , the control builder would determine that the HeadingStyle object property has an object property of Font .

Using the ICollectionLister Server Control

The ICollectionLister server control is a simple composite control that can enumerate the contents of any collection class implementing the ICollection . For each item in the collection, it creates a Label control, and sets the text of the label using the ToString method of the current item in the collection. This causes a linebreak because for each item in the collection, the label starts with <BR> . The control also has a fixed heading of ICollection Lister Control which is also created using a label control.

The ICollectionLister control has three properties:

  • DataSource : A public property of the ICollection type. When CreateChildControls is called, this property is enumerated to generate the main output of the control.

  • HeadingStyle : A public property of the Style type. This allows users of the control to specify the style attributes used for the hard-coded heading text. The Style.ApplyStyle method is used to copy this style object into the Label control created for the header.

  • ItemStyle : A public property of the Style type. This allows users of the control to specify the style attributes used for each of the collections that is rendered. The Style.ApplyStyle method is used to copy this style object into the Label control created for each item.

The code for this server control is shown here:

  using System;   using System.Web;   using System.Web.UI;   using System.Collections;   using System.Web.UI.WebControls;   namespace WroxControls   {   public class ICollectionLister : WebControl, INamingContainer   {   ICollection _datasource;   public ICollection DataSource   {   get { return _datasource; }   set { _datasource = value; }   }     Style _headingStyle = new Style();   public Style HeadingStyle   {   get{ return _headingStyle; }   }     Style _itemStyle = new Style();   public Style ItemStyle   {   get{ return _itemStyle; }   }     protected override void CreateChildControls()   {   IEnumerator e;   Label l;     // Create the heading, using the specified user style   l = new Label();   l.ApplyStyle(_headingStyle);   l.Text = "ICollection Lister Control";   Controls.Add(l);     // Create a label for each key/value pair in the collection   if (_datasource == null)   throw new Exception("Control requires a datasource");     e = _datasource.GetEnumerator();   while(e.MoveNext())   {   l = new Label();   l.ApplyStyle(_itemStyle);   l.Text = "<BR>" + e.Current.ToString();   Controls.Add(l);   }   }   }   }  

There is nothing new in this code that hasn't already been discussed. Refer to Chapter 15 for an explanation of using IEnumerator and ICollection .

The following ASP.NET page uses the ICollectionLister control to list the contents of a string array. This array is created in the Page_Load event and associated with a server control which has been given a name/Id of SessionList in this page:

  <%@ Register TagPrefix="Wrox" Namespace="WroxControls"   Assembly="DictionaryLister" %>   <script runat="server" language="C#">   void Page_Load(object sender, EventArgs e)   {     string[] names = new string[3];   names[0] = "Richard";   names[1] = "Alex";   names[2] = "Rob";   SessionList.DataSource = names;   }   </script>     <Wrox:ICollectionLister id="SessionList" runat="server">   <HeadingStyle ForeColor="Blue">   <Font Size="18"/>   </HeadingStyle>     <ItemStyle ForeColor="Green" Font-Size="12"/>   </Wrox:ICollectionLister>  

The output from this page (if viewed in color) is a blue header with green text for each item in the collection:

click to expand
Figure 18-26:

For controls that have fixed style and layout requirements, initializing them using object properties as we have in the ICollectionLister control is a good approach. You will have seen the same approach used throughout the standard ASP.NET server controls, such as the data grid and data list. However, for a control to provide ultimate flexibility, it's better to enable the user of the control to define what the UI of a control looks like by using templates. You've seen this in earlier chapters, with controls such as the data grid.

Using Templates

As you saw in Chapter 7, templates allow the users of a control to define how chunks of its UI “such as the header or footer “should be rendered.

Templates are classes that implement the ITemplate interface. As a control developer, you declare public properties of the ITemplate type to support one or more templates. When the default control builder sees a property of this type, it knows to dynamically build a class that supports the ITemplate interface, which can be used to render the section of UI the template defines.

Supporting template properties in a server control is relatively straightforward, although when using them within a data-bound control, things can initially seem a little complex, since the creation of child controls has to be handled slightly differently.

Let's introduce templates by rewriting the ICollectionLister control to support a heading and item template. Make the following changes to your code:

  • Change the HeadingStyle and ItemStyle properties to the ITemplate type.

  • Make the HeadingStyle and ItemStyle properties writeable . This has to be done since the objects implementing the ITemplate interface are dynamically created by the ASP.NET page and then associated with the server control.

  • Use the TemplateContainer attribute to give the control builder a hint about the type of object within which your templates will be instantiated . This reduces the need for casting in databinding syntax.

The changed code is shown here:

  ITemplate _headingStyle;   [TemplateContainer(typeof(ICollectionLister))]   public ITemplate HeadingStyle   {   get{ return _headingStyle; }   set{ _headingStyle = value; }   }   ITemplate _itemStyle;   [TemplateContainer(typeof(ICollectionLister))]   public ITemplate ItemStyle   {   get{ return _itemStyle; }   set{ _itemStyle = value; }   }  

At runtime, if a user specifies a template, the properties will contain a non-null value. Null means no template has been specified.

The ITemplate interface has one method called InstantiateIn . This method accepts one parameter of the type Control . When called, this method populates the Controls collections of the control passed in with one or more server controls that represent the UI defined within a template by a user. Any existing controls in the collection are not removed, so you can instantiate a template against another server control one or more times.

Note

A server control could use the Page class' LoadTemplate method (string filename) to dynamically load templates, but this is not recommended. It is very slow and is known to be unreliable. If you need dynamic templates, you should write your own class that implements the ITemplate interface.

Using the InstantiateIn method, you can change the CreateChildControls to use your new template properties to build the server controls for the header and each item. Since we're not supporting the data-binding syntax yet, the UI created for each item in the collection will not contain any useful values.

In the following code, the InstantiateIn method is called only if a template is not null. If a template is null, we throw an exception to let the user know the control needs a data source:

  protected override void CreateChildControls()   {   IEnumerator e;   if (_headingStyle != null)   _headingStyle.InstantiateIn(this);   if (_datasource == null)   throw new Exception("Control requires a data source");   e = _datasource.GetEnumerator();   while(e.MoveNext())   {   if(_itemStyle != null)   _itemStyle.InstantiateIn(this);   }   }  

With the new template properties and revised CreateChildControls , you can now declare a page that uses templates to style your controls UI. Here is a basic example that uses a <H3> element for the heading, and some bold text for each item (remember we're not showing the item value yet):

  <Wrox:ICollectionLister id="SessionList" runat="server">   <HeadingStyle>   <h3>ICollection Lister</H3>   </HeadingStyle>     <ItemStyle>   <BR><Strong>An item in the collection</Strong></BR>   </ItemStyle>   </Wrox:ICollectionLister>  

With these changes, the UI will now render as shown in Figure 18-27:

click to expand
Figure 18-27:

Although not visually stunning, these changes allow the UI of our control to be completely controlled and changed by the user in their declaration of our server control. As you've seen in previous chapters, this is a very powerful technique.

Your controls template support can use data-binding syntax without any additional changes. However, it is limited to the data it can access. You can access public properties or methods on the page within which the control is declared, or any public property or method of any other server control you have access to. For example, if you had a Name public property declared in your ASP.NET page, you could bind your item template to this using the databinding syntax introduced in Chapter 7:

 <ItemStyle> <BR><Strong>An item in the collection:  <%#Name%>  </Strong></BR> </ItemStyle> 

When this expression is evaluated, ASP.NET will try and locate the Name property on the naming container first (the control in which the template was instantiated in this case); if it's not found there, it will check the ASP page. Assuming you defined this property to return a Templates Rock string, you'd see Figure 18-28 as the output from the control:

click to expand
Figure 18-28:

To bind to a text field called mylabel declared within the same page, use the following syntax:

  <ItemStyle>   <BR><Strong>An item in the collection: <  %#mylabel.Text%  ></Strong></BR>   </ItemStyle>  

To bind to the naming container in which the template is instantiated, use the Container. syntax:

  <ItemStyle>   <BR><Strong>An item in the collection: <  %#Container.DataItem%  ></Strong></BR>   </ItemStyle>  

Using the last syntax, you could be forgiven for thinking you could enable the item template to access the current collection item being enumerated. To achieve this, it looks as if you'd simply add a public object property to your DataItem server control:

  object _dataitem;   public object DataItem   {   get{ return _dataitem; }   }  

After this, set that property to the current item being enumerated in the loop that instantiates the item template, as follows:

  ...   e = _datasource.GetEnumerator();   while(e.MoveNext())   {   if (_itemStyle != null)   {   // Set the current item   _dataitem = e.Current;   _itemStyle.InstantiateIn(this);   }   }   ...  

But if you made these changes and compiled them, you'd encounter an interesting problem, as seen in Figure 18-29:

click to expand
Figure 18-29:

Each template item instantiated has the same value! This occurs because the data binding for controls instantiated using a template, or just added using Controls.Add by hand, are not data bound unless the parent control is data-bound. This means your collection has already been enumerated, and the DataItem will always point to the last item in the collection, as the controls instantiated by the item templates are data-bound. To resolve this problem, instantiate your template on a control that has a DataItem property that holds the correct value. This control will not render any UI, and will do very little except expose the DataItem property:

  public class CollectionItem : WebControl, INamingContainer   {   object _dataitem;   public object DataItem   {   get{ return _value; }   }   public CollectionItem(object value)   {   _dataitem = value;   }   }  

The class derives from the WebControl since it will be the container for the controls instantiated by the item template. It also implements the INamingContainer interface to signal that it is a naming container for any child controls. This is important. Without this, the data-binding syntax would still refer to the parent control.

Using this new class, you can change your enumeration code to create an instance of the class for each item enumerated in the collection, passing in the current item being enumerated as a parameter to the constructor. The item template is then instantiated against the CollectionItem object created, before being added as a child control of the ICollectionLister .

Here is the revised section of the enumeration code:

  ...   e = _datasource.GetEnumerator();   CollectionItem item;   while(e.MoveNext())   {   if (_itemStyle != null)   {   item = new CollectionItem(e.Current);   _itemStyle.InstantiateIn(item);   Controls.Add(item);   }   }   ...  

The end result of these changes is that the server control hierarchy shown in Figure 18-30 will be created:

click to expand
Figure 18-30:

At the top of this diagram is the instance of the ICollectionLister control declared on the page. Assuming the DataSource associated with the control only contained two items, the CollectionItem object would have two CollectionItem child server controls. Each of these child server controls has a DataItem property that exposes the associated collection item, passed in via the constructor. The item template is instantiated within each of the CollectionItem objects, so its child controls vary depending on the "what's defined in the item" template. However, any data-binding using the Container syntax will always refer back to its parent CollectionItem , and therefore the correct DataItem .

Your item template is now being instantiated within the CollectionItem class. Thus, you have to update the TemplateContainer attribute declared on the ItemTemplate property to reflect this. Without this, ASP.NET would throw a cast exception when evaluating a data-binding expression:

  ITemplate _itemStyle;   [TemplateContainer(typeof(CollectionItem))]   public ITemplate ItemStyle   {   get{ return _itemStyle; }   set{ _itemStyle = value; }   }  

With these changes in place, your UI renders each item in the collection and displays its value, as shown in Figure 18-31:

click to expand
Figure 18-31:

When implementing a control that supports templates, here are a few simple rules you should follow:

  • If you are going to instantiate a template more than once, do not instantiate it against the same instance of a control unless you have very good reason to do so. For example, if a template does not contain any data-binding syntax and you can ensure none of the controls will be assigned an id ( ids must always be unique within a naming container).

  • If a control will be supporting a header template, it should always support a footer template.

This is important for scenarios where a user may want to create an HTML table (or any other item that has a start element, several items, and then an end item).

  • Be consistent with the intrinsic controls, and follow the same naming conventions that they use.

DataBind and OnDataBinding

When a server control can be data-bound, it should support a couple of additional features:

  • The ability for the page developer to determine when (and if ) a control should data-bind itself This high degree of control over when a control accesses its data source, allows a page developer to optimize data source usage keeping it to a minimum.

  • The ability to recreate itself and all child controls during postback, without being connected to its data source. The goal here is to reduce load on the data source provider.

To signal a control to connect to its data source and create its child controls, a page developer calls the Control.DataBind method. This call results in the Control.OnDataBinding method being called and the DataBinding event being raised. This behavior is in line with all other stages of pages.

When a server control's OnDataBinding method is called, it should create its child control tree as we did previously in the CreateChildControls method, but with a few changes:

  • The number of controls instantiated (the numbers of items in the collection) is remembered using ViewState . You have to do this, as you need to recreate the same number of controls when a postback occurs. All other state will automatically be remembered by the other server controls instantiated as part of the template.

  • Because OnDataBinding may be called one or more times, the ClearChildViewState and Controls.Clear methods are called to clear any existing viewstate for the control, and delete all child controls.

  • The ChildControlsCreated property is set to true . Setting this flag ensures that CreateChildControls is not subsequently called.

The following code implements these changes:

  protected override void OnDataBinding(EventArgs args)   {   base.OnDataBinding(args);   if (_datasource == null)   throw new Exception("Control requires a data source");     // Clear all controls and state   ClearChildViewState();   Controls.Clear();   IEnumerator e;   int iCount;     if (_headingStyle != null)   _headingStyle.InstantiateIn(this);     e = _datasource.GetEnumerator();   CollectionItem item;   while(e.MoveNext())   {   if (_itemStyle != null)   {   item = new CollectionItem(e.Current);   _itemStyle.InstantiateIn(item);   Controls.Add(item);   iCount++;   }   }     // Remember the number of controls, so we can recreate the   // same controls, without the data source.   ViewState["count"] = iCount;     // stop CreateChildControls() being called again     ChildControlsCreated = true;     // Ensure viewstate is being tracked   TrackViewState();   }  

When a postback occurs, a server control's CreateChildControls method will typically be called, unless a page developer explicitly calls DataBind . This method should recreate the control tree, using only information stored in viewstate.

Here is the implementation of CreateChildControls . The basic creation logic is similar to OnDataBinding , except that the data source is not at all used:

  protected override void CreateChildControls()   {   int iCount;   int i;   CollectionItem item;     if (_headingStyle != null)   _headingStyle.InstantiateIn(this);     iCount = (int) ViewState["count"];   for(i=0; i< iCount; i++)   {   if (_itemStyle != null)   {   item = new CollectionItem(null);   _itemStyle.InstantiateIn(item);   Controls.Add(item);   }   }   }  

The changes in this code are:

  • The data source hasn't been used.

  • The number of controls to create was determined by the count property held in viewstate.

  • A null value was passed to the constructor of CollectionItem , since the DataItem will not be used.

With these changes in place, you have a data-bound templated control. The control should behave just like any of the built-in controls you have used.




Professional ASP. NET 1.1
Professional ASP.NET MVC 1.0 (Wrox Programmer to Programmer)
ISBN: 0470384611
EAN: 2147483647
Year: 2006
Pages: 243

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