Custom Components in ASP.NET (using C#)

Sayed Hashimi

ASP.NET already provides a plethora of sophisticated controls you can use to develop web pages. However, to make the most of your efforts, it's better to think of developing web pages from the point of writing components. Instead of just asking yourself "what controls do I need for this page?" also ask, "and how can I break this page into components?"

Some of the advantages of breaking your pages into components are:

Your components can be either specific to or independent of a particular browser.

ASP.NET makes it very easy to develop your own components. To develop truly independent components, you have to consider factors such as events, state management, performance/caching, and the influence of the client browser. This article first introduces the various mechanisms available to develop custom controls (components) in ASP.NET and then discusses how to include custom events and state management in your controls. In addition, I include notes on how you can improve the performance of your controls.

Believe it or not, there are nearly half-dozen ways to create your own controls in ASP.NET. However, from the point-of-view of creating independent components, you have three mechanisms: Composition, Inheritance, and Rendering. Inheritance refers to deriving from an existing control and overriding implementation (properties, events and methods). Generally, unless you can fulfill the "is-a" relationship you should not inherit from an existing control. For example, if you are writing a custom DropDownList that contains countries, you should not derive from DropDownList. Instead, use aggregation and simply forward calls to the contained DropDownList. Since the inheritance mechanism is straightforward we will not discuss it here. A discussion of composition and rendering follows.

Composition

By definition, a composite control abstracts the functionality of several controls into one. To see an example, we will create a simple date-range (begin date and end date) component (see Figure 1). A date-range component is common in applications where you run database queries and want to restrict the result over a specified date range. Rather than rewriting the same code over and over, it makes perfect since to create a component that abstracts out the concept of a date-range. The ASP.NET framework allows you to create composite controls in two ways: declaratively or programmatically.

Declaratively Creating Composite Components

Just as you create web forms declaratively, you can create reusable components (called user controls) in ASP.NET. The ASP.NET framework identifies a user control by its file extension; user controls are created in separate files and tagged with an ascx extension (pages are tagged with the aspx extension). These user controls can then be included inside web forms pages using the Register directive. User controls, by themselves, are of no use; you have to embed them inside of web forms. Although you can write your user controls in any text editor, the visual studio IDE has built in support for this. When you create a user control using the IDE, the IDE generates two files, the ascx file and the associated code-behind file. Our date-range component is developed as shown in Listing 1.

As you can see, the difference between an aspx page and an ascx page is the directive at the top and the absence of the <html><body> tags. In fact, the aforementioned tags are restricted from ascx files. Just as you can reference controls in the aspx page (or the associated code-behind) you can do the same in ascx files. Our date-range component is developed by composing labels and text boxes (along with two validator server-side controls). The only thing that's missing is that properties that the component exposes to its client. If you think about a date-range component, the only thing clients of your component will be interested in is the beginning date and ending date. Date-range exposes these as properties of the component. Have a look at the code-behind for the user control shown in Listing 2.

Above, we have used the code-behind to write the accessor/mutator methods for the properties of date-range. You could have alternatively written this inline in the ascx file just like you can in aspx pages. As we previously mentioned, clients of your user controls can then embed use your controls by using the Register directive.

<%@ Register TagPrefix="UC" Tagname="DateRangeUserControl" 
Src="DateRangeUserControl.ascx" %>

To use the control, you register the control with a tag prefix, a tag name, and the source for the control. You can then refer to the control in the aspx page as follows:

<UC:DateRangeUserControl ID="dateRangeUserControl" runat="server" 
BeginDate='<%#GetDate()%>' EndDate='<%#GetDate()%>' />

Note the similarities between how we refer to user controls and ASP.NET controls:

<asp:DropDownList Id="." runat="server" />

The reference consists of a tag prefix, asp, and the tag, DropDownList, followed by properties.

Programmatically Creating Composite Components

If you run through the exercise of creating a component as a user control, you will surely agree that the ASP.NET architects have made it very easy. In a matter of minutes you can transform the existing code into user control components.

Creating composite components programmatically is not as easy; you have to do some of the work yourself. To create our date-range component programmatically, all you need is a cs file. The component is implemented as shown in Listing 3.

DateRangeComposite derives from System.Web.UI.Control. When developing your own controls, you'll want to consider deriving from either Control or WebControl. Control is the base of all server controls in ASP.NET. This class provides functionality that is common to all server controls (e.g., ViewState) without any UI (e.g., Background color) functionality. WebControl is a direct derivative of Control and adds some UI facility (for example, BorderWidth). As a general rule, if your control encapsulates controls that have rendering capability, or your control will not render itself, use Control. Conversely, you'll inherit from WebControl when your control needs to render UI. Since DateRangeComposite encapsulates controls that do their own rendering, it derives from Control. DateRangeComposite also implements INamingContainer. INamingContainer is an empty interface that tells the ASP.NET framework to create a naming scope for your control, which ensures that your child controls have unique Ids in the hierarchy of controls. (You'll see the importance of this interface when I talk about events.) Clients of your control will undoubtedly want to query DateRangeComposite for the beginning and ending date. DateRangeComposite exposes, for example, BeginDate as a property and simply forwards the call to the child control. Also note that prior to querying the contents of the begin date TextBox, the accessor calls Control.EnsureChildControls(). EnsureChildControls(), as the name suggests, ensures that the controls for this control have been created. The core work of creating your control comes in overriding the virtual CreateChildControls() method. The ASP.NET framework calls this method when it's time for you to create your control. DateRangeComposite packages its controls in a Table control and then adds the table to its ControlCollection.

Our programmatically created date-range component resides in a DLL. I can embed the component in the web forms now. I first register the component, using the Register directive, and then I refer to the component by tagname and class.

<%@ Register TagPrefix="CC" Namespace="MyControls" Assembly="Controls" %>
.
<CC:DateRangeComposite id="composite" runat="server" Orientation="1" />

Note the difference between the properties used for the declarative version versus this one. Here we specify the namespace and assembly, rather than simply giving it a tag name and the source for the component.

Finally, I have shown that you can create the same date-range component either declaratively or programmatically (so far). The primary advantages of writing your components either way are reuse and maintenance. However, there are issues to consider when deciding between the two. When deciding whether to write your components declaratively or programmatically, you have to ask whether the component is going to be used in one project (possibly in several pages) or if you will need the component in several projects. If your component will be used by several applications, take the time and write the component programmatically. Create an assembly of custom controls and include your component in that assembly. Any project that needs to use the component can then add the custom components assembly to the project. If a change needs to be made to the component, you will only have to change it in one place-the assembly. On the other hand, if you are sure you will only use the component in one project, write the component declaratively. You will still have the component localized in one place.

Rendering Components

Rendering your own controls is considerably more time consuming than composing them. This is because you have to render all of the child controls that compose your control, rather than delegating the rendering to your child controls. The motivation for doing so is that you have total control over how your component is rendered to the client. In today's environment, you cannot assume that your page will be displayed on any one particular device or browser. You can take advantage of the ASP.NET framework to customize the html output (the UI) to particular clients (see Listing 4).

DateRange is a rendered version of DateRangeComposite. To render your control, you override the virtual Render method of Control. Render is called, with an HtmlTextWriter, when it's time for your control to render itself. As with DateRangeComposite, DateRange wraps itself in an html table. The difference is that DateRange pushes out raw html to the client, rather than the child controls rendering themselves. In addition to the form elements (e.g., input tag), DateRange also handles all client-side scripting required by the child controls. As you can see, the real advantage to rendering your own controls comes with this runtime capability. That is, you can query specific browser capabilities (using Page.Request.HttpBrowserCapbilities object) and render browser specific html. Many of the existing ASP.NET controls already do that (e.g., the Calendar control).

Including Custom Events in your Components

Clients of your control will expect feedback from your control. Controls communicate with clients via events. You can define public event(s) in your control and clients of your control will subscribe to be notified when those events occur. To illustrate events, I'll add a button to the date-range component and define a public Click event. For simplicity I have made the event public here. Generally this is not a good idea, you will want to make your events protected and read only. However, for simplicity we have made it public to simplify the discussion. To raise the event and call registered delegates (subscribers), DateRange implements interface IpostBackEventHandler and raises the event (on the server) in method RaisePostBackEvent(...). When the Click event is raised on the server, OnClick invokes all of the registered delegates. Note that the OnClick method first checks to make sure that there are subscribers to the event and then invokes each subscriber's delegate within a try/catch block. Invoking subscriber's delegate within try/catch blocks is a good idea because you cannot be sure that an exception will not be through within one of the handler delegates. To ensure all subscribers are called, invoke delegates within try/catch blocks.

State Management within your Components

When you render your controls, you are also responsible for maintaining control state on post backs. As with writing your own controls, there are several mechanism with which you can maintain a control's state in ASP.NET. The available mechanisms include server-side and client-side state management. Server-side state management is done through the session and/or application object. The client side approach requires you to use the Control.ViewState property or tradional cookies or hidden fields. When you are faced with options in any scenario, you have to weigh the advantages and disadvantages of each option relative to your application. To keep things simple, I'll just say that since server-side state management requires additional server resources, and because ViewState is the approach taken by existing ASP.NET controls, using the ViewState property will suffice for most applications. To manage state, DateRange implements interface IPostBackDataHandler. When form data is posted back to the server, the ASP.NET framework will look at the controls that posted back data and see if they implemented IpostBackDataHandler. If so, the framework calls its LoadPostData method with two arguments: a key that identifies the control (UniqueID) and a NameValueCollection object that hold all incoming values.

Performance/Caching

ASP.NET provides a great framework to develop web pages; however, the great framework comes at a cost. If you think about the construction process of an ASP.NET page, you will see that it is indeed an expensive process. On each postback to the server, the page has to be reconstructed from scratch. That means the page gets instantiated, controls created, viewstate loaded, postback events fired, etc. If you develop some decent sized pages while mindlessly writing server controls, you will surely experience performance problems. There are many ways in which you can directly improve the performance of your pages. One of those is caching. The caching in ASP.NET can be applied to objects (e.g., DataSet), controls, and even entire pages. What's more is that you can decide where the caching occurs. For example, you can specify whether caching occurs on the client, proxy, or the requested server. The only restriction is that the devices participating in caching have to be HTTP 1.1 compliant. Before I can talk about caching controls, I need to show you how caching works on pages. In a dynamic site, you rarely have the option of caching an entire page. However, generally web sites have a welcome page that contains several static images that greet the visitor. Users can then click on one of the images to enter the site. To see how page caching works, I will create a simple welcome page. The welcome page contains two images, a header (used by the entire site) and a welcome image. Clicking on the welcome image, takes you to the custom controls page. Surprisingly, caching, in general, is very easy. Caching an entire aspx page requires you to place the @OutputCache directive on top of the aspx page. You control the how and where the caching is done through the attributes of the directive.

<%@ OutputCache Duration="#ofseconds" 
            Location="Any | Client | Downstream | Server | None" 
            VaryByControl="controlname" 
            VaryByCustom="browser | customstring" 
            VaryByHeader="headers" 
            VaryByParam="parametername" %>

As shown above, the Duration attribute sets the expiration for the specified cache (in seconds). Location specifies where the caching will take place. Specifically, Any means that the cache can be anywhere from the client to the server, Client means only at the client, Downstream means anywhere from the client to the server (e.g., the proxy), Server means only at the requested server, and None specifies no caching (e.g., for a login page). VaryByParam can be used to vary the cache only by a control on the page (partial caching). VaryByCustom can be used, for example, to cache on a browser-specific basis. VaryByHeader is used to cache on the http headers and VaryByParam is used to cache on querystring or posted form parameters. Although caching is easy, you must be aware of the following:

(1) Duration is a required attribute.

(2) The default value for Location is Any and Location can only be applied to pages (not controls).

(3) Values assigned to VaryByHeader, VaryByParam, and VaryByControl can be a semi-colon separated string to indicate more than one.

(4) If you specify caching declaratively for a page, Duration and VaryByParam are required attributes.

(5) VaryByParam is required unless you use VaryByControl.

In the example file HomePage.aspx, I have cached the welcome image. To employ caching on the image, I have to create a user control out of it and then use the outputcache directive (see WelcomeImage.ascx).

<%@ OutputCache Duration="360" VaryByParam="none" %>

The above directive will cache the welcome image for 360 seconds.

Most websites have uniform headers and footers. Therefore, these items are a great target for custom components because they will be used throughout the site. To show an example of how to cache your components programmatically, I have implemented a simple header component (Header.cs). To enable caching on a control, all you have to do is add metadata to the class.

[PartialCaching(240)]
public class Header :Control {.} 

As shown, you can programmatically cache controls via the PartialCaching attribute applied to the control class. The argument passed to PartialCaching is the duration for the cache. A good exercise would be to verify that the controls are really being cached. To do this, you could print out the time on the server (implement Init) on every post back.

As stated above, caching is not limited to just pages and controls. You can cache objects as well. For example, objects such as DataSet can be placed in the Page.Cache collection. This technique is great for database-intensive sites. For example, if I have a DataGrid that allows pagination, I could cache the data source of the grid in the cache and use that data source to move from page to page rather than going to the database on every post back.

Finally, the above discussion on caching does not cover all of the details of caching. The caching facility in ASP.NET is very flexible and would require several documents to cover in its entirety.

Conclusion

I have shown that the ASP.NET environment provides several mechanisms with which you can develop custom components. When deciding between the various mechanisms, you have to weigh the advantages and disadvantages and then choose the appropriate method for your application. Even though, you have several mechanisms available, one thing is for sure, you will want to take advantage of abstracting out your pages into components. In the end, you will gain a much better understanding of how ASP.NET works and ease the pain of maintenance over the course of your development. Moreover, by componentizing your applications, you will reuse your components over and over rather than cutting and pasting the code.

About the Author

Sayed Y. Hashimi has a Masters degree in Engineering from the University of Florida and is currently working as a Software Architect for New Road Software, Inc. He can be reached at shashimi@newroadsoftware.com or hashimi_sayed@hotmail.com.