From the June 2002 issue of MSDN Magazine.
Develop Polished Web Form Controls the Easy Way with the .NET Framework
|David S. Platt|
|This article assumes you're familiar with Visual Basic .NET, C#, and HTML|
|Level of Difficulty 1 2 3 |
|Download the code for this article: WebC.exe (274KB) |
|SUMMARY Pre-built custom controls make application design easier and faster and allow you to maintain UI consistency. However, prepackaged controls can be big and slow, and are OS-specific. For those who don't want to use prepackaged controls, Visual Studio .NET provides controls for Web Forms similar to those found in Windows Forms, including label and textbox, and new additions such as the DataGrid, all of which you can customize.|
If you want to design your own controls, the .NET Framework provides inheritable classes that take care of all the nasty stuff you want to avoid, including page lifecycle, maintaining state across invocations, and browser detection. This article discusses these concepts, as well as eventing, rendering, and client-side scripting.
|ontrols are a winning concept. When you use prepackaged UI functionality you get faster, less costly design, and a more consistent UI across applications. But that's not the end of the story. Controls can also be clunkier and run slower than code that's been hand-tuned to a specific task. And Windows®-based control architectures, such as Windows Forms controls and the older ActiveX®, OCX, and VBX controls run only in a Windows environment—a problem in today's heterogeneous Internet world in which new types of platforms appear almost daily.|
To work cross-platform, Microsoft® designed the Web Forms architecture of ASP.NET. Using Visual Studio®, you can select components called Web Forms controls from a toolbox and place them on an ASPX page. Then you can set the control's properties and write code in any language that supports .NET to tie its behavior to other controls. The process is designed to feel very much like writing an app in Visual Basic®, a familiar model to most programmers.
Figure 1 Page Request
When a client requests a page containing the Web Forms control, the ASP.NET processor loads the page and creates the controls on the server, as shown in Figure 1, then executes the page's programming logic, tying the controls together. At the end of the process, each control provides ASP.NET with the HTML describing its current appearance. The HTML is returned to the client and rendered in the browser. Read about ASP.NET Web Forms at ASP .NET: Web Forms Let You Drag and Drop Your Way to Powerful Web Apps.
Visual Studio comes with a generic set of Web Forms controls, more or less mirroring the set available in Windows Forms. It contains such old favorites as the label and textbox, along with newer, more sophisticated controls such as the DataGrid.
This article describes the functionality the .NET Framework provides for you to write your own Web Forms controls. Since I already covered the basic concepts of controls (methods, properties, and events) in the April 2002 issue of MSDN® Magazine, in this article I'll concentrate on the differences between Web Forms controls and Windows-based controls. These differences stem primarily from running in the relatively austere browser runtime environment rather than the rich Windows environment.
.NET Support for Developing Web Forms Controls The .NET Framework contains prefabricated software classes that make writing a Web Forms control relatively easy. You'll need to understand HTML to produce the control's required output just as a Windows Forms control designer needs to understand the Windows GDI. But routines that hook into the ASP.NET page lifecycle, maintain state across multiple invocations, and detect the capabilities of the hosting browser—the infrastructure common to all controls—have already been written for you.
You write a control in your choice of .NET-compliant language, using the prefabricated infrastructure by inheriting from your choice of several .NET Framework classes. These base classes correspond roughly to classes in Windows Forms. But, as is often the case, Microsoft has used the same name to mean different things in similar-looking environments, so you have to tread carefully and read the fine print. Let's look at the five ways to develop a Web Forms control.
The first choice is to derive from System.Web.UI.Control, the base class for all Web Forms controls. It participates in all the lifecycle events of the ASP.NET page-rendering process. The documentation states that this class doesn't have any user interface-specific features, despite the fact that it lives in the System.Web.UI namespace. I don't agree. It contains a Render method (described in the next section), which a control uses to emit the HTML to display in the browser. It has fewer built-in UI properties than the next class I'll describe; for example, it lacks Width, Height, ForeColor, BackColor, and Font, but you can still very easily build a UI with it if you so desire. It's more lightweight because of these omissions, but the difference is barely noticeable. If you don't care about any of the functionality it omits, it's perfectly fine to use this as your base class.
Your next option is to derive from System.Web.UI.WebControls.WebControl, which in turn derives from System.Web.UI.Control. This is a control with basic user interface properties added. Visual Studio automatically uses it as the base class when you generate a new Web Control Library project. If you want a control that provides a user interface, which most controls do, you should probably start here.
You can also start with an existing Web Forms control, one that ships with Visual Studio or one that you bought from a third party. In this case, you will derive your control from the existing control using the .NET inheritance mechanism. You'll reuse whatever portions of the existing functionality you want, override the pieces you want to change, and add whatever additional functionality you want your control to have. In my previous article, I showed how to do this with a Windows Forms textbox control. Doing it for a Web Forms control is the same, so I won't show it again.
Another option is to design a control that contains other controls. This is called a user control in Windows Forms, but in Web Forms it is called a composite control. You choose any of the three inheritance scenarios that I've already described for your base class. Unfortunately, Web Forms do not support the addition of child controls, as Windows Forms do. Therefore, you must create and position the child controls by hand in your code. It's not difficult, so I won't discuss it here. However, I found its omission to be quite surprising.
Finally, you have the option to create a user control. Each of the controls I've discussed is a fully compiled .NET assembly. They work with the Visual Studio toolbox and designers and can live in the Global Assembly Cache (GAC), so you don't need a separate copy for every client that wants to use them. Web Forms provides one more way of producing a reusable control package (user control). Like the user control you saw in Windows Forms, a Web Forms user control is produced in the Visual Studio designer by placing other controls onto a design surface. But unlike the Windows Forms user control, this one is an HTML page rather than a compiled assembly and therefore cannot live in the GAC or in the Visual Studio toolbox, not does it show its appearance in the Visual Studio designer. For these reasons, I find it much less useful than the other types of Web Forms controls I've discussed. I suspect it exists because of the lack of designer support in composite Web Forms controls.
A Simple Custom Control As I always do when learning or teaching a new piece of software, I started with the simplest example I could think of. It's a label control containing a property called Text, which is the string displayed by the label, and a property called ForeColor, the color of the text string (see Figure 2).
Figure 2 Label Control
I started by generating the project in Visual Studio and selecting Web Control Library from the new project dialog, as shown in Figure 3. The wizard generates a project containing a new class, derived from System.Web.UI.WebControls.WebControl. Adding methods and properties to this class is exactly the same as adding them to any other .NET class. In fact, the system-provided base class already contains properties called Text and ForeColor, which I use in this example.
Figure 3 Generating the Project
Told you it was simple. Well, sort of. The key to understanding a custom Web Forms control is the Render method, which conceptually is identical to a Windows Forms OnPaint method, except the former emits HTML and the latter emits GDI calls. When the ASP.NET server framework assembles your Web Forms page in response to a request from a user, it creates the controls listed on the page, sets their properties and persistent data, then calls each of their Render methods. The framework is telling your control, "You're alive and in the state you're supposed to be in. I need you to tell me what you look like, because I have no other way of knowing." The author of a Web Forms control places code in the Render method that emits HTML telling a browser how to display the control's appearance based on the control's current state and properties and anything else in the world that the control cares about.
The author of a Web Forms control needs to know HTML because the environment provides very little abstraction of it. The HTML that specifies text color looks like this:
To produce this HTML, I wrote the code you see in Figure 4. Most readers tell me that they prefer to see source code in Visual Basic; for the sake of all you C# readers, I've written the downloadable sample code in both Visual Basic and C#.
Here is some text
When it calls your control's Render method, the ASP.NET framework passes an object of type System.Web.UI.HtmlTextWriter. This is conceptually similar to the Graphics member of the System.Windows.Forms.PaintEventArgs that your OnPaint method receives in a Windows Forms control. Both represent the connection to the framework that directs your output to its proper location. The HtmlTextWriter contains methods, properties, and constants that allow your control to emit HTML into the output page that will be sent to the client's browser. In the sample code, I first call the method AddStyleAttribute, which internally creates an HTML attribute called style, sets its value to the value of the control's inherited ForeColor property, and adds it to an internal buffer. Additional values of the style attribute can be added to the buffer by additional calls to the AddStyleAttribute method, and other attributes can be added by calling the AddAttribute method, though I didn't need either one in this example.
I next call the method RenderBeginTag, specifying the name of the HTML tag I want to appear in the text, in this case "span." This call fetches any attributes (here it's the style) from the internal buffer, places them into the tag, and writes it to the HTML output stream. These two calls produce the first line of HTML:
Next, in order to write the text of the label, I call the method HtmlTextWriter.Write, passing the control's internal text string. This method passes the text string verbatim into the HTML output stream, thereby producing the second line:
To close the <span> tag, I call HtmlTextWriter.RenderEndTag. This causes the writer to look back to the last open tag and emit the closing tag for it, in this case </span>, as the last line of HTML.
Here is some text
This object contains other methods for performing output, which provide finer control but which are somewhat more complex. For simplicity, I'll stick to these for the rest of this article.
Figure 5 Adding a New Control to the Toolbox
Finally, I need a client to use this control so I can debug and display it. To my existing solution, I add a new Web Application that contains an ASP.NET page. To add my new Web Forms control to the toolbox, I right-click and select Customize Toolbox, bringing up the dialog box shown in Figure 5. I surf to my new Web control DLL, select it, and it appears in the control list, as shown in Figure 6.
Figure 6 New Control in Control List
I can then place it on my ASPX page and set its properties. When I build the project and start it in the browser, the control looks like it does in Figure 2.
A More Complex Example Now that you've seen the basic functionality of a .NET Web control, let's look at an example that demonstrates how a Web Forms control can fire .NET events for other controls on its page. I find the SDK documentation murky when it comes to Web control events. It defines the click by the user on the browser that starts the process, the postback to the server that it triggers, and the notifications that the server-side control that receives the postback sends to other controls on the page all as events. I'll attempt to make accurate distinctions among these events.
I've written a more complex sample control, shown on an ASPX page in Figure 7. It displays a table in the user's browser, each cell of which displays its row and column number. The control exposes properties called Rows and Columns, each an integer, which are set at design time. When the user clicks on any cell in the table, the form gets posted back to the server. Once the page has been assembled and initialized on the server, the table control determines which cell the user has clicked and fires a .NET Framework event on the server to any other control on the page that cares to listen. In this example, the page contains an event handler that sets the value of a separate label control, displaying the row and column of the cell that the user has clicked.
Figure 7 Table Control
When I developed this control, I first wrote my Render method to simply cough up the HTML to display a table with the desired number of rows and columns; this was quite easy. Next I wanted to add the HTML that would cause the browser to post the form back to the server when the user clicked on a cell in the table. This required some additional fancy footwork (see Figure 8).
You can see that each table data entry (<TD>), which creates a single cell, contains an onClick attribute that calls a client-side script, passing the ID of the server-side control that will handle the postback (in this case, my table control), and a string that contains an arbitrary argument. Here it's the text of the table cell, which allows the server-side control to identify the cell that the user has clicked on, but it can be anything you want. The client-side postback script, which you can see at the bottom of the figure, places these parameters into hidden input controls and performs a postback to the server.
I'll discuss the server-side handling of this postback shortly, but first let's see how I produced this HTML. It looks ugly, and you'll be happy to learn that you don't have to write it yourself. Remember, your control lives on an ASP.NET page, and therefore has access to all the methods of this page through the base Page class member variable. The method Page.GetPostBackEventReference causes the framework to generate the HTML script on the page and to return the HTML string that calls it. I then add this string to the attributes of the <TD> element, which is no problem using the HtmlTextWriter methods shown previously. You can see the Render method's code in Figure 9. For now, ignore the portions of the code dealing with View State; I'll explain these in the next section.)
What happens if the browser can't run JScript®? I'll discuss that later. Right now, assume that it does, or rather, explicitly declare that you are only handling the case where it does.
The postback form comes to ASP.NET, which loads the target page on the server and creates the controls on it. ASP.NET needs to deliver the postback to the control it's addressed to, which it knows from the hidden input control filled by the client-side script. A server-side control accepts this input notification by implementing an interface called IPostBackEventHandler and overriding the RaisePostBackEvent method. I find the method name highly misleading. It doesn't raise a postback event; it accepts one from the browser via ASP.NET and raises a server-side .NET event. The method exists for the sole purpose of transforming the generic form postback event sent by the browser into a named, meaningful .NET control event with useful parameters, for which other server-side controls can listen and for which a page designer can easily write code. If you think of it as AcceptPostbackAndOptionallyRaiseServerEvent, you'll have the right mental model.
Into this poorly named method I place the code that I want my control to execute when it receives the postback from the user's browser (see Figure 10). After figuring out which control should receive the postback, ASP.NET calls this method and passes it the eventArgument string that was passed to the client-side script by the client and which then got transmitted in the hidden input variable. In this case, it's the text of the table cell the user clicked on. My sample code parses the row and column numbers out of the string, so my server-side handler knows which cell the user clicked.
If you know that your control is the only one that ever cares about whatever it was that caused the postback, you don't need to do anything else; just write your handler code right there in the RaisePostBackEvent method. But one of the main things your control might want to do after receiving a postback is to notify other controls on the page that something has happened to your control.
To do this, your control needs to fire .NET events to the page and the other server controls using the same generic eventing mechanism you saw in Windows Forms controls. In this case, I've added an event called TableCellClicked to my control, containing two parameters, the row and column of the click, as shown in Figure 10. Anyone who wants to can set up a handler to receive this event. In my sample, the page contains a handler that receives the event from the table control and sets the clicked cell into a label control (see Figure 11).
To summarize, eventing in Web Forms controls involves two required parts and an optional third part. First, your control's Render method must generate client-side HTML that causes a postback to your control when an event that interests you happens on the client. Second, your control must implement the IPostBackEventHandler interface, so that ASP.NET can tell your control that it has received this postback and pass you additional information about it. Third, and optionally, your control can and probably will choose to fire .NET events so that other controls can receive notifications of these happenings.
View State Management Web pages are inherently stateless. By this I mean that what is shown on one page is independent of the pages that the user has previously seen, unless you write code to somehow tie them together. When users simply viewed static text pages, that wasn't too bad. But since most interaction with Web sites now involves ongoing conversations spread over many pages, that won't do. The SDK documentation says that you must "provide your user with the illusion of continuity." That author has stated the problem exactly backwards. The user's experience is the center of the universe, and don't you forget it. It is your code that must match the user's expectations, not the other way around, and if your programming model doesn't match what the user needs, it's your job to write code so that it does. The user's continuity is real; it's your code's continuity that's illusory.
A designer writing code on an ASPX page has access to functionality such as the Session and Application collection objects to maintain state from one page to another. But the control designer can't use these, because she doesn't know when a page's session state is abandoned for a timeout or explicitly dumped by the page programmer. In fact, she doesn't even know if session state has been turned on at all, and can't alter it if it hasn't. So if your controls need to maintain state from one rendering of the page to another, you have to devise some other way.
The .NET Framework provides a mechanism that allows Web Forms controls to maintain their state safely and easily. The control base class contains a member called ViewState. It's a property bag collection of the same type used by Session and Application object collections, except that it stores its data in a hidden text field on the page. You can see the view state in the hidden input field named __VIEWSTATE shown in Figure 8.
When your control places data into the view state collection, ASP.NET serializes it into the ViewState string and transmits it to the client as part of the rendered page. When the page gets posted back to the server, ASP.NET fetches the hidden variable and deserializes it into each control's ViewState member variable. This architecture is especially good for scalability in Web farm situations, because it avoids any kind of server affinity. Whichever machine handles a postback can see what the state was the last time and store it, possibly updated, for the next time.
I wrote my sample table control to use the ViewState to remember its click state, the row and column of the cell that the user had clicked. In my control's constructor, you can see that I create the view state variables for remembering clicked row and column and set them to -1, indicating no selection.
When I receive a click postback from the client (see Figure 10), I fetch the cell the user has selected and store it in the ViewState. When I render (see Figure 9), I fetch the selected row and column and adjust the HTML to properly display the selected cell. That's all there is to it. Smooth, huh?
' Start member variables in desired default state;
' no selected cell
Public Sub New()
Me.ViewState("SelectedRow") = -1
Me.ViewState("SelectedColumn") = -1
Note that the base class contains a member variable called EnableViewState, whose description says that it tells the control whether to save its internal state in the ViewState. But if you work with the sample code, you'll find that this variable seems to have no effect on it. That's because the variable doesn't turn off the ViewState mechanism internally. It's simply a Boolean flag telling your control that the page designer would really like it if you would be so kind as to knock it off with the ViewState already. It is entirely up to you to write code that examines and responds to the state of this variable, which I've chosen not to do in this example. See how annoying it is when someone violates the Principle of Least Astonishment? So don't do it to your customers, OK?
Client-side Scripting Most of the control functionality I have been discussing takes place on the server side, and indeed, many people refer to these Web Forms controls as "server controls" to emphasize this. Really good Web UI design usually requires at least some code on the client in the form of browser scripts. For example, the validator controls in the Web Forms toolkit ensure that a user has filled in required fields on a form with data that meets their criteria (any string, valid e-mail address, integer between 5 and 15, and so on) before it will allow a form to be submitted. If the data doesn't meet a validator's criteria, the validator displays an error message and aborts the postback. This saves network bandwidth, server cycles, and also user frustration as the feedback is immediate; again a lovely combination.
The ASP.NET framework provides a built-in capability for Web Forms controls to easily emit script to be placed on a page returned to the client and easily access the scripts put there by themselves or other controls. I've written a sample program showing the three places where a control can place scripts on a page returned to the client (see Figure 12, Figure 13, and Figure 14).
Figure 12 Scripting a Sample App
All of the methods used by a control to place script on a page reside in the Page member variable, representing the page on which the control resides. A control can place a startup script on a page via the Page.RegisterStartupScript method. This script will be automatically executed when the page is shown in the user's browser. The sample program simply pops up a message box when the page is displayed. You can also place a script block that needs to be explicitly called by other script code on the page using the Page.RegisterClientScriptBlock method. The sample program simply pops up a message box when you click on the text on the page.
Both methods accept two string parameters: the name of the script block and the script itself. If two controls attempt to register a script with the same name, the page will ignore the second attempt. This means that to avoid naming conflicts with other controls that you didn't write, industrial-strength controls ought to use long distinctive names and avoid short generic ones like MyScript. If you want to find out if a script block is already registered before you register it yourself, you can do so via two methods on the Page object, namely IsClientScriptBlockRegistered and IsStartupScriptBlockRegistered.
The scripts that you pass may be literal, as they are in this example. However, sometimes scripts can get long, as can validators. When this happens, you may want to place your scripts into a separate file to be fetched at run time. In this case, you would write your script tag to use the src attribute to direct the script execution engine to the location of the script file, something like this:
Probably the most common use of client-side script is to validate data before allowing a form to be submitted. To do this, your code must be informed of an impending postback operation, and have the ability to cancel the postback if your script's demands are not met. Using the ASP.NET framework, you can register a submit handler via the Page.RegisterOnSubmitStatement method . When the form is submitted, the browser steps through all of the registered submit statements to see if the submission should go forward. Your submit statement must contain your validation logic, returning true if you want to allow the submit operation to proceed, or false otherwise. The sample program puts out a submit handler into the generated script that pops up a prompt box when you click the Submit button. If you enter "Y," it goes ahead; otherwise it aborts the operation.
Not every browser is capable of running scripts in your preferred language or running scripts at all. You need a way to detect this so that your control can decide to run in a degraded way, notify the user, or not run at all. The validator controls, for example, run their logic on the server side if they are unable to perform their validation on the client. In fact, they run on the server side anyway, even if they think it has successfully run on the client, just to make sure that it hasn't somehow miscarried or been tampered with to inject invalid data onto the server.
Conclusion Web Forms make it easy to write good Web applications, and the .NET Framework lets you easily create Web Forms controls. All you must understand is the control's business logic and how to render it in HTML; the rest of the infrastructure is handled by classes inherited from the .NET Framework.
| For related articles see:|
ASP .NET: Web Forms Let You Drag and Drop Your Way to Powerful Web Apps
Windows Forms: Developing Compelling User Controls that Target Forms in the .NET Framework
| David S. Platt is president and founder of Rolling Thunder Computing Inc. He teaches .NET at Harvard University and at companies all over the world. He publishes a free e-mail newsletter on .NET, available at http://www.rollthunder.com. David is the author of Introducing Microsoft .NET (Microsoft Press, 2001).|