Anatomy of a Server Control
Part 3 - Events
by Uncle Chutney
| Table of Contents: |
|
Introduction: This is the third
of a series, which begins with "Part 1 - A Simple Control3,"
followed by "Part 2 - Maintaining State4."
This series builds on principles outlined in my "ASP.Net Fundamentals1"
article and my "The Life and Times of a Server Control2"
article. If you're unfamiliar with how ASP.Net works, you should begin by
reading those. What is an Event? Webster's Dictionary defines "Event" as: \E*vent"\, n. [L. eventus, fr. evenire to happen, come out; e out + venire to come. 1. That which comes, arrives, or happens; that which falls out; any incident, good or bad. In programming, an Event is just that: Something that happens. We tend to think of Events as something which happens as a result of the user doing something, such as clicking a button, hyperlink, image, or moving the mouse cursor over an object. However, in point of fact, an Event can happen as a result of an application doing something which the user may not even be aware of. Actually, in programming, an Event is just a wee bit more than just something that happens. An Event is a message which is sent as a result of something happening. What actually happens is that the Class which fires the Event has an instruction somewhere, which, when executed, sends an Event message (Raises an Event) to the Operating System. The Operating System then broadcasts the Event Message. Any Class which has an Event Handler routine is listening for this message, and picks it up. The Event Handler routine (for that Event) then processes the Event, in whatever way the Class is designed do do so.
In actuality, the Event message contains references to two objects: The class which fired the Event, and the Event object itself (Event Class). The Event Class can contain additional data which the Event Handlers may need in order to process the Event. For example, you might want to send the "selectedIndex" value of a drop-down list box (HTML select object) with the message, when it changes. The Event Class can contain this sort of information. In .Net, (nearly) all Event objects inherit the System.EventArgs Base Class. The CLR (Common Language Runtime) contains many derivatives of this class, and of course, you have the ability to create your own derivatives of this Class for your own Event-handling purposes. System.EventArgs is a class which has no Event data contained in it; you can use it directly for handling Events that need no additional data passed with them, such as a Button Click. There is one other aspect of Events which we will need to describe before we continue. So far we have talked about Events and Event Handlers. Now we need to examine the concept of Event Delegates. As you recall from the previous paragraph, there are many derivations of System.EventArgs which can be passed in an Event message. Each of these is for a different kind of Event, and has additional properties (data) that are specific to the kind of Event and Class that it is designed for. An Event Message, as you recall, almost always has 2 parameters: The object which fired the Event, and the EventArgs class for that Event. How does the developer distinguish what type of Event should be passed for a specific EventArgs Class? This is done using Event Delegates. A Delegate is a special kind of Class, which serves as a Function pointer5 to the Event Handler(s) for this Event. Since the object raising the Event has no knowledge of the Classes which will handle the Event, it uses this special Class to define the signature6 of the Event Handler Methods employed by other Classes to handle this particular type (Class) of Event. This way, the Class which raises the Event passes a certain EventArgs data type, and the Classes which handle the Event have Event Handler Methods which are designed to receive that EventArgs Class as a parameter. If you create your own EventArgs Classes, you will also need to create a Delegate Class to handle these Event Classes. The following is an example of a Delegate Class (in Visual Basic.Net): Public Delegate Sub myEventArgsClassEventHandler(sender As Object, e As myEventArgsClass) Note that all this Delegate Class contains is a definition of the signature6 of the Event Handler Method. To review, we have now identified 4 necessary aspects to Event Handling:
Now, you should have it firmly in your mind by now, if you've read all my previous articles, that HTTP is stateless1, and that there is no "invisible connection" between the server-side classes and the client-side HTML document. How then, can a client-side Event be handled on the server side? Of course, this is done by using those hidden form fields I talked about in my "ASP.Net Fundamentals" article7. A client-side Event, such as an "onclick" or "onchange" event, can be captured by a JavaScript event handler Function. ASP.Net provides such a Function (__doPostBack()). What this Function does is to record the name of the object which fired the Event, as well as any additional Event information, place this data into hidden form fields (__EVENTTARGET and __EVENTARGUMENT), and then submit the form, initiating a PostBack. On the server side, any controls that implement either the IPostBackDataHandler or IPostBackEventHandler interface (meaning that they process or handle client-side events) will have the appropriate method called to examine the PostBack data, and see if they raised the client-side event. If so, a corresponding server-side Event is raised. At this point, the Event Handler for the control which raised the Event is called.
In this article, we will build a "simple" Drop-down list box (HTML select object) that maintains its selectedIndex Property between PostBacks (maintains state), and fires a custom "SelectedIndexChange" Event. In our ASPX page, we will define an Event Handler for that Event. Requirements: This demonstration was built with the Microsoft Visual Basic.Net language, using Microsoft's Visual Studio.Net. While these examples could fairly easily be written in Notepad, and converted in order to prevent the necessity of compilation, it is recommended that you use the same tools. About DropDownList: Until now, we've been fairly closely emulating existing (CLR) Server Controls in our Demonstrations. However, for this one, we're going to depart from that path, and build one from scratch. There are several reasons for this:
Building DemoControls.DropDownList: As mentioned above, this Class will not be much like System.Web.UI.WebControls.DropDownList, except that it will render a drop-down list box, and maintain its' state between PostBacks, as well as raising a "SelectedIndexChange" Event when the selectedIndex Property of the HTML select object (interface) on the client is changed (a selection is made). So, we have basically 3 areas of concern here:
Oops. Looks like I added a third area there ("Populating the List"), which I haven't talked about. The reason I haven't mentioned it is that this article is not about using a Server Control with a Data Source. But since this is a Drop-down list box, we are going to have to deal with how to populate it, as a matter of course. As a result, we will have to touch briefly on the topic of Reflection. So, let's get that bit out of the way first. Using Reflection to Create a Data Source A drop-down list box is an HTML (select) object which has a variable number of options. As we are developing a Server Control, we want it to be as flexible and easy to use as possible, so that we can plug it into a variety of applications, with a variety of different requirements. There are several data types in .Net which can be used for this type of thing, including Arrays and Collections. Of course, there are many derivatives of these, but they all share some properties in common. So, we want to be able to use any of them without having to know anything about them initially. Therefore, we create a Property of the "DropDownList" Server Control, to hold the values used to populate the list, and we give it a type of "Object." All .Net classes are derived from Object, so we can plug any kind of Object into this Property. Note that this is a simplified DataSource. For example, we aren't going to cover using a DataTable or DataReader to populate this, although it certainly wouldn't be too difficult to do so. But this article isn't about Reflection. And putting all the necessary code to account for every possible DataSource type would add unnecessary length to this already lengthy article. But if you can follow this, you should be able to add whatever functionality to it you wish. Of course, there are many Object types that will not work for this Property. We must exclude Object types that are not Arrays or Collections. We can both examine the Object, and use its properties to populate our DropDownList using System.Reflection. This set of classes is specifically designed to be able to discover everything we want to know about an Object, and to manipulate it as well. Now, Arrays and Collections are 2 entirely different types of classes, but they do share some things in common. All Arrays and Collections have a method called GetEnumerator(), which returns an IEnumerator object. The IEnumerator class has methods for sequentially accessing the member objects in an Array or a Collection. Reflection offers a number of ways of deriving what the various properties of a Type are, but unfortunately, you can't use the same methods to determine whether an Object is an Array or a Collection. For example, Arrays and Collections both implement IEnumerable, but due to the special nature of Arrays, you can't call Type.GetInterfaces() with an Array and get back any of the interfaces it implements. However, you can check the Type.IsArray Property to find out whether this is an Array or not. So, in order to qualify the Data Source object as an Array or a Collection, we need to use both IsArray and GetInterface() to determine whether or not the Object is an Array or a Collection:
' ** First, we make sure that the DataSource Property
has been set
' ** Check to see whether it's an Array
' ** If not an Array, check to see whether it implements IEnumerable Note that we have obtained an Enumerator in any case. There is still work to be done, and this time we will need to discover what we can about the members of the Array or Collection. The IEnumerator gives us access to the members. Now, here we have a dilemma as well. As you may know, there are 2 basic types of classes in .Net - Value Types and Reference Types. This is due to the fact that everything in .Net is an object, even simple numbers, dates, and strings. But because of the overhead involved in working with objects, .Net has Value Types, which are special classes which aren't classes until you treat them as such. This is done using a technique called "Boxing." Numbers, Characters, Strings, and Dates fall into this category. The members of our Array or Collection may be Value Types or they may be Reference Types. Our DropDownList object has an array of Options. An Option can have a different "value" attribute than the text displayed for that option. If we are working with a Collection of Objects, we can use one Property for the Value, and another for the Display, which can come in handy. For this reason, I added 2 properties to the Control: DisplayMember and ValueMember. These 2 properties are the names of the properties (or Fields8) that will be used from the members of the DataSource. But what if the DataSource is an Array of Strings, Numbers or Dates? While these data type classes have members, we aren't going to actually use any of the members, but the Value contained in the object itself. If it is a String, the String is the value we will want to use in both the Value attribute (DisplayMember), and the Displayed text (ValueMember). Therefore, we need to determine whether the members of our Collection are Value Types or Reference Types. Fortunately, the Type class has a Property called "IsValueType" which can be used to determine this.... with one exception (surprise, surprise). When you invoke this Property of a String, which is a special kind of Value Type, you get False. So, we need to check 2 things again. We need to check the "IsValueType" Property, and if that is False, we need to check the type name itself, to see whether it is a string. Here's an example using an Array: If (CType(DataSource,
Array).GetValue(0).GetType.IsValueType) Or _ For this Server Control, I created a number of global Private Fields to hold the various kinds of information the Server Control would need in order to determine how to insert the data in the DataSource into the control. Then I created a Function to get this information and populate those fields. The Function, called "ConfirmProperties()" returns a Type class, which is the Type of the members of the Collection/Array: ' ** Used to store Display
Property Info In Part 24 of this series, we discussed Maintaining State in detail. This Server Control also maintains state (the selectedIndex Property of the control must be maintained between PostBacks). So, I'm just going to do a slight review here: As you should recall, in order to maintain state, the Control must implement IPostBackDataHandler, so that it can capture the PostBack, and correctly set its server-side (private) _SelectedIndex Property. This Property will be used in the Render() Method, in order to add the "selected" attribute to the appropriate option in the Options of the HTML select object we will render. The server-side _SelectedIndex Property will also have to be persisted between PostBacks, so we will want to store that in ViewState. However, in order to determine the SelectedIndex, we will only know the Value Property passed from the form. So we also need to store the Value in a persistent Property; therefore the (private) _Value Property of the Control will also use ViewState. As the developer from time to time might want to access these values, we also create Public ReadOnly Properties to return the Private values:
' ** Used to programmatically access the SelectedIndex
Property of the ListBox Of course, all we have to do now is to write our implementations of the IPostBackDataHandler's LoadPostData() and RaisePostDataChangedEvent() Methods. These methods will also be used to raise our SelectedIndexChange event, so, before we go into the code for this, let's get on with the Main Event: Raising an Event. Raising an Event in a Server Control In many cases, such as a Button click, all an Event needs to do is to indicate that something has happened, and nothing more. But in some cases, we would like the Event Message (Class) to contain some information about the Event. This Server Control is one of those cases. We can imagine that when the SelectedIndex Property changes for this control, the developer may want to access the value of the SelectedIndex Property as well as the Value that was selected from the Options. For this reason, we are going to create a custom Event Class which can expose that data:
' ** Custom Event Class for dropDownList As you can see, it's pretty easy to create a Custom Event class. The Public Properties are ReadOnly, and expose Private properties which are set in the New Constructor for the class. Now all we need is a Delegate for this Event:
' ** Event Delegate for DropDownListEventArgs Now we're ready to raise a SelectedIndexChange Event in our class. The following code both persists the SelectedIndex and Value properties of the Control, and raises an Event when the SelectedIndex changes:
' ** This is our custom Event Let's make a few notes here. First, while we are using these Methods to persist the Value and SelectedIndex properties, note that we don't set the SelectedIndex Property in the LoadPostData() Function. Why? The answer lies in the Control Event LifeCycle. Note that in order to set the SelectedIndex Property, we need to use the Value Property and loop through the DataSource. However, if we try to loop through the DataSource in the LoadPostData() Function, we will get a "Null Reference" error, because the LoadPostData() Function fires before the Page_Load Sub of the Page. In other words, the DataSource has not been set yet. Now, the SelectedIndex Property is persisted in ViewState, so it isn't necessary to set it in the LoadPostData() Function. It's only going to change when the Value Property changes. Since the LoadPostData() Function returns True if the Value Property changes, and thus indicates that the RaisePostDataChangedEvent should execute, we can put the code to get the SelectedIndex in the RaisePostDataChangedEvent() Sub. This Sub fires just prior to the Render() Method, so we will have access to the SelectedIndex property when it comes time to render our Options, and set which one should be Selected. Also, note that rather than having the RaisePostDataChangedEvent() Function raise the Custom Event, we use it instead to call a Protected Overridable Sub (OnSelectedIndexChange()). Why? Because the developer may want to override the default Event handler. This gives the developer that option. Now let's examine the DHTML object model for an HTML select object, in order to evaluate which properties we want to be able to configure programmatically when the Control is rendered to the page. As with any other DHTML object, this one has quite a few properties/attributes. For the sake of the simplicity of this article, I am not going to list all of them, but only the ones we will use for this Demo. The process is the same, however, in general: If we want to (1) render a Property, and/or (2) programmatically control how that Property is handled on the client, we need to create properties of the Server Control Class which can be used to do this. Rendering DemoControls.DropDownList After looking over the DHTML object model for a select object, I came up with the following (truncated for the sake of this article) list of properties/attributes which we will be wanting to control:
Next, we need to create these properties, and in the Render method for the Control, make sure that the properties correctly render the HTML for the properties in the page:
We also want to be able to set the names of the Fields/Properties of the DataSource that we will use for the Options' Value property and Text Displayed, so I created 2 additional Properties, DisplayMember and ValueMember: ' ** This is the name of the
property or field of the DataSource Of course, we also need a DataSource Property. This property should check to make sure that the DataSource implements IEnumerable: ' ** This property can be any
object which implements IEnumerable Finally, we have all of our Fields and Properties, and we are ready to implement our Render() Sub: ' ** Renders the HTML to the
Page I had to break up the code for this Control in order to explain it properly. If you'd like to see the entire code as it was written, click here. Now all we need is an ASPX page to display this Control in. I created a Page which would be able to test this Control with an Array of Strings, an Array of Objects, or a Collection of Objects. I have omitted the class code for my custom Object and Collection classes which I used to test this, but you could certainly come up with your own if you want to test it. The following is the Page_Load Sub and Event Handler which I used for this project: Private Sub Page_Load(ByVal sender As System.Object,
_ Here are a couple of screen shots of the results:
Conclusions: By building a DropDownList Server Control, we have demonstrated how a Server Control can raise an Event on the Server in response to a client-side event, such as the change of the selectedIndex property of the client-side HTML select object which our Control renders. In addition, we have reviewed how a Server Control maintains State, as well as introducing the concept of using Reflection to make a Server Control more flexible in practical use. |
|
Footnotes:
1. See "ASP.Net Fundamentals" 2. See "The Life and Times of a Server Control" 3. See "Anatomy of a Server Control Part 1: A Simple Control" 4. See "Anatomy of a Server Control Part 2: Maintaining State" 5. A pointer is a special data type, which represents (or references) an address in memory. If you have used any derivative of the C programming language you should be familiar with the concept of pointers. In Visual Basic.Net, the term "pointer" is generally substituted with the term "reference." 6. A signature is a kind of pattern, defining the format and data types associated with a Method. 7. See "ASP.Net Fundamentals" 'ASP.Net and Events' section 8. In Object-Oriented Programming, a global variable without a Get and Set method is a Field, not a Property |


