Topics in This Chapter
Web Client-Server Communications: Web page requests and responses rely on HTML to represent content, and HTTP to define the protocol that governs client-server interaction. JavaScript, ASP, and ASP.NET are compared as ways to develop an application.
Structure of an .aspx Page: An .aspx
Web page is compiled as a class. It contains a variety of directives that control the page's behavior and can link it to external code. This section looks at the basic elements that comprise an .aspx
file, including how Viewstate
is used to maintain state information for the form.
Inline and Code-Behind Pages: ASP.NET offers three models for designing a Web application: inline code, code-behind, and code-behind with partial classes. The latter two permit the page interface to be separated from the program logic. The implementation code is placed in a code-behind file that is referenced by an .aspx
page.
HTML Server-Side Controls and Web Controls: ASP.NET provides controls that correspond to HTML tags and are accessed as classes in the server-side code. It also provides a number of native “Web” controls that include features not found on standard HTML controls. These include validation controls, data source controls, and sophisticated data display controls such as the DataList
, GridView
, and TreeView
.
Master and Content Pages: ASP.NET supports the creation of a master Web page that serves as a template for other “content” pages that visually inherit from it. This chapter explains how to create master pages containing special “place holder” controls that define where derived content pages may insert their custom content.
Developing applications to run on the Internet is a broad topic, and this book devotes its last three chapters to the subject. This chapter introduces key features of ASP.NET and focuses on using controls to create Web pages; Chapter 17, “The ASP.NET Application Environment,” looks at application development issues such as managing sessions and configuring ASP.NET control files; and the book's final chapter discusses Web Services.
ASP.NET is technically regarded as the next generation of ASP. There are syntactic similarities and compatibilities, but the differences are even greater. Thus, this chapter makes no attempt to explain ASP.NET in terms of ASP. There are some comparisons, but no prior knowledge of ASP is assumed. You will also find traces of JavaScript sprinkled in a couple of applications, but the code is easily understood within the context of the examples.
The first section provides a tour of client-server Web interaction. It begins with a simple JavaScript application that demonstrates the fundamental techniques used to transfer information between a client and Web server. It then shows how the ASP.NET model encapsulates these principles and adds an object-oriented approach to Web page design and implementation. Subsequent sections survey the array of Web presentation and validation controls. Special attention is given to the DataList
and GridView
controls.
One note: IIS (Microsoft Internet Information Server) is conspicuous by its absence in our discussion. Although all of the applications were tested in an IIS environment, and the preponderance of ASP.NET applications will run on this Web server, they are not bound to it. Microsoft has created an open-source HTTP server named Cassini that is written in C#. It's fully HTTP/1.1 compliant, supports directory browsing, as well as many of the standard MIME types, and most importantly, supports ASP.NET. It has been tested on Apache servers and is clearly geared toward making ASP.NET the Web development tool of choice for multiple Web platforms.
At its core, the Internet consists of resources and users who want to access Web-based resources. Three mechanisms are required to enable this access: a naming scheme (URI) to locate the resources, a protocol (HTTP) defining how the request/response process works, and a language (HTML) for publishing information and providing a means to navigate to the resources. This environment is quite different than that facing the Windows Forms programmer and presents the Web application developer with three major challenges:
HTML. The developer must be familiar with the Hypertext Markup Language in order to create Web Forms. Certainly, HTML generators are a useful accessory, but at some point, the developer has to understand the raw HTML.
HTTP. The primary task of a Web application is to respond to HTTP requests. These requests may be sent by either an HTTP GET or POST method and may contain headers in addition to the main message body. The language used by the Web developer must provide access to the full HTTP message. For example, to process a received form, a Web application must be able to extract data from fields on the form. Header information can be equally important. It can be used to control caching, identify the source of the request, and pass cookies between the client and server.
State. There is no intrinsic feature in HTML or HTTP that maintains the state of variables during the request/response operation. Figure 16-1 illustrates this. The client receives an empty survey form, fills it out, and posts it back to the server. The server detects an error and returns the same Web page—which is blank again. Preserving data (state) over the round trip(s) between client and server is, perhaps, the biggest challenge facing the Web programmer.
One of the best ways to understand and appreciate a technology is to compare it with other technologies that perform a similar task. With that in mind, this section takes a simple Web application and implements it in JavaScript, ASP, and ASP.NET. The objectives are twofold: to understand the problems associated with implementing a Web Forms application, and to illustrate the evolutionary approach that ASP.NET uses to meet these challenges. You do not need experience with ASP or JavaScript to understand the examples, but it helps. Their function is to demonstrate how the HTTP GET and POST methods send data to a server and maintain state information. The examples could just as easily use Perl or PHP scripting.
This application calculates the Body Mass Index (BMI) for a given height and weight. The user enters this information (see Figure 16-2) into a form and then clicks Submit Form to send the request to a server. The server calculates the BMI and returns a Web page that includes the BMI and original data entered.
Realistically, the calculation could be done using JavaScript on the client's page without requiring a trip to the server. But let's suppose that we also want to record each time the calculator is used. For that, a trip to the server is necessary.
JavaScript remains the primary language used for client-side coding on a Web application. It is platform and browser independent (although there are a few exceptions). As we will see later, ASP.NET automatically generates JavaScript for its client-side processing.
The first example uses JavaScript on both the client and server side. Listing 16-1 contains the HTML and JavaScript that comprise the client-side page. For brevity, some of the less important code is excluded.
The HTML in the <body>
defines the fields and buttons on a form bmi_input
. This form defines GET as the HTML method for delivering the contents of the form to the server. When the button is clicked, control passes to the JavaScript function post
that verifies the content of the form and sends it to the server. We'll get to the other JavaScript code after we look at the server side.
Example 16-1. JavaScript/HTML to Display a BMI Calculator— bmi.htm
<HTML> <HEAD><TITLE>BMI Calculator</TITLE> <SCRIPT LANGUAGE="Javascript" TYPE="text/javascript"> <!— // hrefstr is set to querystring values—if any var hrefstr= location.search.substring(1, location.search.length); function showbmi(){ if (hrefstr ) // display values in form fields { var parms = hrefstr.split('&'), var f = self.document.forms[0]; f.bmi.value = eval(parms[0]); f.hti.value = eval(parms[2]); f.wt.value = eval(parms[3]); f.htf.value = eval(parms[1]); } } // -->Code for Verify goes here. // Post Form to Web Host function post() { if (verify()) //Call function to verify values in form fields { var f = self.document.forms[0]; f.bmi.value=0; f.submit(); // Use HTTP GET to send form to server } } //--> </script> </HEAD> <BODY bgcolor=#ffffff> <FORM NAME="bmi_input" method=GET action=bmicalculator.htm> <table border=0 cellpadding=2 cellspacing=0 width=180 bgcolor=#cccccc> <tr><td colspan=3 align=center><font size=2 color=#33333> <b>BMI Calculator</b> </td></tr> <tr><td><font size=2 ><b>BMI:</b></td> <td colspan=2 ><input type=text size=5 name=bmi> </td></tr> <tr><td colspan=3><hr size=1></td></tr> <tr><td><font size=2 >Height:</td> <td><input type=text size=3 name=htf maxlength=1></td> <td><input type=text size=3 name=hti maxlength=2></td> </tr> <tr><td> </td><td valign=top><font size=2>feet</td> <td valign=top><font size=2>inches</td> </tr> <tr><td><font size=2 >Weight:</td> <td colspan=2><input type=text size=3 name=wt maxlength=3></td> </tr> <tr><td colspan=3 align=center> <INPUT TYPE="button" VALUE="Submit Form" ONCLICK= "self.post()";> </td></tr> <tr><td colspan=3> </td></tr> </table> </FORM> <SCRIPT LANGUAGE="Javascript" TYPE="text/javascript"> <!-- showbmi(); // Fills form with values if they exist //--> </script> </body> </html>
Using the GET method causes the form's data to be sent to the server as part of the URL. This string of data, referred to as a query string, contains name-value pairs that correspond to the name of the fields on the form, followed by their value. Figure 16-3 shows how form data is passed to the Web page bmicalculator.htm
that calculates the BMI.
Data is returned to the client in the same manner—by appending it to the URL of the page containing the original form. Note that bmi
is now set to the calculated value. Here is the server-side code that creates this response:
<html><head></head> <body > <script language="javascript"> <!— // Use location.search to access the query string var hrefstr = location.search.substring(1, location.search.length); var parms = hrefstr.split('&'), feet = parms[1]; inch = parms[2]; pounds = parms[3]; totinches = eval(feet)*12 + eval(inch); // ..... Calculate BMI var h2 = totinches * totinches; bmi = Math.round(eval(pounds) * 703 * 10/ h2)/10; // --> Place code here to maintain count of visits. //... Return value and original parameters as a query string ndx = hrefstr.indexOf('htf'), self.location = 'bmi.htm?bmi= '+bmi+"&"+hrefstr.substring(ndx); //--> </script> </body></html>
This code grabs the values from the query string using location.search
, parses them, calculates the BMI, and then creates a URL for the page to be returned. The final step is for the client browser to display the calculated BMI value.
At the bottom of the code in Listing 16-1 is a call to the JavaScript function showbmi()
. This function operates much like the preceding server code. It extracts the data from the query string and displays it in the appropriate fields on the form. Note that the function first
confirms that the query string is not empty, as will be the case the first time the form is loaded.
The use of a query string is popular for applications that transfer small amounts of data. However, it becomes a problem with large forms because the query string is limited by the 2K maximum string length imposed by some browsers (IE). In addition, it raises obvious security concerns by exposing data in a URL line that can be altered by the user. Query string encryption can mitigate this problem and should be considered where it makes sense. A more robust solution is to use the HTTP POST method in place of GET.
JavaScript is intended primarily for client-side scripting and cannot process data sent by a POST method. This example replaces the JavaScript server code with an ASP file that handles the POST request. We change one statement in the client code to indicate that POST is being used and to provide the name of the ASP server page:
<FORM NAME="bmi_input" method=POST action=bmicalculator.asp>
The code for the ASP server file is quite simple. It accesses the data passed from the client by referencing a request
object. The data is passed to a VBScript function that calculates the BMI. A URL is then constructed that contains the query string with the BMI value and other parameters. The response.redirect
method sends the form to the client's browser.
<script language="VBScript" runat="server"> function getBMI(inch,feet,pounds) totinches = feet*12 + inch h2 = totinches * totinches getBMI= (pounds * 703 * 10/ h2)/10 end function </script> <% ' ... POST data is available in request object inch = request("hti") feet = request("htf") pounds = request("wt") bmi= left(cstr(getBMI(inch,feet,pounds)),4) ' ... return value and original parameters as a query string hrefstr = "&htf=" +cstr(feet) + "&hti=" + cstr(inch) + "&wt="&cstr(pounds) response.redirect ("bmi3.htm?bmi="+bmi+hrefstr) %>
This solution illustrates the fundamental ASP approach of using VBScript to interact with HTTP response and request objects. In addition, HTML and JavaScript can be intermixed with the ASP code to create Web Forms. Although this offers a degree of flexibility, it often results in a babel of code and inconsistent coding techniques.
Before building an ASP.NET solution for the calculator, let's first take a general look at the features of ASP.NET and how they affect Web Forms programming. One way to understand the Web Forms model is to look at the requirements faced by the ASP.NET designers—and how they met them.
Integration with the .NET environment. Web Forms are built on the Common Language Runtime (CLR) and have access to all types in the Framework Class Library, as well as special namespaces, such as System.Web
and System.Web.UI
that support Web applications. An important aspect of .NET integration is that it is required only on the server side; users are not required to install the .NET Framework on their computers. The emphasis is on rendering code that runs unaided on a browser. In fact, the emitted code is tailored to recognize the browser it is running on, so it can take full advantage of its features.
Linking client-side HTML controls with server-side controls. All controls in ASP.NET are classes. Controls that appear on a browser window are renderings based on HTML tags. To reconcile this differing technology, ASP.NET includes server-side controls that map directly to HTML tags. For example, the <input type = text>
tags we saw in the earlier examples are represented by the HtmlInputText
class found in the System.Web.UI.HtmlControls
namespace. When information on a Web page is sent to a server, all HTML tags designated to run on the server are converted into their corresponding .NET class and compiled as members of the Web page. We'll see how HTML tags are mapped to server controls in the next example.
Compiled rather than interpreted Web pages. Traditionally, Web pages have been rendered by JavaScript, VBScript, Perl, and other scripting interpreters. In contrast, an ASP.NET Web page consists of a user interface defined by HTML, and interface logic written in a .NET language such as C# or VB.NET. The first request for an ASP page (.aspx
file) results in the page being compiled into a .NET class. Further requests are then handled by the assembly created by compilation. For the user, it means faster Web access; for the developer, it means applications are developed using the same .NET tools available for desktop and component applications.
ASP.NET offers three models for implementing a Web application:
Inline code. The HTML markup code and application code (C#) coexist in a single .aspx
file.
Code-behind. The markup code and application code are placed in separate files. The markup is in an .aspx
file and the logic code resides in a .cs
or dll
file.
Partial classes. This is a variation of the code-behind model that places the markup and code in separate files. The difference is that the code-behind file is implemented using partial classes. It is stored as a .cs
file and is compiled along with the markup file. This model is available only with ASP.NET versions 2.0 and later.
None of the models offers a performance advantage over the others. This leaves the choice of model up to one's preference—or need—for code separation. Let's now examine the models by using each to implement the BMI application.
The code for the BMI Web Form application is shown in Listing 16-2. Although it resembles an HTML page, an .aspx
page is actually an XML-formatted page. It is processed on the server and used to generate a mixture of HTML and JavaScript that is sent to the browser.
Notice that this example contains actual C# code—between the <script> </script>
tags—that calculates the BMI value. This method is executed on the server and is not visible as source code to the client.
Example 16-2. ASP.NET Inline Code Implementation of a BMI Calculator
<%@ Page Language="C#" %> <HTML> <HEAD><TITLE>BMI Calculator</TITLE> <script runat="Server"> // Calculate BMI from values on form private void getBMI(object sender, System.EventArgs e) { try { decimal f = Convert.ToDecimal(htf.Value); decimal inch = Convert.ToDecimal(hti.Value); decimal w = Convert.ToDecimal(wt.Value); decimal totinches = f * 12 + inch; decimal h2 = totinches * totinches; decimal massIndex = (w * 703 * 10/ h2)/10; bmi.Value = massIndex.ToString("##.##"); }catch (Exception ex) { bmi.Value=" "; } } </script> </HEAD> <BODY bgcolor=#ffffff> <FORM NAME="bmi_input" runat=server> <table border=0 cellpadding=2 cellspacing=0 width= 180 bgcolor=#cccccc> <tr><td colspan=3 align=center><font size=2> <b>BMI Calculator</b> </td></tr> <tr><td><font size=2 ><b>BMI:</b></td> <td colspan=2 ><input type=text size=5 id=bmi runat=server></td></tr> <tr><td colspan=3><hr size=1></td> </tr> <tr><td><font size=2 >Height:</td> <td><input type=text size=3 id=htf maxlength=1 runat=server></td> <td><input type=text size=3 id=hti maxlength=2 runat=server></td> </tr> <tr><td> </td><td valign=top><font size=2>feet</td> <td valign=top><font size=2>inches</td> </tr> <tr><td><font size=2 >Weight:</td> <td colspan=2><input type=text size=3 id=wt maxlength=3 runat=server></td> </tr> <tr><td colspan=3 align=center> <INPUT TYPE="button" VALUE="Submit Form" OnServerClick="getBMI" id=bmiButton runat=server> </td> </tr> <tr><td colspan=3> </td> </tr> </table> </FORM> </body> </html>
To understand the differences between standard HTML and ASP.NET code, let's compare how controls are specified in Listing 16-1 versus Listing 16-2.
The most obvious difference is the addition of the runat=server
attribute in the <Form>
and <Input>
tags. This designation converts any HTML elements to HTML server controls that can be processed by the server-side code prior to sending them to the browser. The following code illustrates how an <input>
tag is transformed before it emitted to the client's browser:
ASP.NET statement: <input type=text size=3 id=wt maxlength=3 runat=server> HTML statement emitted: <input name="wt" id="wt" type="text" size="3" maxlength="3" value="168" />
The <Form>
tag does not include the method or action attribute. By default, the POST method is used. As the following code shows, the page is self-referencing, which means the page sends the form to itself.
ASP.NET statement: <FORM NAME="bmi_input" runat=server> HTML statement emitted: <form name="_ctl0" method="post" action="bmi.aspx" id="_ctl0">
The <button>
tag contains an OnServerClick
event delegate. This indicates that a method on the server will handle the event. The resulting HTML references a JavaScript function that has been added to post the form when the button is clicked.
HTML statement emitted:
<input language="javascript" onclick =
"__doPostBack('_ctl1','')" name="_ctl1"
type="button" value="Submit Form" />
ASP.NET also adds three hidden fields to the HTML page it returns to the browser:
<input type="hidden" name="__EVENTTARGET" value="" /> <input type="hidden" name="__EVENTARGUMENT" value="" /> <input type="hidden" name="__VIEWSTATE" value="dDwxMzc5NjU4NTAwOzs+iIczTTLHA74jT/02tIwU9FRx5uc=" />
The first field, __EVENTTARGET
, specifies the control that invoked the request (known as a postback) to the server; the second, __EVENTARGUMENT
, contains any parameters required by the event. The __VIEWSTATE
field is by far the most interesting.
View state is the feature in ASP.NET that automatically maintains the state of server-side controls (controls declared with runat=server
) as a form makes the round trip between the client and the server. In other words, it allows a page to place data in a control such as a ListBox
or GridView
one time—usually when the page is first loaded—and ensures the data is retained as subsequent postbacks occur. Here is how it works.
When a page is posted back to the server, the data in the hidden __VIEWSTATE
field is deserialized and used to set the state of controls and the overall Web page. Data received in the HTTP request as part of a POST operation is used to set the values of those related controls (note that for controls whose contents are posted—TextBox
, CheckBox
, RadioButtons
—the posted data overwrites the view state data). The __VIEWSTATE
field is then updated before it is passed back to the client. The returned view state value plays no role on the client other than to represent a snapshot of control values at the time the page is received by the browser.
Because the view state string can be viewed in a source listing, questions about security become a legitimate issue. However, unlike the query string, the value is not represented as clear text.
value="dDwxMzc5NjU4NTAwOzs+iIczTTLHA74jT/02tIwU9FRx5uc="
By default, a machine-specific authentication code is calculated on the data and appended to the view state string. The full string is then Base64 encoded. It is possible to decode the string, but the difficulty in doing so will thwart most casual efforts. Tampering with the string can also be detected by the server and results in a security exception being thrown. As always, use Secure Sockets Layer (SSL) to ensure absolute security for Internet communications.
Maintaining view state data within a Web page makes the page independent of the server. This means that a Web page request can be sent to any server in a Web farm—rather than restricting it to a single server.
Performance is another issue that must be considered when working with the view state value. By default, it maintains data for all server-side controls on the form. The control information is not limited to only the data value associated with the control. For example, when a DataGrid
is used, the view state includes not only the data in each cell, but also column and row headers, and related style attributes. The view state data can easily add several thousand bytes to a Web page and slow performance.
To improve performance, you may want to disable view state for the Web page and apply it only to selected controls. Set the EnableViewState
attribute in the @Page
directive to disable it at the page level:
<%@ Page Language="C#" EnableViewState="False" %>
Then, to enable view state for an individual control, apply the attribute as shown in the following code:
<input type=text size=3 id=wt maxlength=3 EnableViewState=true runat=server >
Of course, you can also take the opposite tact and leave view state on for the page and turn it off for selective controls.
The decision to enable or disable view state is one of the key decisions in designing a Web page that displays large amounts of data in a server-side control. The easiest approach is to allow ViewState
to take care of the details. However, this can result in a large HTML payload being transferred repeatedly between browser and server. An alternative is to design the code to reload the data into the controls on each postback. The data may be fetched from the original source—usually a database—or it may be stored on the server in session state or a cache. These last two options are described in Chapter 17, “The ASP.NET Application Environment.”
ViewState
maintains not only a control's data but also its state. For example, it keeps track of the last item(s) selected in a ListBox
and permits the ListBox
to be redisplayed in its most recent state. The drawback of manually repopulating a control, rather than using ViewState
, is that this state information is lost.
The last element to discuss in Listing 16-2 is the @Page
directive. An .aspx
file can contain only one @Page
directive, and it is typically—although not required—the first statement in the file. The purpose of this directive is to assign attribute values that control the overall behavior of the Web page, as well as specify how the page is assembled and compiled on the server. Table 16-1 lists selected attributes for the @Page
directive.
Table 16-1. Attributes for the @Page
Directive
Attribute/Value | Description |
---|---|
| Specifies the type of access the page has to the session state information. Sessions are discussed in Chapter 17. |
| Enables or disables view state for the page. Individual controls can override this value. |
| Is used to make the view state more secure by adding a validator hash string to the view state string that enables the page to detect any possible attempt at corrupting original data. |
| Setting this to
|
| Specifies the URL of a Web page that is called when an unhandled exception occurs. |
A culture setting for the page based on the | |
| Specifies the user interface culture for this page. Example: |
| Turns tracing on or off for the page. Default is |
| Specifies the base class used to generate a class from the |
| Specifies the “master page” from which the current page visually inherits its layout. Introduced with 2.0. |
| Specifies the subdirectory containing the |
| Specifies the language for inline code. |
| Specifies the name of a compiled code-behind file. This file must be in the |
| Specifies a code-behind file containing a partial class. Introduced with 2.0. |
| Specifies a code-behind file containing source code. |
The Codebehind
, Codefile
, and Src
attributes specify the assembly or source file containing the business logic code for the page. Instead of placing code between <script></script>
tags as we did in Listing 16-2, the code is placed in a separate code-behind file that is referenced by these attributes. Before discussing code-behind, let's look at some additional directives that are frequently used in .aspx
pages.
This directive is used to import a namespace in an .aspx
page. It serves the same purpose as the C# using
statement.
<%@ Import namespace="System.Net" %>
Several namespaces are automatically imported in a page, making this directive unnecessary in most cases. These namespaces include
System
, System.IO
System.Web.UI, System.Web.UI.HtmlControls
, System.Web.UI.WebControls
System.Web
, System.Web.SessionState
, System.Web.Caching
System.Text
, System.Text.RegularExpressions
This directive links an assembly to the current page while the page is being compiled. This provides the page with access to all types in the assembly. It takes two forms:
<%@ Assembly Name="webfunctions" %> <%@ Assembly Src="webfunctions.cs" %>
The first version references an assembly that may be private or deployed in the Global Assembly Cache; the second statement causes the source to be dynamically compiled into an assembly that is linked to the Web page. Note that assemblies in the application's in
subdirectory are automatically linked to a page and do not need to be referenced.
This directive associates alias names with namespaces and classes. Its purpose is to provide a convenient syntax for adding custom controls to a Web page. The directive takes two forms:
<%@ Register Tagprefix="tagprefix" Namespace="namespace" Assembly="assembly" %> <%@ Register Tagprefix="tagprefix" Tagname="tagname" Src="pathname" %>
Attributes:
| Alias for a namespace. |
| The namespace to associate with |
| Assembly in which namespace resides. |
| Alias to associate with a class. |
| The file containing the user control |
The first form of the directive is used to add an ASP.NET server control to a page; the second form is used with a custom control contained in a source file. In the latter case, the TagPrefix
and TagName
are always used together as a colon-separated pair. Here is a code segment that places a user control defined in the file hdr.ascx
on a Web page. The @Register
directive defines the alias pair that is used to declare the control on the Web page.
<%@ Register TagPrefix="uc1" TagName="hdr" Src="hdr.ascx" %> <form id="Form1" method="post" runat="server"> <uc1:hdr id="Hdr1" runat="server"></uc1:hdr> </form>
We'll make use of this directive in Section 16.4, which provides examples of how to create and use custom controls. Note that @Register
directive information also can be stored in the Web.config
file (see Chapter 17), eliminating the need to place it in a Web page.
The example in Listing 16-2 contains both C# to implement program logic and HTML to render the user interface. A Web page can also be configured as an .aspx
file, containing only the interface code and a separate code-behind file that contains the program logic and serves as the base class for compiling the .aspx
file (see Figure 16-4). This code-behind file takes the .cs
extension.
The code-behind page is linked to the .aspx
file as an assembly or source file using the Codebehind
or Src
attributes of the @Page
directive. If the Codebehind
attribute is used, the assembly must be stored in the in
directory of the application.
Let's now look at how the code in Listing 16-2 can be changed to use a code-behind file. We create a code-behind file named bmicb.cs
(see Listing 16-3) to replace the code currently between the <script/>
tags. The @Page
directive links this file to the .aspx
file:
<%@ Page Language="C#" Src="bmicb.cs" Inherits="BMI" %>
The code-behind page is always structured as a class whose name must be specified by an Inherits
attribute. This class is shown in Listing 16-3. Let's take a close look at it, because knowledge of how the code-behind file and the .aspx
file interact on the server is essential to understanding the ASP.NET Web page model.
Example 16-3. Code-Behind File for BMI Calculator—bmicb.cs
using System; using System.Web.UI.HtmlControls; public class BMI : System.Web.UI.Page { // <input type=text id=htf runat=server> protected HtmlInputText htf; // <input type=text id=hti runat=server> protected HtmlInputText hti; // <input type=text id=wt runat=server> protected HtmlInputText wt; // <input type=text id=bmi runat=server> protected HtmlInputText bmi; // <input type="button" VALUE="Submit Form" id=bmiButton // runat=server> protected HtmlInputButton bmiButton; override protected void OnInit(EventArgs e) { // Delegate to handle button click on client bmiButton.ServerClick += new EventHandler(getBMI); } protected void getBMI(object sender, System.EventArgs e) { decimal f = Convert.ToDecimal(htf.Value); decimal inch = Convert.ToDecimal(hti.Value); decimal w = Convert.ToDecimal(wt.Value); decimal totinches = f * 12 + inch; decimal h2 = totinches * totinches; decimal massIndex = (w * 703 * 10/ h2)/10; bmi.Value = massIndex.ToString("##.##"); } }
The first thing to observe from this listing is that it consists of one class—BMI
—that derives from the System.Web.UI.Page
class.
The Page
class is to Web Forms what the Form
class is to Windows Forms. Like the Form
, it has a sequence of events that are fired when it is initialized and loaded. It also has several properties that control its behavior—many of which correspond to the @Page
directive attributes already discussed. We'll look at the Page
class later in this section.
One of the trickiest aspects of learning ASP.NET is grasping how server-side controls work—specifically, how the content and action of controls displayed on a browser are managed on the server. Figure 16-5 illustrates the relationship. Each server control is declared as a field in the BMI class. When the values of the controls are posted to the server, they are assigned as field values. In this example, all of the controls are HTML controls—that is, standard HTML controls with the runat=server
attribute.
The id
value in the tag must match the field name identically. The field types are defined in the System.Web.UI.HtmlControls
namespace. Each HTML control has a one-to-one mapping with an HTML tag, as shown in Table 16-2.
Table 16-2. HTML Controls and Their Tags
Control | HTML Tag |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HtmlTableRow HtmlTableCell |
<tr> <td> |
|
In our example, clicking the Submit button sends a request to the server to calculate and return a BMI value based on the form's content. The .aspx
code for the button looks like this:
<INPUT TYPE="button" VALUE="Submit Form"
id=bmiButton runat=server>
Compare this with the tag defining the button in Listing 16-2:
<INPUT TYPE="button" VALUE="Submit Form" OnServerClick="getBMI" id=bmiButton runat=server>
This earlier code defines a method (getBMI
) to be called when the click event occurs. Because our current example has the method placed in a code-behind file, there is no reference to it in the .aspx
file. Instead, the server-side code handles the event using a standard delegate-based event handling approach. An event handler method (getBMI
) is defined that matches the signature of the EventHandler
delegate. Then, using the button's id
value, we create a delegate instance that registers the method for the ServerClick
event of the bmiButton
control:
bmiButton.ServerClick += new EventHandler(getBMI);
When the button is clicked on the browser, the contents of the form are posted to the server, which recognizes the button click event and calls the appropriate event handler. This raises one obvious question: Because a form can contain any number of buttons, how does the server determine the event that triggered the post-back? The answer is that the name of the control causing the event is passed to the server in the __EVENTTARGET
hidden field that was discussed earlier. This is handled automatically by ASP.NET; the developer's responsibility is to create the delegate and server-side control event handler.
The problem with the preceding code-behind model is that each control in the markup page must be explicitly mapped to a protected member in the code page. Changes to the markup page require that the code page members be kept in sync. The use of a partial class in the code-behind page eliminates the need for the protected class member. The partial class is compiled with the markup page, which permits the markup and code sections to directly access each other's members. The effect is the same as using the inline model. To demonstrate this, extract the getBMI
method from the inline code (refer to Listing 16-2) and place it in its own file inside a partial class. The result is a code-behind partial class as shown in Listing 16-4.
Example 16-4. Code-Behind Partial Class File for BMI Calculator
//file: bmi.aspx.cs
using System;
partial class BMICalc: System.Web.UI.Page{}
{
void getBMI (Object sender, EventArgs e)
{
try
{
decimal f = Convert.ToDecimal(htf.Value);
decimal inch = Convert.ToDecimal(hti.Value);
decimal w = Convert.ToDecimal(wt.Value);
decimal totinches = f * 12 + inch;
decimal h2 = totinches * totinches;
decimal massIndex = (w * 703 * 10/ h2)/10;
bmi.Value = massIndex.ToString("##.##");
} catch (Exception ex)
{ bmi.Value=" "; }
}
}
The client markup page links to this code by specifying the file name and the partial class name in its @Page
declaration:
<%@ Page codefile="bmi.aspx.cs" inherits="BMICalc" %>
ASP.NET 2.0 continues to support the original code-behind model, but it should be used only for preexisting code. New development should employ the inline or partial class model.
The first time an ASP.NET page is accessed, it is parsed and compiled into an assembly. This is a relatively slow process that results in the delay one notices the first time the .aspx
page is called. Subsequent requests receive much faster responses because the assembly handles them. This assembly consists of a single class that contains all of the server-side code, as well as static HTML code.
The compiled class derives either directly from the System.Web.UI.Page
class or indirectly via an intermediate code-behind class. It is important to understand the members of the Page
class. Its methods and properties define much of the functionality the .aspx
code relies on for handling requests, and its events define junctures at which the code must perform initialization and housekeeping tasks.
Table 16-3 summarizes some of the important properties of the Page
class.
Table 16-3. Selected Properties of the Page
Class
Control | HTML Tag |
---|---|
| Returns the |
| Boolean value that indicates whether controls retain their values between requests. Default is |
| Boolean value indicating whether the page is being loaded and accessed for the first time or in response to a postback. |
| Provides a reference to the page originating the call. Information on the calling page is available when the page is reached by cross-page posting or is invoked by |
| Gets the |
| Gets the |
| Gets the |
| Gets the |
The Application
and Session
properties provide state information for a Web application and are discussed in the next chapter.
The Request
and Response
properties expose underlying objects (HttpRequest
and HttpResponse
) that are used to interact with the incoming HTTP request and the outgoing HTTP response. These classes include properties and methods to read cookies from the client's machine, receive uploaded files, identify the browser, get the IP address of the client, and insert custom content into the outgoing HTTP body. Chapter 17 discusses these in detail, but as an introduction, let's look a simple code example that illustrates the fundamentals of accessing these objects within an .aspx
page.
The next code segment uses the Response.Output.Write
method to write information into the HTTP content returned to a browser. The information comes from Request.Browser
properties that indicate the type of client. Note that by using <% %>
tags in an .aspx
file, we can intersperse these statements with the HTML code. Do this judiciously. An .aspx
file is much easier to read when the C# code is segregated between <script>
tags, as shown in Listing 16-2, or placed in a code-behind file.
<table border=0 cellpadding=0 cellspacing=0>
<%
Response.Output.Write(@"<tr><td>Browser Version: {0}
</td></tr>", Request.Browser.Type); // IE6
%>
</table>
This useful property enables a Web application to distinguish between a postback and the first time a Web page is invoked. It lets the program know when to initialize values that only need to be set once. A typical example is a ListBox
that contains unchanging values, such as all of the states in the United States. When assigned, these values are subsequently maintained in the __VIEWSTATE
hidden field and do not need to be re-initialized.
To demonstrate, let's extend our BMI inline code example to display the date and time when the user first requests the Web page. When displayed, this date and time remain unchanged no matter how many times the BMI calculation is subsequently requested.
The date and time are displayed using a <span/>
tag, which is added beneath the opening <FORM>
tag (refer to Figure 16-2) in the bmi.aspx
file. (This is equivalent to a Web control Label
.)
<FORM NAME="bmi_input" runat=server>
<span id=sessionstart runat=server/><br>
In the code section, we must assign the date and time to the inner contents of the <span>
tag the first time that the page is called. Here is the code to do this:
void Page_Load(object sender, EventArgs e)
{
if(!this.IsPostBack) sessionstart.InnerText =
DateTime.Now.ToString();
}
Recall that the code on the server is compiled into a class that inherits from the Page
class. This class includes several events—discussed in the following section—that are triggered when a request is sent to the Web page. The most useful is the Page_Load
event that is raised each time the page is loaded. Applications typically include an event hander for it that checks IsPostBack
and performs initialization if the call is not a postback. In this example, the InnerText
field of the <span>
tags is set to the date and time the first time the page is loaded.
The preceding example demonstrates how the Page_Load
event handler provides a convenient place to initialize variables. The Load
event is only one of four events defined by the System.Web.UI.Page
class. The others are Init
, PreRender
, and UnLoad
. These occur in a fixed sequence as shown in Figure 16-6.
To best understand the role of each, let's look at what happens when a form is posted to a server:
The temporary class for the page is generated, compiled, and loaded. The ASP.NET runtime begins processing the page and fires the Page.Init
event.
The state of the page object and its controls are set from data in the POST variables and the __VIEWSTATE
field. The Page.Load
event is fired. Typically, the OnLoad
event handler is overridden to initialize control values and establish database connections.
Events related to all controls are fired. The last event that occurs is the one that caused the postback. After all server-side control events have been processed, the Page.PreRender
event occurs.
The page enters its rendering phase. The class that represents the page calls the Render method on all the individual controls included in the page.
The HTTPResponse
is issued, sending the rendered HTML to the client. The Page.Unload
event completes the process. This event should be used to release resources by closing files and database connections.
Although Web Forms provide an event-based model that is similar to the Windows Forms model, the key difference is that the events do not actually fire until control returns to the server. There they must occur in the fixed order described in this section. Your code interacts with these events by overriding or replacing the event handlers for the events described in Figure 16-6.
A Web page in .NET 1.x could only post to itself. In 2.0, you can designate a target page, other than the current page, by setting the PostBackUrl
property of a Button
, ImageButton
, or LinkButton
to the address of the target page. Posting occurs when the button is clicked. To demonstrate, the button defined in this code posts the contents of the current form to nextstep.aspx
when the button is clicked.
<asp:button id="postBtn" text="redirect" postbackurl="nextstep.aspx" runat="Server"> </asp:button>
The page to which a form is posted can view the contents of the calling form using the PreviousPage
property. In this example, nextstep.aspx
uses PreviousPage
to get the text value of the button that initiated the post. Of course, this technique would be used more often to gather data from text boxes on the posted form.
if(!this.IsPostBack)
{
// Sets text to the value of the Text property of the button
// from the calling form having the id postBtn: "redirect"
string text =
((Button)PreviousPage.FindControl("postBtn")).Text;
}
The designer of an ASP.NET Web page can choose from three types of controls for the GUI design: client-side HTML controls, HTML server controls, and Web Forms controls. Client-side controls are the traditional controls defined by HTML tags (<table>
, <button>
, <input>
, and others). These controls are still useful for client-side form validation and creating hyperlinks.
HTML server controls are server-side versions of the standard HTML controls (see Listing 16-2). As we have seen, they're created by adding a runat = server
attribute to the HTML tag. The main reason they are included in .NET is to ease the transition from legacy HTML files to ASP.NET. However, except for incurring less overhead, there is no real advantage in using HTML server controls. Web controls—a much more powerful alternative—are nearly as easy to implement and unlock the full capabilities of ASP.NET.
Web controls are native ASP.NET controls that extend the features of the HTML server controls and add non-HTML controls, such as a calendar and data grid. They are actual classes in the .NET Framework and expose properties that enable the developer to exert much more control over their appearance and behavior than is possible with server controls. With the exception of the Table
—the HTML table is easier to work with for general use—Web controls should be the control of choice for ASP.NET pages.
Web controls behave much like the HTML server controls: Each has a corresponding class; those with a user interface render themselves in HTML understandable by a browser; and most expose events that are handled on the server. A significant difference from the HTML controls is that Web controls provide a richer object model. From the WebControl
class, they inherit properties that affect their appearance: ForeColor
, BackColor
, BorderColor
, BorderStyle
, Height
, and Width
. In addition, many can be bound to a data source that provides their content.
A Web control is declared using the asp:
prefix. For example, a TextBox
and Button
are declared as
<asp:TextBox id="First Name" type="text" runat="server" /> <asp:Button id="Save" Text="Save Data" runat="server" OnClick="SaveBtn_Click" />
ASP.NET Web controls are defined in the System.Web.UI.WebControls
namespace. There are a lot of them—more than 70 with the release of ASP.NET 2.0. They naturally fall into functional categories that provide a convenient way to present them (see Figure 16-7):
Simple controls. Enhanced alternatives to the HTML Server controls and standard HTML tags.
List controls. These inherit directly from the System.Web.UI.WebControls.ListControl
class and are populated by ListItem
objects.
Data Display controls. These complex controls are designed to display multiple rows of data (GridView
, DataList
, Repeater
) or multiple fields for a single row of data (FormView
, DetailsView
).
Data Source controls. Introduced with ADO.NET 2.0, these controls serve as source of data for other controls—primarily the data display controls—that bind to them. As their names imply, these controls provide data from a variety of sources.
Validation controls. These helper controls perform predefined and custom data validation on other controls.
Login controls. A set of controls designed to control the login process by managing and authenticating users.
In addition to these, there are a few of highly specialized controls: Calendar
, AdRotator
, and FileUpload
. This section provides examples of commonly used visual controls, as well Validation controls and the new (.NET 2.0) Data Source controls. Before examining specific controls, let's look at the properties shared by all Web controls that govern their appearance.
All Web controls inherit numerous properties from the base WebContro
l class that can be used to set their appearance and behavior. The properties are most effective when used to decorate simple controls where their effect is more pronounced than with complex controls. Table 16-4 demonstrates how these properties can be used to alter the appearance of a simple Button
control.
Table 16-4. Properties That Affect the Appearance of a Web Control
Property | Button1 | Button2 | Button3 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Button Displayed |
Other useful properties include Enabled
, Visible
, and TabIndex
. The latter indicates the tab order of a control. Note that browsers may render these properties differently or ignore some of them altogether. For example, Firefox and Netscape tend to ignore the Width
property.
The simple controls are typically used in combination to create an interface for users to enter data or make selections. The easiest way to present and manage controls that are related by function and style is to place them in a common container. In traditional HTML, the <DIV>
element is used for this purpose; in ASP.NET, the panel
control serves as a generic container (and is often rendered in browsers as a <DIV>
element). There are many advantages to using a panel to layout controls:
It eliminates the need for multiple Web pages. Because ASP.NET is designed around posting back to the calling Web page, it's an easy way for a single Web page to present multiple interfaces by simply toggling panels' visibility on or off.
Controls on a panel maintain data that can be referenced even if the panel is not visible. This eliminates the use of multiple hidden fields.
It serves to unify the appearance of grouped controls by providing a common background or border. A screen is easily sectioned into multiple panels.
The screens in Figure 16-8 are created using a combination of button, label, text box, and panel controls. The page consists of a form that accepts name and address information. Controls to accept name fields are contained on one panel, whereas address fields are on another. Clicking the Name
and Address
buttons toggles the visibility property of the two panels. The effect is to reuse the same display space for both types of data—obviating the need for a long scrolling form or multiple Web pages.
A look at the underlying code reveals the syntax and mechanics of working with Web controls. We'll look at the three most interesting areas of the code: the Page_Load
event handler, the button declarations and event handlers, and the layout of the panels.
When the page is loaded for the first time, the panel that accepts name fields, pnlName
, is made visible, whereas pnlAddress
has its visibility turned off.
The two menu buttons across the top of the form are declared as
<asp:Button ID="btnName" CommandName="name" OnCommand="Button_Command" text="Name" Runat="server" /> <asp:Button ID="btnAddress" CommandName="address" OnCommand="Button_Command" text="Address" Runat="server" />
The buttons specify Button_Command
as an event handler to be called when each button is clicked. As shown in the event handler code, the buttons' CommandName
property identifies the button raising the event. This enables a single method to handle multiple button clicks, which is a useful way to group code performing similar tasks. It's most commonly used with buttons that control column sorting in a grid.
void Button_Command(Object sender, CommandEventArgs e) { // CommandName identifies control invoking event switch(e.CommandName) { case "address": pnlName.Visible = false; pnlAddress.Visible = true; break; case "name": pnlAddress.Visible= false; pnlName.Visible = true; break; default: break; } }
The Clear and Submit buttons use the OnClick
property—rather than OnCommand
—to specify their Click
event handlers as separate methods:
<asp:Panel id="pnlBottom" style="Z-INDEX:103; LEFT:20px;POSITION:absolute; TOP: 240px" runat="server" BackColor=#cccccc Height="26px" Width="278px"> <asp:Button id="btnClear" Text="Clear" OnClick="clear_Form" runat="server" /> <asp:Button ID="btnSubmit" Text="Submit" Font-Bold="true" OnClick="store_Form" runat="server" /> </asp:Panel>
The most important thing to note about the event handlers is their signature. The Click
event requires an EventArgs
type as the second parameter; the Command
event requires a CommandEventArgs
type.
private void clear_Form(object sender, System.EventArgs e) { if(pnlName.Visible) { txtFirstName.Text =""; txtLastName.Text =""; txtMiddleName.Text =""; } else { // Clear fields on pnlAddress } } private void store_Form(object sender, System.EventArgs e) { // Code to verify and store Form }
ASP.NET 2.0 adds an OnClientClick
property to the Button
, ImageButton
, and LinkButton
controls, which can be used to execute client-side script. The following code causes a JavaScript function to be executed when the button is clicked.
<asp:Button id=btn text="Client Click" OnClientClick="popup()" runat="server" />
The panel declaration specifies its location, size, and appearance attributes such as background color. Controls are placed on the panel by declaring them within the <asp:Panel />
tags. In this example, the text box and label controls are members of the panel's control collection:
<asp:Panel id="pnlName" runat="server" style="Z-INDEX: 101; LEFT: 20px; POSITION: absolute; TOP: 64px" BackColor = "LightGreen" Height="160px" Width="278px"> <TABLE> <TR> <TD><asp:Label id="lblFirstName" Runat="server" text="First Name:"></asp:Label></TD> <TD><asp:TextBox id="txtFirstName" MaxLength=30 Runat="server"></asp:TextBox></TD></TR> <TR> <TD><asp:Label id="lblMiddleName" Runat="server" text="Middle Name:"></asp:Label></TD> <TD><asp:TextBox id="txtMiddleName" MaxLength=30 Runat="server"></asp:TextBox></TD></TR> <TR> <TD><asp:Label id="lblLastName" Runat="server" text="Last Name:"></asp:Label></TD> <TD><asp:TextBox id="txtLastName" MaxLength=30 Runat="server"></asp:TextBox></TD></TR> </TABLE> </asp:Panel>
It is interesting to note how ASP.NET tailors the HTML code to capabilities of the client browser. The HTML returned to Internet Explorer renders the panel as a <div>
element.
<div id="pnlName" style="background-color:LightGreen; height:160px;width:278px;Z-INDEX: 103; LEFT: 20px; POSITION: absolute; TOP: 64px">
The Firefox and Netscape browsers receive HTML in which the panel is rendered as a table:
<table id="pnlName" cellpadding="0" cellspacing="0" border="0" bgcolor="LightGreen" height="160" width="278" style="Z-INDEX: 101; LEFT: 20px; POSITION: absolute; TOP: 64px"><tr><td>
With the exception of the MaxLength
property that limits the amount of text entered in the control, the text box controls in this example rely on default property values. However, they can be customized easily to present a more meaningful interface. The most useful properties include Width
and Column
, which both specify the width of the control (Column
is the better choice since all browsers recognize it); ReadOnly
, which can be used to prevent the user from changing the content; Rows
, which specifies the number of rows in a multi-line text box; and TextMode
, which indicates whether the text box is SingleLine
, MultiLine
, or contains a Password
. In the latter case, text entered into the text box is masked.
The text box also supports an OnTextChanged
property that specifies an event handler method to call when text in the box is changed. However, this is a delayed event that is not processed on the server until a round-trip event occurs. You can prevent the event from being delayed by adding the AutoPostBack = true
property to the control's declaration. However, for best performance, the program should be designed to process all control content in one trip.
The four ASP.NET list controls—DropDownBox
, ListBox
, CheckBoxList
, and RadioButtonList
—provide alternative ways of representing a collection of data. All are data-bound controls that provide a visual interface for an underlying ListItemCollection
; and all are derived from the System.Web.UI.WebControls.ListControl
class that contributes the properties and methods used to populate the controls with data and detect selected control items (see Figure 16-9).
Individual data items within a list control are represented as a ListItem
control. There are several ways to specify the text displayed for an item in the list control. The most common method is by placing text between the opening and closing tags of the ListItem
control—referred to as the inner HTML content. You can also use the Text
property to specify the text displayed in the list control for the item.
<asp:RadioButtonList id="RadioButtonList" BackColor="LightBlue" RepeatColumns=1 RepeatDirection="Vertical" runat="server"> <asp:ListItem Value="1">Rembrandt /> <asp:ListItem Value="2">Courbet /> <asp:ListItem Value="3" Text="Manet" /> <asp:ListItem Value="4">Degas /> </asp:RadioButtonList>
The ListItem
control also exposes a Value
property that allows you to associate a value with the item in the list control, in addition to the text displayed in the control. This is often used as a key when retrieving data related to the selected item from a database table.
As mentioned, list controls are data bound, which means they expose a DataSource
property that can be set to reference a collection of data. The control's DataBind
method is called to load the data from the source into the control. It is possible to bind to many kinds of data sources, such as database tables, hash tables, XML files, and even other controls.
As an example, let's declare a ListBox
that will be bound to an array:
<asp:ListBox id="ListBox" runat="server" Rows=4 Width=150px> </asp:ListBox>
In the script section of the .aspx
page, we define an array and bind its contents to the ListBox
. The DataBind
method copies the data from the array into the control, creating a ListItem
collection (see Figure 16-9).
<script runat="Server">
void Page_Load(object sender, EventArgs e) {
string[] artists = new string[4] {"Rembrandt","Courbet",
"Manet","Degas"};
//
if( !this.IsPostBack) {
// Bind the first time the page is loaded
ListBox.DataSource = artists;
ListBox.DataBind();
}
}
Note that the binding occurs only when the page is loaded the first time. ViewState is used to populate the control on subsequent postbacks.
When an item in a list control is selected, the SelectedIndexChanged
event occurs. A postback to the server occurs only if the control specifies an event handler to be called, and sets the AutoPostBack
attribute to true
.
<asp:ListBox id="ListBox" runat="server" SelectionMode="Multiple" Rows=4 AutoPostBack="true" OnSelectedIndexChanged="ShowSelections" Width=150px> </asp:ListBox>
The event handler declaration has the familiar parameters of the EventHandler
delegate declaration.
public void ShowSelections(Object sender, EventArgs e) { Label1.Text=""; foreach (ListItem item in ListBox.Items) { if(item.Selected) { Label1.Text += item.Text + "<br>"; } } }
The event handler iterates through the items in the ListBox
and displays those that are selected. By default, the ListBox
permits only one item to be selected, but this can be overridden by setting SelectionMode
to Multiple
.
A single selected item is available through three properties: SelectedIndex
returns the index of a selected item in the list; SelectedItem
returns the entire item; and SelectedValue
returns the value of the selected item. If there are multiple selected items, these properties return information on the selected item having the lowest index.
The DataList
control makes it easy to display data in a repeating pattern. The idea is to create a template that defines the layout for items it contains. Each “item” is defined as a mixture of visual Web controls and HTML. Item content comes from one or more fields in a data source. When an instance of the DataList
control is set to a data source, such as a DataReader
, each row of the data source is represented as an item in the DataList
. The DataList
is processed much like a ListBox
control: Items.Count
provides the number of items in the control, and the SelectedIndex
and SelectedItem
properties reference a specific item.
To illustrate the use of this control, let's create a Web page that lists DVDs for sale. The items for sale are displayed in two-column rows as shown in Figure 16-10. The row/column layout is specified in the DataList
declaration:
<ASP:DataList id="MyDataList" RepeatColumns="2" RepeatDirection="Horizontal" OnItemCommand="Item_Command" runat="server">
The RepeatColumns
property specifies the number of columns displayed, and RepeatDirection
indicates the direction in which items are displayed. OnItemCommand
specifies the method to be called when a button in the DataList
control is clicked. In this example, the ItemCommand
event fires when the user adds an item to the cart. Here is an example of the code to handle this event:
private void Item_Command(object source, System.Web.UI.WebControls.DataListCommandEventArgs e) { // (1) Crucial: select an item in the DataList MyDataList.SelectedIndex = e.Item.ItemIndex; // (2) Get the value of a control in the selected item string id= ((Label) MyDataList.SelectedItem.FindControl("movID")).Text; }
An item selected in the DataList
is indexed by the SelectedIndex
property. After an item is selected, the FindControl
method can be used to obtain a reference to any control in the item definition. In this case, the value of the Label
containing the movie's ID is assigned to a variable.
The ItemTemplate
shown in Listing 16-5 describes the appearance of each item. The most important thing to note in this code is the use of DataBinder.Eval
to bind to a property or column in the data source. As shown here, the method has two overloads, one of which takes a formatting parameter.
<%# DataBinder.Eval(Container.DataItem, "movie_ID") %> <%# DataBinder.Eval(Container.DataItem,"r_price", " {0:c2}") %>
Note that ASP.NET 2.0 offers a simpler, but equivalent version:
<%# Eval("movie_ID") %>
Either construct is replaced by the corresponding data from the data source. It can be displayed directly in the HTML stream or assigned to a control's property.
Example 16-5. Defining an ItemTemplate for a DataList
Control
<ItemTemplate> <table cellpadding=10 style="font: 10pt verdana" width=380> <tr> <td width=1 bgcolor="00000"/> <td valign="top"> <a href=showdvd.aspx?id=<%# DataBinder.Eval( Container.DataItem, "movie_ID") %>> <img align="top" border=0 src='./images/<%# DataBinder.Eval( Container.DataItem, "movie_ImgID") %>' ></a> </td> <td valign="top"> <b>Title: </b><%# DataBinder.Eval( Container.DataItem, "movie_title") %><br> <b>ID: </b> <asp:Label id="movID" Text=<%# DataBinder.Eval(Container.DataItem, "movie_ID") %> runat="server" > </asp:Label></b><br> <b>Year: </b><%# DataBinder.Eval( Container.DataItem, "movie_Year") %><br> <b>Price: </b><%# DataBinder.Eval(Container.DataItem, "r_price", " {0:c2}") %><br> <%# DataBinder.Eval(Container.DataItem, "movie_shortdesc") %> <br> <asp:Button Value="Select" Text="Add to Cart" BackColor="#bda563" CommandName="cart" runat=server> </asp:Button> <br> </td> </tr> </table> </ItemTemplate>
The easiest part of working with a DataList
is binding it to a data source. In this example, we use a DataReader
to load rows from a database. To display these rows as items in the control, set the DataSource
to the reader and then bind it:
rdr= cmd.ExecuteReader(); MyDataList.DataSource= rdr; MyDataList.DataBind();
The contents of the DataList
can be changed at any time by reassigning and binding a new data source.
DataBinder.Eval
and Eval
use reflection to perform late-bound evaluation. The advantage of this approach is that it can determine the data type (making casting unnecessary) and has a simple formatting syntax. The disadvantage is that it is much slower than an early-bound approach. As an alternative—particularly when formatting is not needed—consider this early-bound syntax:
<%# ((IDataRecord)Container.DataItem)["movie_ID"] %>
IDataRecord
casting is used when a DataReader
is the data source. Cast with DataRowView
when the data source is a DataSet
.
In summary, the DataList
acts as composite control that can be configured to display data in just about any format. In addition to the ItemTemplate
, it supports header, footer, edit, and select templates that can be used to expand its capabilities beyond those described in this section.
Data binding enables the contents of a control to be populated by data from a designated data source. Technically speaking, the data source is any collection of data that implements the IEnumerable
interface. In simple cases, the source may be an object such as an Array
, ArrayList
, Hashtable
, or SortedList
. More often, the data comes from an ADO.NET DataTable
, DataSet
, or IDataReader
object. The control may bind directly to one of these objects, or indirectly, using the special data source controls introduced with ASP.NET 2.0.
The data reader provides a forward-only, read-only resultset that is the most efficient way to present data. It is generated by issuing the ExecuteReader
method of the IDbCommand
object. A control binds to the results by setting its DataSource
property to the data reader object and executing its DataBind
method.
The example in Listing 16-6 illustrates how a data reader is used to populate a ListBox
with movie titles from the Films
database (described in Chapter 10). The values displayed in the ListBox
are based on the value assigned to the DataTextField
property—in this case, the Movie_Title
column in the movies
table. The DataValueField
property specifies an additional column value that is assigned to each item in the ListBox
. When an item is selected, this latter value is returned by the SelectedValue
property.
Example 16-6. Data Binding a List Box to a Data Reader
<%@ Page Language="C#" %> <%@Import namespace="System.Data.SqlClient" %> <%@Import namespace="System.Data" %> <html> <body> <head><TITLE>Bind Films data to a ListBox</TITLE> <script runat="Server"> void Page_Load(object sender, EventArgs e) { if(!this.IsPostBack) { getMovies(); } } private void getMovies() { string cnstr=GetConnString(); // Get a connection string SqlConnection conn = new SqlConnection(cnstr); IDataReader rdr=null; IDbCommand cmd = new SqlCommand(); cmd.Connection= conn; conn.Open(); cmd.Connection=conn; cmd.CommandText="SELECT movie_id, movie_title FROM movies ORDER BY AFIRank"; conn.Open(); rdr = cmd.ExecuteReader(); // Bind DataReader to ListBox ListBoxMovie.DataSource = rdr; ListBoxMovie.DataBind(); conn.Close(); rdr.Close(); } </script> </head> <FORM NAME="FORM1" runat=server> <asp:ListBox id="ListBoxMovie" dataValueField = "movie_ID" dataTextField = "movie_title" AppendDataBoundItems=true Rows="10" BackColor=#efefe4 font-size=9pt runat="server" > <asp:ListItem Value=-1 Text="Select Movie" /> </asp:ListBox> </FORM> </body> </html>
Observe the presence of the AppendDataBoundItems
property in the ListBox
declaration. This property, a new feature added by ASP.NET 2.0, provides a way to specify whether data binding should overwrite any preexisting items in a list control. This is particularly useful for placing an entry in a list that precedes the actual data. In the preceding example, the ListBox
declaration includes a ListItem
that causes Select Movie
to be placed in it first row.
It is often preferable to bind a server control to a data reader, rather than to a DataSet
. To understand why, let's compare the two (see Figure 16-11). The data reader connects to a table and streams data into the control when the DataBind
method is called. A data set, on the other hand, is a cache in memory that is filled with the resultset when the DataAdapter
.Fill
method is invoked. Its contents are then copied into the control when the control's DataBind
method is called. In a WinForms application, the data set remains available to the application until it is closed; in a Web application, the data set disappears after a reply is sent to the browser. As we discuss in the next chapter, it can be saved as a Session
variable, but this requires memory and can lead to scalability problems.
There are a couple of situations in which using a DataSet
makes sense. One is when multiple controls are bound to the contents of a DataSet
. In this code segment, a data set is built that contains a list of all the movies in a table. Two ListBox
controls are then populated with different views of the data. One ListBox
contains movies produced prior to 1951, and the other contains those produced on or after that year. The advantage of the DataSet
in this case is that only one query is applied against the database.
string sql ="SELECT movie_id, movie_title,movie_year FROM movies ORDER BY movie_year"; SqlDataAdapter da = new SqlDataAdapter(sql,conn); DataSet ds = new DataSet(); da.Fill(ds,"Movies"); // DataSet and DataTable DataView dview = new DataView(ds.Tables["Movies"]); dview.RowFilter = "movie_year < 1951"; // List box containing movies before 1951 ListBoxMovie.DataSource= dview; ListBoxMovie.DataBind(); // List box containing movies produced after 1950 dview.RowFilter = "vendor_name > 1950"; ListBoxMovie2.DataSource= dview; ListBoxMovie2.DataBind();
A DataSet
is also useful when the Web application is designed as a three-tier Web site in which the presentation layer accesses a database through an intermediate data access layer. The data access layer contains a method or methods that return a DataSet
in response to the call.
Let's look at how the .aspx
file in Listing 16-6 can be converted from its current two-tier structure to a three-tier design. The first step is to remove the getMovies
method and place it in a separate assembly. In the following code, the method is part of the DataMethods
class and rewritten to return a data set containing the vendor data. This code is compiled into a DLL file and placed in the bin
subdirectory below the Web page.
// datalayer.dll – place in in subdirectory below application
using System.Data.SqlClient;
using System.Data;
namespace myUtil{
public class DataMethods{
public DataSet getMovies(){
string cnstr= GetConnString(); // Get a connection string
SqlConnection conn = new SqlConnection(cnstr);
IDbCommand cmd = new SqlCommand();
cmd.Connection= conn;
string sql="SELECT movie_id, movie_title FROM
movies ORDER BY AFIRank";
SqlDataAdapter da = new SqlDataAdapter(sql,conn);
DataSet ds = new DataSet();
da.Fill(ds,"Movies");
return (ds);
}
}
}
The code changes in the .aspx
file are minimal. An @Import
directive is added so that the namespace in datalayer.dll
can be accessed.
<%@Import namespace="myUtil" %>
The getMovies
call is replaced with the following:
if(!this.IsPostBack) { DataMethods dm = new DataMethods(); DataSet ds = dm.getMovies(); ListBoxMovie.DataSource = ds; ListBoxMovie.DataBind(); }
The use of a data presentation layer promotes code reusability, hides the messy ADO.NET connection details, and results in cleaner code.
As we have seen, ASP.NET makes it easy to bind a control to data by simply setting the control's DataSource
property to the collection of data it is to display. However, it is still up to the developer to assemble the collection of data. For example, using the data reader as a data source requires the following pattern of operations:
Create a data connection.
Create a Command
object.
Build a query and use the Command
object to retrieve data into a data reader.
Data source controls encapsulate the functionality required to perform these operations—eliminating the need for coding by the developer. The data-bound control is no longer bound to the data collection, but to a data source control. To illustrate, Figure 16-12 shows how a grid can be populated by binding it to a data reader or a SqlDataSource
control.
Data controls are not limited to database access. In fact, ASP.NET 2.0 supports data source controls that attach to six types of data:
AccessDataSource
. Binds to a Microsoft Access database.
DataSetDataSource
. Binds to non-hierarchical XML data.
ObjectDataSource
. Binds to data through custom classes implemented in a data access layer.
SiteMapdataSource
. Binds to XML site maps.
SqlDataSource
. Binds to a SQL database.
XmlDataSource
. Binds to XML documents.
We'll look at the SqlDataSource
, ObjectDataSource
, and XmlDataSource
controls in this section.
This control represents a connection to a relational data store, such as SQL Server, DB2, or Oracle. It requires a .NET managed data provider with the capability to return a SQL resultset.
The SqlDataSource
control is declared using standard Web control syntax.
Control Declaration:
<asp:sqldatasource runat="server" id="controlID"
Properties:
| Connection string to access database. |
| Managed provider. Default is |
Controls how the select command retrieves data. Is a | |
| True or false. Default is |
| How long the contents of the data source aremaintained in memory. Value is in seconds. |
| SQL statement that retrieves data from associated data store. |
| SQL statement to delete row(s) from data store. |
| SQL statement to insert row(s) into data store. |
| SQL statement to update row(s) in data store. |
The four command properties are strings that contain either a SQL command or the name of a stored procedure (if the database supports it) to be executed. Each command can contain parameters whose values are defined by an associated collection of parameters. In an upcoming example, we'll see how the SelectParameters
collection is used with SelectCommand
. Before that, let's look at how the SqlDataControl
is used to populate a Web control.
As in our earlier example, Listing 16-7 fills a ListBox
with the name of movies from the Films
database. However, in place of raw ADO.NET coding, it defines a data source control and assigns to its SelectCommand
property a SQL select string that retrieves a list of movies from the database. A ListBox
control is declared with its DataSourceID
property set to the ID of the data source control. When the page is loaded, the list control is populated with the resultset.
Example 16-7. Binding a ListBox
to a SqlDataSource
Control
<%@ Page Language="C#" %> <%@Import namespace="System.Data.SqlClient" %> <%@Import namespace="System.Data" %> <HTML> <HEAD><TITLE>Using a DataSource Control</TITLE> </HEAD> <body> <form id="Form1" runat="server"> <asp:sqldatasource runat="server" id="SqlDataSource1" connectionstring="SERVER=(local);DATABASE=FILMS; Integrated Security=SSPI; " providername = "System.Data.SqlClient" selectCommand= "SELECT movie_ID, movie_Title FROM movies ORDER BY AFIRank" > </asp:sqldatasource> <table border=0> <tr><td> <asp:ListBox runat="server" id="ListBoxMovie" dataSourceid = "SqlDataSource1" dataValueField = "movie_ID" dataTtextField = "movie_Title" /> </td></tr> </table> </form> </body> </html>
Let's extend this example so that when a movie is selected from the list, its cast members are displayed. To do this, we add a GridView
, as shown in Figure 16-13.
The data for the GridView
also comes from a new data source control. The purpose of this control is to dynamically retrieve a list of actors for any movie selected in the ListBox
. The challenge is to identify the movie selected and to specify it in the query. Here's how it's done.
The query assigned to the SelectCommand
property contains a parameter (@movieID
) that serves as a placeholder for the actual movie ID. Within the data source control is another control, ControlParameter
, that has the same name as the parameter in our query. It links the data source control to the ListBox
via two properties: ControlID,
which specifies the ListBox
ID, and PropertyName
, which specifies the value to assign to the query parameter. When an item in the ListBox
is selected, ASP.NET replaces the parameter with the current SelectedValue
of ListBoxMovie
(which is a movie ID) and executes the query to retrieve the actor data.
<asp:SqlDataSource ID="ActorSource" RunAt="server" connectionstring="SERVER=(local);DATABASE=FILMS; Integrated Security=SSPI; " SelectCommand= "SELECT actor_first, actor_last, actor_sex FROM actor_movie LEFT JOIN actors ON actor_movie.actor_ID= actors.actor_ID WHERE movie_ID=@movieID"> <SelectParameters> <asp:ControlParameter Name="movieID" ControlID="ListBoxMovie" PropertyName="SelectedValue" </asp:ControlParameter> </SelectParameters> </asp:SqlDataSource>
The GridView
control contains three columns that are bound to the data source control:
<asp:GridView ID="MovieGridView" DataSourceID="ActorSource" Width="100%" runat="server" AutoGenerateColumns="false" SelectedIndex="0" AutoGenerateSelectButton="true" > <Columns> <asp:BoundField HeaderText="First Name" DataField="actor_first" /> <asp:BoundField HeaderText="Last Name" DataField="actor_last" /> <asp:BoundField HeaderText="Sex" DataField="actor_sex" /> </Columns> </asp:GridView>
The ObjectDataSource
is used when data is retrieved through a data access layer, rather than directly from the database. To demonstrate, recall the three-tier structure we created earlier by placing the getMovies
method in a separate assembly. An ObjectDataSource
control can be declared to access this method by setting its typename
field to the class containing the method and its selectmethod
field to the name of the method.
<asp:objectdatasource id="ObjectDataSource1" runat="server" typename="myUtil.DataMethods" selectmethod="getMovies"> </asp:objectdatasource>
This could be used to populate the ListBox
control in Listing 16-7 by replacing the SqlDataSource
control with the ObjectDataSource
control and resetting DataSourceID
to ObjectDataSource1
in the ListBox
declaration.
It is also worth noting that an ObjectDataSource
can be used to fetch data from a Web Service. Because Web Services (described in Chapter 18) are nothing more than classes that expose remotely accessible methods, the typename
and selectmethod
properties can be used to refer to the class and method as if they were in a local assembly.
The XmlDataSource
control is likely to be the most popular of the data source controls. It reads XML data from a local file or a stream transmitted across a network. It's particularly useful for handling the XML formats that are becoming standards for exchanging information across the Internet. To illustrate, we'll create an example that uses the control to read data in the increasingly popular RSS format.
RSS, which stands for Really Simple Syndication, is an XML formatting standard designed originally for news feeds. However, it is now used as a generic way for Web sites to periodically publish information feeds that can be picked up by RSS readers. It not only provides an easy way to distribute data, but the simple format makes it easy for the recipient to determine when any updates have occurred. Because the data is sent as an XML stream, the XmlDataSource
control can play the role of a simple RSS reader. As an example, we pair it with a DataList
control to capture and display a sample RSS feed from the BBC news network (see Figure 16-14).
The underlying XML conforms to the RSS standard (several versions are now in use). At the top level is the <rss>
element that contains a required version attribute. Subordinate to it is a single <channel>
element that contains a description of the channel along with its content. The content is supplied by <item>
elements that have three mandatory subelements: <title>
, <link>
, and <description>
. Their purpose should be clear from the portion of the XML feed shown here:
<rss version="0.91"> <channel> <title>BBC News | Science/Nature | World Edition</title> <link> http://news.bbc.co.uk/go/click/rss/0.91/public/- /2/hi/science/nature/default.stm </link> <description>Updated every minute of every day</description> <item> <title>Huge 'star-quake' rocks Milky Way</title> <description> Astronomers say they are stunned by the explosive energy released by a super-dense star on the far side of our galaxy. </description> <link> http://news.bbc.co.uk/go/click/rss/0.91/public/- /2/hi/science/nature/4278005.stm </link> </item> <item> ... other items go here </channel> </rss>
Listing 16-8 shows how the XML is displayed using only a DataList
and XmlDataSource
control. The XmlDataSource
control identifies the data source with the DataFile
property. As mentioned, this can be a file or a URL. The purpose of the XPath
property is to set a filter for the XML document so that only a subset of the document is returned. In this case, we are interested in only the <item>
data.
The DataList
control identifies the data source component it is bound to by setting the DataSourceID
property to the component's ID—XmlDataSource1
. The XPathBinder
object is used to select items or nodes from the XML document. Its XPath
method returns a single node, whereas the XPathSelect
method returns an ArrayList
of matching values. Both take an XPath
expression (see Chapter 10) to identify the desired item.
<%# XPath("xpath-expression"[, "format"]) %> <%# XPathSelect("xpath-expression") %>
Example 16-8. Displaying RSS Feed with a DataList
and XmlDataSource
Control
<asp:DataList ID="DataList1" Runat="server" RepeatColumns=1 RepeatDirection="Horizontal" GridLines="Horizontal" BorderWidth="1px" BackColor="White" CellPadding="2" BorderStyle="None" BorderColor="#E7E7FF" DataSourceID="XmlDataSource1"> <ItemTemplate> <asp:HyperLink ID="HyperLink1" Runat="server" Text=<%# XPath("title") %> NavigateUrl=<%# XPath("link") %> Target="_blank" Font-Names="Sans-Serif" Font-Size="X-Small"> </asp:HyperLink><br/> <i><%# XPath("description")%></i><br /><br /> </ItemTemplate> <AlternatingItemStyle BackColor="#F7F7F7"> </AlternatingItemStyle> <ItemStyle ForeColor="#4A3C8C" Font-Size=9pt BackColor="#E7E7FF"> </ItemStyle> <HeaderTemplate>BBC RSS Feed: Nature</HeaderTemplate> <HeaderStyle ForeColor="#F7F7F7" Font-Bold="True" BackColor="#4A3C8C"> </HeaderStyle> </asp:DataList> <asp:XmlDataSource ID="XmlDataSource1" Runat="server" XPath="rss/channel/item" DataFile= "http://news.bbc.co.uk/rss/newsonline_world_edition/ science/nature/rss091.xml"> </asp:XmlDataSource>
By binding a data component to a sophisticated visual data control, we are able to create an application in which all data binding is specified through declarations. This can eliminate the need for code to input data, iterate through it, and parse it into a format that can be displayed. In addition, data components have built-in caching features that improve the efficiency of accessing data and eliminate code-managed caching. We look at caching in the next chapter.
One of the more frustrating Internet experiences is to fill in a long form, submit it, and—after a lengthy wait—have it rejected due to an invalid field entry. Any well-designed form should attempt to avoid this by including client-side verification to check fields before the form is submitted. Validation is typically used to ensure that a field is not empty, that a field contains a numeric value only, that a phone number or credit card has the correct format, and that an e-mail address contains the @
character. JavaScript is traditionally used for this purpose.
ASP.NET offers validation controls as a flexible alternative to implementing your own client-side JavaScript functions. The purpose of these controls is to perform a specific type of validation on an associated control. For example, a RequiredFieldValidator
control checks an input control to ensure it is not empty.
Table 16-5 lists the six built-in validation controls along with their unique properties and values.
Table 16-5. Validation Controls
Control | Properties | Description and Possible Values |
---|---|---|
RequiredField Validator | Checks whether input control field contains a value. | |
| Compares the value of an input control with a constant or other control. | |
| Has value of:
| |
|
| |
| Constant value used for comparison. | |
| Other control to compare value with. | |
| Checks the value of the input control against a range of values. | |
| Constant value that represents upper value. | |
| Constant value that represents lowest value. | |
|
| |
RegularExpression Validator | Matches the value of the input control against a regular expression. | |
| Regex to match input control's value against. | |
| Checks a field's value against custom validation logic. | |
| JavaScript client-side function to perform validation. | |
| Server-side method to perform validation. | |
| Collects and lists all the error messages from the form validation process. | |
| Format of error messages:
| |
| Title for error message summary. | |
| Display errors in pop-up box: | |
| Display errors on control: |
Only the final control in the table, ValidationSummary
, does not perform a validation. Instead, it displays a summary of the errors generated by the other validation controls.
Validation controls are used only with controls that have a single input field type. These include the HTMLInputText
, HTMLSelect
, TextBox
, DropDownList
, and ListBox
controls. ASP.NET implements the validation function on both the server and client side. To handle the client side, it includes a .js
file containing validation code in the response to the browser. Note, however, that if scripting is disabled on the browser, only client-side checking will occur.
To illustrate the mechanics of using a validation control, here is the code to associate a RequiredFieldValidator
and RangeValidator
control with a TextBox
control. The range validator control uses its MaximumValue
and MinimumValue
properties to ensure that the value entered in the text box is a numeric value from 0 to 12.
<td><asp:TextBox Width=30 id=hti maxlength=2 runat=server></td> <asp:RequiredFieldValidator id="htivalidator" runat=server ControlToValidate="hti" ErrorMessage="Must enter height value." Display="dynamic"> </asp:RequiredFieldValidator> <asp:RangeValidator id="htirangevalidator" ControlToValidate="hti" MaximumValue="12" MinimumValue="0" Type="Integer" Display="dynamic " ForeColor="Blue" ErrorMessage="Invalid Height." runat=server> </asp:RangeValidator>
This example also illustrates useful properties that all validator controls inherit from the BaseValidator
class:
ControlToValidate
. Set to the identifier of the control to be validated.
Display
. static
, dynamic
, or none
. This specifies how the error message takes up space on the form: static
reserves space and makes the message visible when it is needed; dynamic
uses no space on the form until it is displayed; none
is used when the error message is to be displayed by the ValidationSummary
control.
ForeColor
. Sets the color of the error message text. Red is the default.
ErrorMessage
. The message displayed when a validation exception is detected.
ValidationGroup
. A string
value that enables validation controls to be grouped by assigning the same value to this property. When a Button
control that has its ValidationGroup
property set to a group value submits a form, only controls with validators in the group are validated. This allows sections of a page to be validated separately. This property is introduced in ASP.NET 2.0.
Of the controls listed in Table 16-4, the CustomValidator
and ValidationSummary
exhibit unique behavior that requires further explanation.
There are many common validation patterns that the built-in validation controls do not handle. For example, the contents of one control may be dependent on another, such as when a credit card number format depends on the type of card selected. Another common example is to require that a string entered in a field conform to a minimum and maximum length. Cases such as these are handled with a CustomValidator
control that points to server-side and/or client-side routines that implement custom validation logic. To demonstrate how this control works, let's use it to validate a field that accepts a password, which must be between 8 and 12 characters in length.
The declaration of the control is similar to that of the other validation controls. The main difference is the ClientValidationFunction
and OnServerValidate
fields that specify client and server routines to validate the associated password field.
<input type=password id="pw" runat="server" /> <br> <asp:CustomValidator id="pwvalidator" ControlToValidate="pw" ClientValidationFunction="checkPWClient" OnServerValidate="checkPW" Display=dynamic ErrorMessage="A password must be between 8 and 12 characters." runat="server"/>
The validation routines contain identical logic. The client side is written in JavaScript and the server side in C#. They are contained in separate <script/>
blocks in the .aspx
file.
<script language=javascript> <!— // Client side function to check field length function checkPWClient(source, args) { var pw = args.Value; var ln= pw.length; args.IsValid=true; if(ln <8 || ln > 12) args.IsValid=false } //--> </script> <script Language="C#" runat="Server"> private void checkPW(object source, ServerValidateEventArgs args){ if(args.Value.Length<8 || args.Value.Length>12) {args.IsValid=false; } else{ args.IsValid=true; } } </script>
Two parameters are passed to the validation routines: source
and args
. Args
is the more important of the two. Its value
property exposes the content of the form field being validated. In this example, the length of the field value is checked. If it falls within the bounds, IsValid
is set to true
; otherwise, it is set to false
. A false
value triggers the error message defined by the CustomValidator
control.
For consistency with the built-in validation controls, include both server- and client-side routines for custom validation. If only one is to be implemented, server side is always preferred.
This control collects all of the error messages generated by the validation controls and displays them as a list or customizable paragraph. The messages are displayed either within the control (as a <span>
element within the HTML) or as a pop-up window by setting the ShowMessageBox
to true
.
<asp:ValidationSummary id=validsumm runat="server"
ShowMessageBox=true
DisplayMode=List
ShowSummary=false>
</asp:ValidationSummary>
The most important factor to consider when deciding how to display validation error messages is that the ValidationSummary
control displays messages when a form is submitted, and individual validation controls display a message when their associated control loses focus. Thus, displaying the message at a validation control provides immediate feedback to the user. Also note that if the validation control has its Display
property set to static
or dynamic
, the error message is displayed by both the validation control and the ValidationSummary
control.
A principal design objective when creating a multi-page Web site is visual and functional consistency. Headers and footers should look the same, and the layout and use of controls should be similar from page to page. One way to achieve this is to drag and drop common controls on to each new Web page; but in many cases, a better approach is to create a template from which new pages can be derived. In Windows Forms programming, a form can be filled with controls and used as a base class to create other interfaces. ASP.NET has a similar feature known as master pages. The idea is to create a template, or master page, containing visual elements that will be common to other pages, as well as placeholders that will be filled in with the unique content from other pages. Pages that provide content for the master page are referred to as content pages.
The major advantages to this approach are that any changes made to a master page are automatically reflected in the content pages and that the act of creating content pages is limited to providing statements that specify the content to be associated with the placeholders in the master page.
To illustrate the fundamentals of using master pages, we'll create a master page that defines the layout for the Web page shown in Figure 16-15. In addition, we'll create two content pages that correspond to the menu items shown on the left side of the page.
Master pages are so similar to regular .aspx
pages that converting an .aspx
page to a master page requires only three changes: the file must have the .master
extension; the @Page
directive is replaced with the @Master
directive; and one or more ContentPlaceHolder
controls are added to serve as containers that will be filled at runtime with content from content pages.
Listing 16-9 shows the master page used by content pages to create the Web page in Figure 16-15. The two menu items are implemented as HyperLink
controls that reference the two content pages. Two ContentPlaceHolder
server controls designate the area in the page where a content page's title and main body are placed.
Example 16-9. Master Page Definition—shell.master
<%@ Master %> <html> <head> <title>Master Page for Shell Design Studio</title> </head> <body bgcolor=#ffffff link=#ffffff alink=#ffffff vlink=#ffffff> <FORM id="mainform" runat="server"> <table width="500" cellpadding="0" cellspacing="0"> <tr><td bgcolor=black align=center> <img src=./images/sdslogo.gif> </td><td> </td><td> <font size=4 face=Verdana> <b>SHELL</b> DESIGN STUDIO </td></tr> <tr> <td width=120 height=300 bgcolor=red valign=top> <asp:HyperLink id="homepage" NavigateUrl="home.aspx" Text="Home Page" Font-Bold="true" Font-Size=9pt Font-Names="Verdana" runat=server /> <br><br> <asp:HyperLink id="clients" NavigateUrl="clients.aspx" Text="Our Clients" Font-Bold="true" Font-Size=9pt Font-Names="Verdana" runat=server /> </td> <td> </td> <td valign=top> <hr size=1 color=red> <asp:contentplaceholder id="Header" runat="Server"> <b>Introduction</b> </asp:contentplaceholder> <hr size=1 color=red> <asp:contentplaceholder id="PageBody" runat="Server"> This is Default Content to be overridden by content pages </asp:contentplaceholder> </td></tr> </table> </FORM> </body> </html>
A content page is an .aspx
file containing <asp:Content>
tags (instances of Content
controls) that override corresponding <asp:contentplaceholder>
tags in the master page. The ContentPlaceHolderID
property of the content tag matches the ID
of the placeholder where the content is to be inserted. The home.aspx
content page in our example illustrates this. It contains two content tags that define the content for the Header
and PageBody
placeholders, respectively. The masterpagefile
attribute specifies the master page from which this page inherits.
[home.aspx] <%@ Page language="C#" masterpagefile=~/shell.master %> <asp:content id="Header" runat="server" contentplaceholderid="Header"> <font size=3 face=Verdana> <b>Introduction </b> </asp:content> <asp:content id="MainBody" runat="server" contentplaceholderid="PageBody"> <font face=Verdana size=2> Shell Design Studios specializes in interior decorating for homes and offices. Our staff contains experts in art, color theory, architectural design and home technology. </asp:content>
The content may consist of any combination of standard HTML markup code, images, managed code, and server controls. In this example, the MainBody
placeholder is replaced with literal text for the home.aspx
page and a list of clients—using the <UL>
tag—for the clients.aspx
content page.
[clients.aspx] <%@ Page language="C#" masterpagefile=~/shell.master %> <asp:content id="Header" runat="server" contentplaceholderid="Header"> <font size=3 face=Verdana> <b>Our Clients </b> </asp:content> <asp:content id="MainBody" runat="server" contentplaceholderid="PageBody"> <font face=Verdana size=2> <ul> <li>Swanburg Medical </li> <li>Lombard & Gable Law </li> <li>Coble Architectural Design</li> </ul> </asp:content>
There are only a few commonsense rules to keep in mind when using master/content pages:
A content page does not have to provide content for all placeholders. When content is not mapped to a placeholder, its default value is used.
Content may include ASP.NET server controls. However, controls cannot be placed outside of the content tags.
A placeholder's ID is unique. You cannot, for example, map the same content to two sections in a master page.
Recall that .aspx
files are compiled at runtime into (System.UI.Web
) Page
objects. The Page object serves as the naming container for all objects on the page. When a master page is involved, one of the objects included is a MasterPage
object that, in turn, serves as a container for the ContentPlaceHolder
and Content
objects. This hierarchy of objects comprises the Web page that is rendered to the client.
To support master pages, the Page object includes a Master
property that can be used to reference objects in a master page's control collection. Content pages have access to this property and can thus alter the appearance of the master page template when they are loaded. As an example of how this can be used, consider the menu on our sample Web page (see Figure 16-15). The items have the same appearance no matter which page is loaded. As a rule, Web pages should distinguish the menu item for the currently loaded page from the other items. One popular technique is to highlight it. This requires adding only a few lines of code to our content files:
<%@ Import namespace=System.Drawing %>
<script runat="Server">
// Highlight link for home.aspx page
void Page_Load(object sender, EventArgs e)
{
// Change color of link to indicate current page
HyperLink h = (HyperLink)Master.FindControl("homepage");
h.BackColor=Color.Goldenrod; // highlight menu item
}
</script>
The Master.FindControl
method is used to reference the link pointing to the current page. In this case, the returned object's BackColor
property is set to highlight the link.
This section provides an overview of how to write and consume a custom Web control. Its objective is twofold: to introduce the fundamentals of implementing a control and, in the process, provide insight into the architecture of the intrinsic .NET controls.
In its simplest form, a custom control is a class that inherits from the System.Web.UI.Control
class, implements (overrides) a Render
method, and uses its HtmlTextWriter
parameter to emit the HTML code that represents the control. Add some public properties to define the control's behavior, and you have a custom control that functions like the built-in ASP.NET controls. More complex controls may require features such as data binding and caching support, which are not covered in this chapter. For a full understanding of those topics, refer to a good ASP.NET book.[1]
Listing 16-10 defines a custom control that we will use to illustrate the basics of control creation. The purpose of this control is to display a large or small version of a company logo, along with the company's name. Two properties determine its appearance and behavior: LogoType
takes a value of small
or large
to indicate the size of the image to be displayed, and Link
specifies the URL to navigate to when the small logo is clicked.
Example 16-10. A Custom Control—logocontrol.cs
using System; using System.Web; using System.Web.UI; namespace CompanyControls { // (1) Inherit from the System.Web.UI.Control class public class CompanyLogo : Control { // Custom control to display large or small company logo private string logo_sz; // "small" or "large" // Page to go to when logo is clicked private string myLink; public string LogoType { get {return logo_sz;} set {logo_sz = value;} } public string Link { get {return myLink;} set {myLink = value;} } // (2) Override the Render method protected override void Render(HtmlTextWriter output) { // (3) Emit HTML to the browser if (LogoType == "large"){ output.Write("<a href="+Link+">"); output.Write("<img src=./images/logo_big.gif align=middle border=0>"); output.WriteLine("</a>"); output.Write(" "); output.Write("<b style=font-style:24;"); output.Write("font-family:arial;color:#333333;>"); output.Write("STC Software</b>"); } else { output.Write("<a href="+Link+">"); output.Write("<img src=./images/logo_small.gif align=middle border=0>"); output.WriteLine("</a>"); output.Write<br>"); output.Write("<b style=font-style:12;"); output.Write("font-family:arial;color:#333333;>"); output.Write("Shell Design Studio</b>"); } } } }
Let's examine the three distinguishing features of a custom control class:
Inherits from System.Web.UI.Control
.
This class provides properties, methods, and events that the custom control requires. The most important of these is the Render
method that we describe next. Other members include the Page
events (Init
, Load
, PreRender
, Unload
) and members for managing child controls.
If you are using Visual Studo.NET for control development, the base class will be System.Web.UI.WebControls.WebControl
. This class derives from the Control
class and adds several members, most of which affect appearance.
Overrides the Render
method.
Each control must implement this method to generate the HTML that represents the control to the browser. Note that the Render
method is not called directly; instead, a call is made to RenderControl
, which then invokes Render
. For controls that contain child controls, the RenderChildren
method is available. This is called automatically by the Render
method, and an implementation that overrides this method should include a base.Render()
call if the control contains child controls.
Uses HtmlTextWriter
object to generate HTML code.
This example uses the HtmlTextWriter.Write
method to generate HTML for the control. This is the simplest approach, but HtmlTextWriter
offers several other methods that you may prefer. One alternative is to use a set of helper methods that eliminate writing full literal strings.
//Following yields: <table border=0> Output.WriteBeginTag("table") Output.WriteAttribute("border","0"); Output.WriteEndTag("table")
A third approach uses “stack-based” methods to render code. It uses AddAttribute
methods to define attributes for a tag that is then created with a RenderBeginTag
method call and closed with a RenderEndTag
call. Although the approach is verbose, it has the advantage of automatically detecting which version of HTML a browser supports and emitting code for that version.
The following code generates the same HTML as is in Listing 16-7 for the large image. It relies on a mixture of HtmlTextWriter
methods and special tag, attribute, and style enumerations. Refer to the documentation of HtmlTextWriter
for the lengthy list of methods and enumerations.
output.AddAttribute(HtmlTextWriterAttribute.Href,Link); output.RenderBeginTag(HtmlTextWriterTag.A); // <a output.AddAttribute(HtmlTextWriterAttribute.Src,bgImg); output.AddAttribute(HtmlTextWriterAttribute.Align,"middle"); output.AddAttribute(HtmlTextWriterAttribute.Border,"0"); output.RenderBeginTag(HtmlTextWriterTag.Img); output.RenderEndTag(); output.RenderEndTag(); // </a> output.Write (" "); output.AddStyleAttribute(HtmlTextWriterStyle.FontSize,"24"); output.AddStyleAttribute(HtmlTextWriterStyle.FontFamily,"arial"); output.AddStyleAttribute(HtmlTextWriterStyle.Color,"#333333"); output.RenderBeginTag(HtmlTextWriterTag.B); output.Write("Shell Design Studio"); output.RenderEndTag();
The key to using a custom control is the @Register
directive, which was discussed in Section 16.1. Its Assembly
and Namespace
attributes identify the assembly and namespace of the custom control. Its TagPrefix
attribute notifies the ASP.NET runtime that any tag containing this prefix value refers to the control specified in the directive. Here is a Web page that includes the custom CompanyLogo
control:
<%@ Page Language="C#" %> <%@ Register Namespace="CompanyControls" TagPrefix="logo" Assembly="logocontrol" %> <script runat="server"> protected void SendPage(object src, EventArgs e) { // Process page here. } </script> <html> <body> <logo:CompanyLogo runat="server" id="lgc" Link="products.aspx" LogoType="large"/> <hr> <font size=2 face=arial color=black><center> This page contains ways to contact us <br> <asp:Button runat="server" text="submit" OnClick="SendPage" /> </body> </html>
The control that we have created behaves similarly to the built-in Web controls, but lacks one important feature that they all have: the capability to maintain its state during a postback operation.
Let's change the preceding code to set the LogoType
property when the page is first loaded, rather than within the body of the code. We use the IsPostBack
property for this purpose:
protected void Page_Load(object src, EventArgs e) { if (!IsPostBack) { lgc.LogoType="large"; } }
On the initial request, LogoType
is set and the page is returned with a large image. However, subsequent postbacks result in the small image being displayed because the value is not retained, and the code defaults to the small image. Recall that state is maintained between postbacks in the hidden _VIEWSTATE
field. This field contains the values of the ViewState
collection, so the secret to state control is to place values in this collection. It operates like a hash table—accepting name/value pairs—and is accessible by any control. The following code demonstrates how property values are placed in ViewState
as a replacement for the simple fields used in Figure 16-7.
public class CompanyLogo : Control { // Custom control to display large or small company logo public CompanyLogo() { ViewState["logo_sz"] = "small"; ViewState["myLink"] = ""; } public string LogoType { get {return (string) ViewState["logo_sz"]; } set {ViewState["logo_sz"]= value; } } public string Link { get {return (string) ViewState["myLink"]; } set {ViewState["myLink"]= value; } } // Rest of class code is here...
The property values are now maintained in the _VIEWSTATE
field and persist between postbacks.
At the beginning of the chapter, we created a Web page (refer to Figure 16-2) that calculates the Body Mass Index. This calculator consists of text boxes, labels, and a button. To turn this into a custom control, we could take our previous approach and override the Render
method with a lengthy list of statements to generate the appropriate HTML. In addition, special code would have to be added to maintain the state of the control during postback operations. A better solution is to create a custom composite control.
A composite control is created from existing controls. Its advantage is that these controls, referred to as child controls, are very low maintenance. They render themselves—eliminating the need to override Render
—and they maintain their state during postbacks. In addition, they let you program with familiar objects and their members, rather than output statements.
There are two major differences in the code used for the “from scratch” custom class in our preceding example and that of a composite control:
The composite does not have to override the Control.Render
method to display controls. Instead, it must override the Control.CreateChildControls
method to add existing controls to the collection of controls making up the composite control.
The custom control class should inherit from the INamingContainer
interface. Its purpose is to indicate to ASP.NET that child controls exist and they should be placed in a separate namespace. This prevents name collision problems when a page contains more than one composite control.
Listing 16-11 contains the code to implement the BMI calculator as a composite control. The calculator comprises three text boxes, a label to display the result, and a button to invoke a method to perform the calculation. Most of the code of interest is in the overridden CreateChildControls
method. It adds the standard controls to the collection, and uses LiteralControl
to add HTML and descriptive information that helps format the control. To simplify the listing, code validation is included on only one control.
Example 16-11. A Composite Control—bmicompos.cs
using System; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; namespace CompositionControls { public class BMIComposition : Control, INamingContainer { TextBox htf; TextBox hti; TextBox wt; Label bmi; private void getBMI(object sender, System.EventArgs e) { if (Page.IsValid) { decimal f = Convert.ToDecimal(htf.Text); decimal inch = Convert.ToDecimal(hti.Text); decimal w = Convert.ToDecimal(wt.Text); decimal totinches = f * 12 + inch; decimal h2 = totinches * totinches; decimal massIndex = (w * 703 * 10/ h2)/10; bmi.Text = massIndex.ToString("##.##"); } } protected override void CreateChildControls() { htf = new TextBox(); hti = new TextBox(); wt = new TextBox(); bmi = new Label(); bmi.Width= 50; bmi.BorderStyle= BorderStyle.Solid; bmi.BorderWidth=2; htf.Width= 30; hti.Width= 30; hti.ID = "hti"; wt.Width = 40; // Display calculator interface Controls.Add(new LiteralControl (" <b>BMI Calculator</b>")); Controls.Add(new LiteralControl ("<br>BMI: ")); Controls.Add(bmi); Controls.Add(new LiteralControl("<br>Height: ")); Controls.Add(htf); Controls.Add(new LiteralControl("  ")); Controls.Add(hti); Controls.Add(new LiteralControl(" (feet/inches)")); Controls.Add(new LiteralControl("<br>Weight: ")); Controls.Add(wt); Controls.Add(new LiteralControl("<br>")); // Validation control for inches accepted RangeValidator rv = new RangeValidator(); rv.ControlToValidate="hti"; rv.MaximumValue="12"; rv.MinimumValue="0"; rv.Type=ValidationDataType.Integer; rv.ErrorMessage="Inches must be 1-12"; Controls.Add(rv); // Button to invoke BMI calculation routine Button calcBMI = new Button(); calcBMI.Text = "Submit Form"; calcBMI.Click += new EventHandler(this.getBMI); this.Controls.Add(calcBMI); } } }
Note that getBMI
performs the BMI calculation only if the Page.IsValid
property is true
. Include this check when validation controls are used, because server-side validation sets the IsValid
flag to false
if validation tests fail, but does not prevent code execution.
To make the control available, compile it and place it in the in
directory of the Web page.
csc /t:library bmicompos.cs
The control is included in a page using the @Register
directive as described earlier:
<%@ Register Namespace="CompositionControls" TagPrefix="bmicon" Assembly="bmicompos" %> <HTML> <table border=0 color=#cccccc> <tr><td> <bmicon:BMIComposition runat="server" id="bmic" /> </td></tr></table>
ASP.NET developers have an overflowing toolbox of controls to choose from—many with overlapping functionality. Table 16-6 offers some recommendations for selecting a control, or combination of controls, to handle frequently encountered Web page tasks. Some of the suggested controls have been introduced in this chapter; but for other important ones, such as the GridView
, DetailsView
, and FormView
, you should refer to an in-depth ASP.NET 2.0 book or reference source.
Table 16-6. Controls Recommended for Typical Web Page Applications
Page Requirement | Controls |
---|---|
Display multiple data records in a spreadsheet or grid format. |
|
Display multiple data records in a custom format using simple controls. |
|
Display a parent-child relationship between data. |
|
Display a hierarchical view, such as a directory or XML elements. |
|
View, edit, or delete data, one record at a time. |
|
Web page with multiple sections or one that steps through a process in multiple steps. | Use multiple panels as containers for simple controls. Panels in ASP.NET 2.0 include a scrolling capability for IE browsers. |
ASP.NET is a development platform that offers a variety of techniques to overcome the inherent problems that plague client-server interaction over the Internet. These problems include browser incompatibility, the difficulty of retaining an application's state between requests, and a reliance on interpreted script rather than compiled code to implement program logic.
The ASP.NET answer to these problems is server-side based model whose code is written in C# or VB.NET. A Web page is a class deriving from the Page class. Controls are all classes that implement a Render
method to generate the HTML code that represents them in the browser. An important design feature of this model is the ability to separate presentation logic from the business logic by placing the latter in a code-behind file.
Controls fall into two categories: HTML server controls and Web controls. The former correspond to traditional HTML tags, but include a runat=server
attribute that indicates they are run on the server. Web controls are much richer than HTML controls. They include list controls; data display controls—DataList
and GridView
—that are usually bound to data sources; and validation controls, which are helper controls that validate the content of other controls. If none of these controls meet an application's need, custom controls can be designed that extend existing controls or present a new interface.