Cover Page

Copyright © 2002 by Microsoft Corporation


Dedication
To Silvia, Francesco, and Michela. To all the people I love. To all the people of Sept 11th. To the Afghan people and children.
—Dino
“Keep it simple. As simple as possible. But no simpler.”
Albert Einstein


Acknowledgments

Microsoft Windows has always played a key role in my life. For a time, I was suspended between the black screen of MS-DOS and the bright colorful screen of that other tempting new operating system. However, when it became clear that I needed to program computers for a living, I suddenly made up my mind and choose Windows as my operating system. And so it is to this day.

There’s an invisible thread that ties Windows releases and the significant events of my life. I was married to Silvia in 1995, one month after the release of Windows 95. Our first baby—a boy, Francesco—coincidently arrived at the same time as the release of Windows 98. My wife and I were signing the contract for our first house when Microsoft shipped Windows 2000. And what about .NET? I received the announcement of this new platform as I was jumping for joy at the news that our second baby was on the way. Our baby—a girl this time, Michela—arrived while I was digging into the intricacies of the first beta of Visual Studio, and psychologically preparing myself for the arrival of the beta 2 T-storm.

The first years of my Windows career were spent discovering the programming world that was all around me. Like a child learning to walk, I hesitated and wobbled with every step. When working with Windows SDK, I experienced uncontainable joy when a function call worked and the deepest depression when a button click got lost in the maze of Windows messages. During all this time, which was only a couple of years but felt like a lifetime, I felt as if there was a man watching over me (figuratively speaking). That man is Charles Petzold. It was a dream of mine to one day meet him and shake his hand. My dream came true, but we met under unusual circumstances. I met Charles Petzold in the kitchen of a Swiss restaurant in Davos while I was chopping onions. Please, do not misunderstand me; read on before you start laughing. You may mistake me for a prep cook, but I’m not the only one!

In the same kitchen, in fact, Charles was cutting meat with Kyle Marsh (Mr. Windows XP), while Francesco Balena (Mr. VB2TheMax) and Doug Boling (Mr. Windows CE) were preparing roulades. Like a background thread, a rather sneaky Jeffrey Richter (Mr. Advanced Windows) was taking pictures of us at what was actually the speaker’s event of the WinSummit 2001 Conference. By the way, the conference was a great show and if you weren’t able to make it, you can check it out at http://www.winsummit.com.

I can’t help but mention Charles for two reasons. With .NET, our internal clock has been moved several years back to a time when Windows programming was largely an unexplored jungle—yes, a kind of Jurassic Windows—where Charles was an early explorer. The .NET Framework documentation looks like the online counterpart of that hefty paper-based tome—Windows SDK documentation. Right now it’s really hard to know everything about .NET Framework. Who knows if there’s a class that does that particular thing you have been struggling with? Another reason why I think of Charles while writing the credits for this book is that he lives in New York and his wife was the first person to describe what was happening to the people of New York on September 11th and the smell of the smoke and burning rubble, the dust and debris that covered the city.

It was early afternoon in Rome, and I was working on DataGrid’s in-place editing (Chapter 4) and listening to the radio. At first the hysterical sounding radio announcer reported bits and pieces of the event that were hard to make sense of. To find out what was going on, I dropped an e-mail to Josh Trupin—the technical editor of MSDN Magazine who works in Manhattan. I was terrified with his very concise description of events, which I received a few minutes later. From his description, it was as if he were writing this e-mail straight from hell. The story of the making of this book intersects with these tragic events on that September morning.

Behind each and every book there is a team of people whose contributions determine the final result—that numbered pile of paper you hold in your hands. I have three outstanding names with faces that I’ve never seen because we always passed our bytes over the Internet bridge that connects Europe and the United States. A monumental Thank You goes to my editors, Lynn Finnel, and Victoria Parker Thulman, and to my technical editor Mark Bukovec. They did an excellent job straightening out the wrinkles in my original text. You’re seeing only the final output and will never know the importance and the dimension of their work. I want to especially thank David Clark, the acquisitions editor and the main person responsible for this book.

Some of the examples in this book come directly from that sort of nebulous entity commonly referred to as the real world. To date, I have a little database with about two hundred ideas, suggestions, tips, and tricks e-mailed to me from readers of my articles. The most educational and the toughest of these problems and examples found their way into this book. Others will be published through a variety of channels—including the Wintellect and VB2TheMax Web sites, as well as my regular columns in MSDN Magazine, MSDN Voices, and the newest—asp.netPRO magazine. Keep ideas and suggestions coming—this book is also yours!

The book touches on areas of ASP.NET and ADO.NET that the official documentation does not yet fully cover. Several times I asked for help from a circle of Windows intellectuals (wintellectuals, for short), who all have very high IQs—or at least this is what their ad says :-). I had productive exchanges with John Lam, Jeff Richter, and Jeff Prosise. I’ll be grateful for eternity to John Robbins who revealed a brand new world to me when he said, “Hey Dino, why don’t you test your ASP.NET code with debug=true?” From that moment on, the project became so easy.

Francesco Balena was the first human being to instill in me the doubt that DataSets and ASP.NET might not always be very close friends. The rub though was that he started this very interesting and lengthy discussion over the phone, which was great except for the unfortunate fact that I was in Rome, and he was in Stockholm and I was paying for the call, and we were to meet face to face in just two days. The connecting power of .NET comes at a price!

Although my name is the only one to appear on the cover, my children contributed a lot! We spent nights sitting together in front of a laptop (instead of sleeping—a more productive and relaxing activity). When I said to my four year old, Francesco, that the book was finished, he said, “Dad, when are we going to write another one? Can I have my name on the cover too?” No such demand came my way from Michela—who’s only one year old. She limited her contributions to the destruction of a couple of mice and made repeated attempts on my laptop keyboard. She also has an appetite for Visual Studio .NET CDs. But all in all, this book has a very happy ending.

—’til next book

Dino


Introduction

Many years ago, when I was just a shy junior programmer, I had a dream. I dreamt of a (perfect?) world in which I never had to figure out which messages to handle in my Microsoft Windows applications. I often caught myself longing for a time when programmers wouldn’t have to search through the hefty Windows SDK tome looking for undocumented functions. I didn’t want to research and refine the best way to export functions from Microsoft Visual C++ to accommodate incoming calls from Microsoft Visual Basic applications. As a first-year student at the Windows University, I found particularly hostile the different programming models that Windows tools featured. My discomfort was not a matter of preferring one language over another—of course you naturally felt more comfortable with Visual Basic syntax or C++ syntax. The difficulty was more systemic. You had to think of applications in different ways, and then you had to code them to comply with the syntax of each language.

I understand that the most obvious response to this complaint is, “OK, but what about using only one language and one tool for all of your development efforts?” This suggestion is certainly fair, but unfortunately it only partially applies to the world of Windows programming. In Windows applications, you inevitably use diverse tools to build applications and multitier systems.

Each Windows programming tool is by nature different from the others, conceived in a different environment. The Windows operating system was developed with the C language in mind and ended up being a tough, somewhat dumbed-down model for only the bravest and most patient programmers. Along the way, and not especially quickly, Microsoft realized that there had to be a better way to perform the same functions. Enter Visual Basic, with its attractive object orientation and rapid development features.

Visual Basic, however, was probably ahead of its time. It was based on one of the best ideas in software, but the actual design wasn’t as good as it should have been. Several versions of Visual Basic and Visual C++ repeatedly failed to offer a “really” unique programming model and, more importantly, a common set of objects callable by using the same semantic model. For a certain time, many programmers felt that COM+ could have been upgraded to the rank of Common Windows Run-time Engine. And in fact one of the very early names of what we now refer to as the .NET common language runtime was COM++.

A number of structural flaws in COM and COM+ suggested a complete redesign before the shipment of a run-time environment entirely based on them would be feasible. COM components are raw binaries with a rough model for exposing functionalities, handling events, registering, and versioning, to say the least. Also, COM leaves you responsible for managing the memory clean-up, which is a delicate task. The big picture of COM was great and timely, but it needed a radical makeover to be upgraded. So then came .NET.

.NET seems to fulfill the somewhat childish dream I had as the shy junior programmer in the early 90s. Recall that I dreamt of a common run time for all applications and development tools available under the Windows umbrella. I envisioned a world in which regardless of the language, the programming objects were the same. No more MFC or ATL C++ classes for the Visual C++ programmers and run time COM classes for outstanding Visual Basic programmers. Thanks to the .NET infrastructure, my dream came true.

.NET has cross-language integration and exception handling, debugging and profiling services, enhanced security, more effective versioning, and deployment. It supplies a brand new model for component interaction and the long-awaited common class library—the .NET Framework. .NET unifies the programming model, making the language choice mostly a matter of personal preference. Individuals on the same team can use different languages without losing reusability and integration. Just one common, consistent, and elegant class library is available for all .NET applications, and it has nothing of the COM+ quirkiness. Finally, that ingenuous dream came true. I bet that shy junior programmer would have really enjoyed .NET!


What Is This Book About?

This book is a code-intensive, solution-oriented book. What’s a solution, anyway? According to my dictionary, a solution is a way of dealing with a difficult situation so that the difficulty is removed. So what’s a Web solution? It’s removing all stumbling blocks that, scattered here and there, can slow down the development of a Web application. The nature of the Web hasn’t changed with the advent of .NET, but the tools available to you are undergoing a radical change. ASP.NET and ADO.NET are the principal new tools for building effective Web applications that rely on the .NET platform.

Being solution- and task-oriented, this book is not about writing .NET Web applications. Neither is it a core reference book about ASP.NET, ADO.NET, Web services, nor any other technology mentioned in these pages. The book targets a relatively advanced audience. You will not find it to be a detailed programmer’s reference for ASP.NET controls or ADO.NET classes, so be prepared to use it in conjunction with a more basic book and the MSDN online documentation.

Just because I focus on solutions and tasks, don’t wait until you have a tough problem to solve before you read the book. Prevention is always the best policy! By knowing the techniques and familiarizing yourself with the technologies, you are in a good position to tackle applications in the future.

The book explains the plumbing in .NET Web applications and how your code can and should interact with ASP.NET and ADO.NET classes. Accessing data from within ASP.NET applications and data binding are the core topics of the book. Data access and data reporting represent a routine task for almost all Web applications. For this reason, there’s a growing demand for an infrastructure that provides automatic binding between data sources and elements of applications. In .NET, several effective data bound controls allow you to easily associate rows of data with graphical HTML elements such as drop-down lists or tables. The power of ASP.NET and ADO.NET is evident when you use server controls in the context of Web applications. Data bound controls and data management code are used in the same way regardless of the programming model—Windows Forms, Web Forms, or even Web services.


What Does This Book Cover?

In this book, I answer the following common questions:

  • What’s the difference between a DataList control and a DataGrid control?

  • What’s the difference between the global objects Cache, Application, and Session?

  • What’s the difference and the recommended use for custom controls and user controls?

  • What’s a data binding expression anyway?

  • How can I make a DataGrid control editable, and how can I save changes?

  • What can ASP.NET templates do for me?

  • What’s the best technique for caching data on the server?

  • How can I arrange a master/details representation of my data?

  • What’s the magic behind Web services? Are they carried only by XML and SOAP?

  • I was so happy with OLE DB providers. Why did they change them to .NET data providers?

  • Can I save my existing ADO code when moving to .NET? If so, how can it communicate with a newer .NET system?

The official language of the book is C#, and all the sample code shown in the book is written in C#. On the companion CD, you will find many of the examples rewritten using Visual Basic .NET. Some of the most interesting examples you’ll review in this book address the following issues:

  • Adding new rows to a DataGrid control, mimicking the behavior of Microsoft Excel worksheets

  • Creating templated columns programmatically using the DataGrid control

  • Inserting summary rows in the body of a DataGrid control

  • Creating enhanced DataGrid controls that page through rows and provide automatic auto-reverse sorting capabilities

  • Creating master/details views

  • Implementing custom data pagination for a Microsoft SQL Server database

  • Resolving conflicts with an ADO.NET batch update

  • Building a multiselection DataGrid control

  • Selecting the rows of a DataGrid control by condition

  • Using a tool to create XML DataSet objects that can substitute as a database for application demos

  • Accessing file system information by using a simple data provider

What Do I Need to Use The Book’s Companion CD?

I would bet the farm that 99.99 percent of readers will write .NET applications using Visual Studio .NET—an excellent development environment tightly integrated with the .NET Framework and the run time. A book that at its core is about programming techniques must maintain independence from a single tool, even an incredibly powerful one such as Visual Studio .NET. I was so careful about honoring this commitment that you can rework a very large part of the code in this book using only Microsoft Notepad and Microsoft Internet Explorer. You should have SQL Server 2000 installed with the whole Northwind database. Oops, I forgot the most important part of all. You need the .NET Framework!

How Do I Use the Book’s Companion CD?

To view the contents of the CD, insert the CD into your CD-ROM drive. If the startup application does not begin automatically, run StartCD.exe in the root directory of the CD. You can view the sample files from the companion CD, or you can install them on your hard disk and use them to create your own applications. Because most of the samples are Web applications, a convenient place to install the sample files would be under your IIS default Web site directory (for example, c:\inetpub\wwwroot). Installing the sample files requires approximately 5 MB of disk space. Some samples have batch files to simplify compiling and copying assemblies. If you have trouble running any of these files, refer to the Readme.txt file in the sample’s directory of the CD or to the text in the book that describes these programs.

At the time this book was printed, the .NET Framework was just about to be released. The samples have been tested with .NET Framework Release Candidate 3. This may or may not be the “golden” version of the .NET Framework; however, in the event of a subsequent release candidate, it is highly unlikely that any changes would be made to render the samples unusable. If such a problem occurs, check the Microsoft Press support Web site for updates to the sample applications.

Note that unlike ASP, ASP.NET does not run applications within the context of the LOCALSYSTEM account. This means that samples that access the file system, for example, will explicitly need to be granted read/write permissions. Again, consult the Readme.txt file for instructions on how to configure the samples.

The CD also includes an electronic version of the book. This eBook allows you to view the book text on screen and to search its contents. For information on installing and using the eBook, see the Readme.txt file in the \eBook folder.

How to Touch Base With Me

Please feel free to send any question about the book directly to me. I can be reached via email at one of the following addresses:

  • dinoe@wintellect.com

  • desposito@vb2themax.com

You can also get in touch with me via the Wintellect Web site (http://www.wintellect.com) and the VB2-The-Max Web site (http://www.vb2themax.com).

Support

Every effort has been made to ensure the accuracy of this book and the contents of the companion CD-ROM. Microsoft Press provides corrections for books through the World Wide Web at the following address:

http://www.microsoft.com/mspress/support/

To connect directly to the Microsoft Press Knowledge Base and enter a query regarding a question or issue that you may have, go to

http://www.microsoft.com/mspress/support/search.asp

If you have comments, questions, or ideas regarding this book or the companion CD-ROM, please send them to Microsoft Press using either of the following methods:

Postal Mail:

Microsoft Press
Attn: Building Web Solutions with Microsoft ADO.NET and ASP.NET
Editor
One Microsoft Way
Redmond, WA 98052-6399

E-mail:

MSPINPUT@MICROSOFT.COM

Please note that product support is not offered through the above mail addresses. For support information regarding the .NET Framework, Visual Studio .NET, and SQL Server, please visit the Microsoft Product Support Web site at http://support.microsoft.com.


Data Bound Controls

Part I
Data Access and Reporting
Chapter 1
Data Bound .NET Controls

Most software applications involve, in one way or another, data access and reporting. Web applications in particular must perform these two functions, so Web programmers are looking for any automated (or even semi-automated) method of associating rows of data with graphical HTML elements such as drop-down lists or tables. The answer to this growing demand for automatic binding between data sources and graphical elements is .NET data bound controls.

Data bound controls play a key role in the development of .NET applications because they allow you to associate controls and their individual properties with one or more fields in any .NET-compliant data source. The power of .NET is the versatility of these data bound controls. You use them in the same way regardless of the programming model—Microsoft Windows Forms, Microsoft Web Forms, or Microsoft .NET Web services.

In this chapter, we’ll discuss the two levels of data binding, simple and complex, as well as the most important Web controls that can be data bound. Data binding and data bound Web controls are the focus of any Active Server Pages (ASP) .NET application, so we’ll explore them at length in the next few chapters. (Web controls are also referred to as ASP.NET controls, server controls, and .NET Web controls.) Getting a handle on the key programming techniques is vital for building effective Web solutions.


ASP.NET Data Binding

Data binding is the process of retrieving data from a source and dynamically associating it to a property of a visual element. Depending on the context in which the element will be displayed, you can map the element to either an HTML tag or a .NET Web control.

In ASP.NET, the atomic elements that work together to generate page output are the Web controls. In ASP.NET data binding, the application retrieves data from any .NET-compliant data source and then allows you to selectively manipulate and assign this data to properties of controls that have been specially designed to support data binding. These controls are known as data bound controls. ASP.NET Web server controls can be bound to a data source by using a heterogeneous bunch of properties, including Text, DataSource, and DataTextField. Later in the chapter, you’ll see how to handle binding by using a combination of these and other control-specific properties.

The DataSource property in particular lets you specify the data source object to which the control is linked. Notice that this link is logical and does not result in any underlying operation until you explicitly call another method. Not all properties in a Web control programming interface can be data bound. You activate data binding on a control by calling the method DataBind. When this method executes, the control loads data from the associated data source, evaluates the data bound properties, and redraws its user interface to reflect changes.

Feasible Data-Binding Sources

Many .NET classes can be used as data sources—not just those dealing with database contents. In general, any object that exposes the ICollection interface is a potential source of data binding. The ICollection interface defines size, enumerators, and synchronization methods for all .NET collections. As a result, you can bind a Web control to any of the following data structures:

  • In-memory .NET collection classes including arrays, dictionaries, sorted and linked lists, hash tables, stacks, and queues.

  • User-defined data structures, as long as the structure exposes ICollection or one of the interfaces based on it (such as IList).

  • Database-oriented classes such as DataTable and DataSet. Bear in mind that in general a DataSet object can contain multiple tables. In this case, you would assign the DataSet object to the DataSource property and assign the name of the selected DataTable object to the control’s DataMember property.

  • Filtered views built on top of DataTable objects that show only a subset of the contained rows. Views are represented by a DataView class or any class that you have built from a DataTable object.

    note

    You cannot directly bind XML documents unless you load their contents in one of the classes mentioned in the preceding bulleted list. You can bind XML documents in two ways. You can load the XML document into a DataSet object and then bind the DataSet object. Alternatively, you can parse the XML document with an XML reader class and bind the collection of nodes you obtain.
Simple Data Binding

In ASP.NET, simple data binding is a connection between one piece of data and a server control property. This connection is established through a special expression that is evaluated when the code in the page being processed calls the DataBind method either on the Page object or on the control.

A data-binding expression is any text enclosed in angle brackets and percent signs (<%…%>) and prefixed by the number symbol (#). You can include a data-binding expression as the value of an attribute in the opening tag of a server control or anywhere else on the page. You cannot programmatically assign a data-binding expression to a property of a Web control. If the data-binding expression contains quotation marks, use a single quotation mark (') to wrap the whole expression, as shown here:

theLabel.Text='<%# "Hello, world" %>';

Within the delimiters, you can invoke user-defined page methods, static methods and properties, and methods of any other page components. The following code shows a label control whose Text property is bound to the name of the currently selected element in a drop-down list control:

<asp:label runat="server" Text='<%# dropdown.SelectedItem.Text %>' />

The data-binding expression can accept a minimal set of operators, mostly for concatenating subexpressions. When you need more advanced processing and are using external arguments, resort to a user-defined method. The only requirement for a user-defined method is to be callable within the page. When you plan to use code that is not resident in the .ASPX page (for example, if you use code-behind techniques), make sure the method is declared public or protected.

Let’s walk through an example that illustrates how to arrange a form-based record viewer by using simple data binding with text boxes. (The full source code for the FormViewer.aspx application is available on the companion CD.) The user interface supplies three text boxes, one for each of the retrieved fields. The data source is the Employees table of the Northwind database in SQL Server 2000. The query selects three fields: employeeid, firstname, and lastname. The binding takes place between the text boxes and the fields. Only one record at a time is displayed; the user clicks the Previous and Next buttons to move through the data. You can see what the application looks like in Figure 1-1.

Figure 1-1
The text boxes are bound to fields in the Employees table from the SQL Server 2000 Northwind sample database.

The binding expression employs a user-defined method named GetBoundData.

<asp:textbox runat="server" id="txtID" cssclass="StdTextBox"
    Text = '<%# GetBoundData("employeeid") %>' />

GetBoundData takes the name of the field to display and retrieves the position of the current record from the ASP.NET Session object. It then retrieves the value and passes it to the ASP.NET run time for actual rendering.

public String GetBoundData(String fieldName)
{
    DataSet ds = (DataSet) Session["MyData"];
    DataTable dt = ds.Tables["EmpTable"];
    int nRowPos = (int) Session["CurrentRecord"];

    String buf = dt.Rows[nRowPos][fieldName].ToString();
    return buf;
}
Complex Data Binding

Complex data binding occurs when you bind a list control or an iterative control to one or more columns of data. List controls comprise DropDownList, CheckBoxList, RadioButtonList, and ListBox. Iterative controls are Repeater, DataList, and DataGrid. The characteristics of ASP.NET iterative controls are briefly summarized in Table 1-1.

Table 1-1 ASP.NET Iterative Controls

Control

Description

Repeater

Creates a custom layout for displaying the control’s contents by repeating a specified template for each item in the bound list. You define an ASP.NET template for various categories of items (header, footer, item, separator, and so forth), and the control applies it repeatedly in the page.

The Repeater control has no built-in layout, so you must explicitly declare all ASP.NET formatting and style tags.

DataList

Displays the contents of a data bound list through ASP.NET templates. The DataList control supports selecting and editing. The appearance of the DataList control may be customized to some extent by setting style properties for different parts of the control. Compared to the Repeater control, DataList shows off predefined layouts, more advanced formatting capabilities, plus support for selecting and editing.

DataGrid

The DataGrid control renders a multi-column, data bound grid of data. It allows you to define various types of columns and control the layout of the resulting cells through predefined layouts and ASP.NET templates. You can also add specific functionality such as edit button columns and link columns. The DataGrid control also supports a variety of options for paging through data.

List controls have a more complex and versatile user interface than labels and text boxes. Because a list control handles (or at least has in memory) more items simultaneously than a label or a text box, you should associate the list control explicitly with a collection of data—that is, the data source. Depending on its expected behavior, a list control will select the needed items from memory and properly format and display them.

Iterative controls take a data source, loop through its items, and apply HTML templates to its rows. This basic behavior is common to all three ASP.NET iterative controls. The controls differ in their layout capabilities and the functionality they support.

Let’s see how complex forms of data binding apply to key Web controls.


The DropDownList Web Control

The DropDownList Web control enables users to select from a single-selection drop-down list (for example, a combo box). You can control the look of the DropDownList control by setting its height and width in pixels, but you cannot control the number of items displayed when the list drops down. The SelectedIndex and SelectedItem properties provide details about the currently selected element. The DropDownList control supports data binding by using the following five properties: DataSource, DataMember, DataTextField, DataValueField, and DataTextFormatString. As shown in the following code, of these five properties, only DataSource, DataTextField, and DataValueField have a corresponding ASP.NET attribute you can declare:

<asp:DropDownList id="DropDownList1" runat="server"
    DataSource="<%# databindingexpression %>"
    DataTextField="DataSourceField"
    DataValueField="DataSourceField">

You can also use a data-binding expression to set the DataSource property. The expression you use must evaluate to a .NET object that exposes the ICollection interface. You cannot use expressions to set the other properties. The values for DataTextField and DataValueField each must match the name of one field in the data source, so you cannot assign DataTextField by combining two or more fields in the data source. Let’s see how to work around this limitation.

Suppose you want a drop-down list to display both the first name and the last name of each employee in the company. You could ask SQL Server to return a calculated column that has the required format. In this case, you would use a query string, as shown in the following code, and set DataTextField to the name of the precalculated field:

SELECT lastname + ', ' + firstname AS 'EmployeeName'
    FROM Employees

You can obtain the same result, however, without the involvement of SQL Server. After you hold a DataTable object that contains the result of the query, you can add a new column on the fly. The content of the column is determined by the expression you use. The following code uses this approach to generate the drop-down list shown in Figure 1-2:

String strConn, strCmd;
strConn = "DATABASE=Northwind;SERVER=localhost;UID=sa;PWD=;";
strCmd = "SELECT employeeid, firstName, lastname FROM Employees";
SqlDataAdapter oCMD = new SqlDataAdapter(strCmd, strConn);
DataSet oDS = new DataSet();
oCMD.Fill(oDS, "EmployeesList");
DataTable dt = oDS.Tables["EmployeesList"];
dt.Columns.Add("EmployeeName",
    typeof(String),
    "lastname + ', ' + firstname");
Figure 1-2
You can create a drop-down list box such as this as you work by using the DataTable object.

The next code example shows how to bind the drop-down list control named EmpList to the dynamically created EmployeeName field:

EmpList.DataSource = oDS.Tables["EmployeesList"].DefaultView;
EmpList.DataTextField = "EmployeeName";
EmpList.DataValueField = "employeeid";

DataTextField links the Text property of any individual item in the list with the EmployeeName field. DataValueField ensures that the Value property of each item is set with the value stored in the corresponding record of the employeeid field. The full source code for the DropDown.aspx application is available on the companion CD.

An expression-based column does not have to be filled explicitly. Whenever the program needs to read the value of one of its rows, it just evaluates the expression as it processes data and then takes the result. Note that expressions can reference other expression columns. Circular references, though, are not allowed. The ADO.NET run time promptly detects them and raises an exception.

Which of the two approaches is preferable? Should you ask SQL Server to return a made-to-measure column, or should you create the extra column you need when you need it? The final result of both approaches is identical, but the cost of using each is not. SQL Server is not as efficient. It returns a new column of potentially duplicated data. Processing data to produce a column creates overhead, especially when the expression is complex. Unless the precalculated column is needed for further processing, you should avoid using SQL Server for this type of operation.


The CheckBoxList Web Control

The CheckBoxList control is a single control that acts as a parent control for a collection of checkable list items, each of which is rendered through an individual CheckBox control. You insert a check box list as follows:

<asp:CheckBoxList runat="server" id="cbEmployees">

The CheckBoxList control supports data binding by using five properties: DataSource, DataMember, DataTextField, DataValueField, and DataTextFormatString. You bind the CheckBoxList control to a data source by using the following code:

cbEmployees.DataSource = oDS.Tables["EmployeesList"];
cbEmployees.DataTextField = "lastname";
cbEmployees.DataValueField = "employeeid";

The properties of the CheckBoxList control play the same role as the properties for the DropDownList control, which we discussed earlier in the chapter. Unlike the DropDownList control, however, the CheckBoxList control does not supply properties that reveal information about the selected items. As you know, this information is vital for any Web application that uses checkable elements.

At any time, CheckBoxList can have one or more items selected. How can you retrieve these items? All list controls have an Items property that contains the collection of the child items. The Items property is implemented through the ListItemCollection class, and each list item can be accessed via a ListItem object. The following code loops through the items stored in a CheckBoxList control and checks the Selected property of each. (Selected is a Boolean property that indicates whether the item is selected.) The code then uses the StringBuilder object to concatenate the text of each selected item into a single comma-separated string.

StringBuilder buf = new StringBuilder("");
foreach(ListItem item in chkList.Items)
{
    if (item.Selected)
    {
        buf.Append(item.Text);
        buf.Append(", ");
    }
}
caution | In .NET, the String object is immutable, so each time you modify a string, a brand new String object is created. This might have an unpleasant impact on performance, especially when repeated modifications to a string are required. Repeated modifications typically occur when you loop through potentially lengthy data sources to build up a summary string. The StringBuilder class solves the problem by letting you modify a string without creating a new object.

Notice in the following code that the ASP.NET CheckBox control is a bit more powerful than the ASP.NET counterpart of the well-known HTML <input> tag. The ASP.NET CheckBox control also supplies the Text property that allows you to automatically associate the check box with descriptive text.

<asp:checkbox runat="server" id="theCheckBox"
    Text="Click me..." />

When the preceding code is processed by the ASP.NET run time, the ASP.NET CheckBox control generates the following HTML code:

<input id="theCheckBox" type="checkbox" name="theCheckBox" />
<label for="theCheckBox">Click me...</label>

This code displays the check box shown here:

A slick trick used by ASP developers to quickly obtain the selected items of a logically related group of check boxes does not work in ASP.NET. When you have several check boxes with identical names in an ASP application, you can use a single line of code to obtain the corresponding values of check boxes that are selected when a form is posted.

<input type="checkbox" name="foo" value="...">

After the form values in the above code are posted, the following line of code written in Microsoft Visual Basic, Scripting Edition (VBScript) sets an array with all the checked values:

<% arrSelected = split(Request.Form("foo"), ",") %>

Request.Form("foo") returns a comma-separated string formed by the value strings of all checked items. You then pass this string to VBScript’s Split function, which transforms the string into an array.

This code won’t work in ASP.NET if you use the CheckBox server control to render the check box component, but it will work if you use the <input> tag. However, using the CheckBoxList control is the preferred ASP.NET way to handle lists of check boxes.

note | The HTML tag <input> and all other HTML tags are rendered through an ASP.NET control taken from the System.Web.UI.HtmlControls namespace. The HTML check box element is rendered through the ASP.NET HtmlInputCheckBox class.


The RadioButtonList Web Control

The RadioButtonList control acts as a parent control for a collection of radio button list items. All child items of RadioButtonList are rendered by using a RadioButton control. A RadioButtonList control can have either no items or one item selected. The SelectedItem property returns the selected element as a ListItem object. However, you have no guarantee that only one item is selected at any given time. For this reason, be extremely careful when you access the SelectedItem property of a RadioButtonList control. Depending on the structure and flow of your code, the item returned could be a null object. A workaround that always shields you from unpleasant surprises is wrapping any call to SelectedItem in a try/catch block, but don’t abuse this technique because a try/catch block is always costly. An even better solution is to design your code to eliminate the risk of returning an unselected item.

The contents of a RadioButtonList control can be obtained from a data source, as shown here:

<asp:RadioButtonList id="rbEmployees" runat="server"
    DataValueField="employeeid"
    DataTextField="employeename" />

You also need to programmatically set the DataSource property as follows:

rbEmployees.DataSource = myDataTable;

The RadioButtonList control supports data binding by using five properties: DataSource, DataMember, DataTextField, DataValueField, and DataTextFormatString. These properties behave in the same way as the properties of controls discussed earlier in the chapter.

Programmers and designers who work with data bound controls in lists are always greatly concerned about content presentation. A list of items can flow horizontally or vertically, can be expressed in a fixed number of columns or rows, and so on.

Some Web controls, such as RadioButtonList, accept layout directives. For example, you can control how a list is rendered by using the RepeatLayout and/or RepeatDirection properties of RadioButtonList. RepeatLayout governs the default layout in which the list items are rendered within a table, ensuring the vertical alignment of the companion text. The alternative is displaying the items as free HTML text using blanks and breaks to guarantee a minimum of structure. With or without a tabular structure, RepeatDirection controls how the items flow. Feasible values for RepeatDirection are Vertical (the default) and Horizontal. The RepeatColumns property determines how many columns the list should have. The default value of 0 indicates that all items are displayed in a single row. The direction of that row—vertical or horizontal—depends on the value of RepeatDirection.


The ListBox Web Control

The ListBox control represents a vertical sequence of items displayed in a scrollable window. As shown in the following code, ListBox allows single or multiple item selection and exposes its contents by using the familiar Items collection:

<asp:listbox runat="server" id="theListBox"
    rows="5" selectionmode="Multiple" />

You can determine the height of the control by using the Rows property. As you might have guessed, the height is measured in numbers of rows rather than in pixels or percentages.

When it comes to data binding, the ListBox control behaves like the other controls discussed earlier in the chapter—that is, it supports properties such as DataSource, DataMember, DataTextField, DataValueField, and DataTextFormatString, and it can be bound to a data source and show its contents, as follows:

lstEmp.DataSource = ds.Tables["EmpTable"];
lstEmp.DataTextField = "lastname";
lstEmp.DataValueField = "employeeid";
lstEmp.DataBind();

The next code snippet illustrates how to write a comma-separated string with the values of the selected items. (This code is nearly identical to the code you would write to accomplish the same operation for a CheckBoxList control.)

public void ShowSelectedItems(Object sender, EventArgs e)
{
    StringBuilder sb = new StringBuilder("");
    for (int i=0; i < lstEmp.Items.Count; i++)
    {
        if (lstEmp.Items[i].Selected)
        {
            sb.Append(lstEmp.Items[i].Text);
            sb.Append(", ");
        }
    }
    lblResults.Text = sb.ToString();
}

Figure 1-3 shows the results of the code. The full source for the ListBox.aspx application is available on the companion CD.

Figure 1-3
This sample application illustrates the use of a data bound, multi-selection list box.

The programming interface of the list box also contains a SelectedItem property, which at first appears to make little sense because you are working with a multi-selection control. In this case, however, the SelectedItem property returns the selected item with the lowest index.


The Repeater Control

The Repeater control is a simple container control that binds to a list of items. It walks through the bound items and produces graphical elements according to a basic rendering algorithm and the HTML templates you supply. The Repeater control supports from one through five templates. These templates, which form a tabular structure, are described in Table 1-2.

Table 1-2 Templates Supported by the Repeater Control
Template | Description
HeaderTemplate | Determines the heading of the final output. It is rendered only once and prior to any row. It cannot contain data bound information.
ItemTemplate | Determines the output format of each row in the data source. This template is called for each item in the list and can contain data binding expressions.
AlternatingItemTemplate | Functions similarly to ItemTemplate. This template applies only to rows with an odd ordinal position.
SeparatorTemplate | Determines the HTML content that goes between each row. It cannot contain data bound information.
FooterTemplate | Rendered only once when all items have been rendered. It cannot contain data bound information.

When you call DataBind on the Repeater control, the control first attempts to locate the HeaderTemplate and, if found, applies it. The control then loops through the list items and applies the ItemTemplate to each one. When the AlternatingItemTemplate is defined, odd rows take it. If the SeparatorTemplate is found, the template is applied in between two consecutive items, regardless of whether the template item is normal or an alternate. When the FooterTemplate is specified, it is applied at the end of the loop.

The Repeater control has no built-in layout facility or predefined style that you can apply by using a declaration, nor does it support selection and editing. You could manually code selection and editing, but for those tasks, you are better off dropping the Repeater control in favor of the more powerful DataList and DataGrid controls. You’ll learn more about these controls later in the chapter.

Accessing Data Bound Information

The Items property of the Repeater control is a collection of RepeaterItem objects, each representing an individual row in the list. To define the template structure for normal and alternating items, you need a way to access the data source fields for the particular row being processed. This information is exactly what the expression Container.DataItem returns when you use Container.DataItem in a data-binding expression for the normal or alternate templates.

Container.DataItem represents the nth object bound to the RepeaterItem objects in a list. It evaluates to the same type of element as the caller placed in the Repeater control’s data source. Let’s look at some examples. If you place a DataTable object in the DataSource property of the Repeater control, then Container.DataItem evaluates to a DataRow object. It evaluates to DataRow because in .NET, a DataTable is perceived as a collection of rows and each row is implemented through a DataRow object. If you use a DataView object, the individual item is evaluated to a DataRowView object. If you use, say, an array of strings, Container.DataItem evaluates to a simple text string. Table 1-3 shows how to obtain the value of a field for the current row.

Table 1-3 Ways to Retrieve Data Item Values in Iterative Controls

Data Source

Data Item

DataTable

<%# ((DataRow) Container.DataItem)["Field"] %>

DataView

<%# ((DataRowView) Container.DataItem)["Field"] %>

Array of strings

<%# ((String) Container.DataItem) %>

Dictionary

<%# ((Dictionary) Container.DataItem)["entry"] %>

Because .NET objects are strongly typed, coercing types is an absolute necessity. If you find the syntax in Table 1-3 somewhat quirky, you can alternatively resort to the DataBinder class. This class features a static method named Eval that has an easier syntax to remember than that of standard data-binding expressions. When you use DataBinder.Eval to get the current data item in the expressions in Table 1-3, the code looks like this:

<%# DataBinder.Eval(Container.DataItem, "Field") %>

As you can see, DataBinder.Eval blurs the distinction between the element types in the control’s data source. However, because of automatic type inference and conversion, the housekeeping code for DataBinder.Eval can result in a slightly slower server response.

The DataBinder.Eval method has two prototypes:

public static Object Eval(Object container, String expr);
public static String Eval(Object container, String expr, String format);

The first argument identifies the data provider to which the next expression must be applied. The third argument, which is optional, specifies a format string that converts the results of evaluating the data-binding expression to String. When you need to apply formatting, DataBinder.Eval helps considerably to build a more readable statement. The following two instructions obtain the same result—formatting a numeric column as a currency value—but the first is easier to read and understand, albeit slightly slower:

<%# DataBinder.Eval(Container.DataItem, "Price", "{0:c}") %>
<%# String.Format("{0:c}", ((DataRowView) Container.DataItem)["Price"]) %>
caution | Using DataBinder.Eval does not give you more programming power. It just makes programming easier because it shields you from many of the casting and formatting details. In return for this, it adds a little more overhead to your code, so try to use it only when necessary, such as when you need formatting.
Repeater Control Events

In addition to firing the standard events supported by all .NET controls (such as Init, Load, PreRender, and DataBinding), the Repeater control can fire three of its own events, which are described in Table 1-4.

Table 1-4 Events Supported by the Repeater Control

Event

Description

ItemCreated

Fires whenever a new item is created, regardless of its type (header, footer, item, or separator) or its ordinal position.

ItemCreated(Object s,
    RepeaterItemEventArgs e)

The event structure makes available to your code the current item (the Item property), the index (the ItemIndex property), the type (the ItemType property), and the DataItem associated with it (the DataItem property).

ItemCommand

Fires whenever a user clicks a link button or a push button embedded in the control’s template.

ItemCommand(Object s, RepeaterItemEventArgs e)

The event structure provides the CommandSource property to let you know about the button that triggered the event.

ItemDataBound

Fires when an item is data bound, always before it is rendered.

ItemDataBound(Object s, RepeaterItemEventArgs e)

The ItemCommand event interface deserves a little more explanation because it is a peculiarity of iterative control and, like the Repeater control, is widely used with the DataList and DataGrid controls. ItemCommand fires whenever the user clicks any button in the Repeater control. You retrieve the current instance of the clicked object by using the CommandSource property. You can give each button control a name that identifies its action. You retrieve this name by using the CommandName property of RepeaterItemEventArgs.

Figure 1-4 shows an HTML table that was generated by using the Repeater control. This figure displays a few fields selected from the Northwind Employees database. (The full source code for the EmployeeList.aspx application is available on the companion CD.)

Figure 1-4
A data bound HTML table created using the Repeater control.

When the user clicks the Search button on the screen in Figure 1-4, the page runs a query against SQL Server and stores the default view of the retrieved table as the data source of the Repeater control.

Repeater has the following structure:

<asp:repeater runat="server" id="Repeater1">
    <HeaderTemplate> ... </HeaderTemplate>
    <ItemTemplate> ... </ItemTemplate>
    <AlternatingItemTemplate> ... </AlternatingItemTemplate>
    <SeparatorTemplate> ... </SeparatorTemplate>
    <FooterTemplate> ... </FooterTemplate>
</asp:repeater>

For each template, you provide the ASP.NET code that will be evaluated dynamically during the execution of the DataBind method. You don’t need to specify all the templates. However, for a reasonably complex control, you need to specify at least the header and the item. Let’s take a look at each of the templates for the example in Figure 1-4.

The Header template is processed only once and is responsible for opening the table and defining the caption.

<table style="border:1px solid black;" class="stdtext">
    <thead bgcolor="blue" style="color:white">
        <td><b>ID</b></td>
        <td><b>First Name</b></td>
        <td><b>Last Name</b></td>
    </thead>

If you don’t define the same number of headers as expected columns, the graphical effect will be poor but you won’t get run-time errors.

Items and alternating items are element types that the control can represent with different graphical settings. Suppose your goal is to build an HTML table. As the following code illustrates, the item templates can add only the necessary number of rows and cells for the overall buffer.

<ItemTemplate>
    <tr>
        <td bgcolor="white"> <%# DataBinder.Eval(Container.DataItem,
            "EmployeeID") %> </td>
        <td bgcolor="white"> <%# DataBinder.Eval(Container.DataItem,
            "FirstName") %> </td>
        <td bgcolor="white"> <%# DataBinder.Eval(Container.DataItem,
            "LastName") %> </td>
    </tr>
</ItemTemplate>

<AlternatingItemTemplate>
    <tr>
        <td bgcolor="lightblue"> <%# DataBinder.Eval(Container.DataItem,
            "EmployeeID") %> </td>
        <td bgcolor="lightblue"> <%# DataBinder.Eval(Container.DataItem,
            "FirstName") %> </td>
        <td bgcolor="lightblue"> <%# DataBinder.Eval(Container.DataItem,
            "LastName") %> </td>
    </tr>
</AlternatingItemTemplate>

For any of the fields, an alternative approach would be to use more direct syntax and avoid DataBinder.Eval, as shown here:

<%# ((DataRowView) Container.DataItem)["LastName"] %>

Like the Header template, the Footer template is drawn only once and only when all items are rendered.

<FooterTemplate>
    <tfoot>
        <td bgcolor="silver" colspan="3">
            <%# "<b>" + ((DataView) Repeater1.DataSource).Count +
                "</b> employees found." %>
        </td>
    </tfoot>
</table>
</FooterTemplate>

The Footer template defines the final row by using a <tfoot> HTML tag and makes sure that the final row spans all columns. Typically, you use the footer to display summary information. To obtain the total number of employees displayed, the code resorts to the Count property of the DataView class. To get the same information with a DataTable set as the data source, you would use the following expression:

<%# ((DataTable) Repeater1.DataSource).Rows.Count +
    " employees found." %>
note | If DataSource is set but no data is returned, the Repeater control attempts to render only the header and footer templates, if either is specified. No items and separators are rendered. If the DataSource property is not set, the Repeater control is not rendered.


The DataList Control

If in the previous example you absentmindedly replaced Repeater with DataList, your code would run perfectly and function in the same way. However, the two controls are not identical. DataList, which is a data bound control, offers you a full bag of new features, mostly in the area of graphical layout—in fact, its set of features begins where the set of features supported by the Repeater control ends. For example, it supports directional rendering in which items can flow horizontally or vertically, depending on the specified number of columns. DataList provides facilities to retrieve a key value associated with a current row in the data source and has built-in support for selection and editing. DataList also supports more templates and can fire more events than Repeater.

Basic data-binding operations that use DataList are similar to those that use Repeater. You use the DataSource property to bind the control to data and the DataBind method to refresh the user interface.

DataList1.DataSource = ds.Tables["EmpTable"];
DataList1.DataBind();

In template code, you can access data bound information using both the DataBinder object and the Container.DataItem expression.

Templates Specific to DataList

The DataList control supports two templates that we haven’t yet discussed: SelectedItemTemplate and EditItemTemplate. SelectedItemTemplate controls how the selected item is displayed. You should use it only when you need to display other controls or apply a particular logic. When you want to change only the appearance of the selected item, resort to a simpler cascading style sheet (CSS) style set via the SelectedItemStyle property.

EditItemTemplate specifies the graphical template for the row currently being edited. If you don’t specify your own template, the standard template is used, which renders any data bound element by using text boxes. Note that the template defines the layout of the control only when in edit mode. Managing the user interaction, saving the edit, or canceling the edit operation must be handled separately. For each template, you can use special style tags to set all the CSS attributes that you need via a declaration. These style tags are <AlternatingItemStyle>, <ItemStyle>, <HeaderStyle>, <FooterStyle>, <SeparatorStyle>, <SelectedItemStyle>, and <EditItemStyle>. The following code assigns a white background to the items and a thick black border:

<ItemStyle BorderColor="black" BorderWidth=3 BackColor="white" />

You can also use style properties that have the same name as the style tags, as shown in this code:

DataList1.ItemStyle.BackColor = Color.White;
note

In ASP.NET pages, you indicate colors by using strings. The ASP.NET run time silently converts the color name to a valid .NET type. To assign a color programmatically, you must use the predefined values of the Color enumeration in the System.Drawing namespace. If you don’t know the .NET name of a given color, use any of the following expression formats:

Color.FromName("white");
Color.FromName("#00ee00");
Events Specific to DataList

All iterative controls fire a generic ItemCommand event when a user clicks a link button or a push button, and this button is associated with a command name that is specified by the programmer. You use the command name within the ItemCommand event to recognize which button has actually been clicked. If these links and buttons are automatically generated during the execution of DataBind, it is especially difficult for you to assign a unique value to the OnClick event for each control. The next code example clarifies my point. The DataList control generates buttons based on the content of an array of strings.

<asp:DataList runat="server" id="DataList1"
    RepeatDirection="Horizontal"
    OnItemCommand="HandleItemCommand">

<ItemTemplate>
    <asp:linkbutton runat="server" CommandName="<%# Container.DataItem %>"
        Text="<%# Container.DataItem %>" /> &nbsp;&nbsp;&nbsp;
</ItemTemplate>

</asp:DataList>

The data source returned by the CreateDataSource function generates the output shown in Figure 1-5.

public ArrayList CreateDataSource()
{
    ArrayList a = new ArrayList();
    a.Add("Search");
    a.Add("Execute");
    a.Add("Load");
    return a;
}
Figure 1-5
A button bar generated using DataList. The ItemCommand event is the only way you can handle the click event for buttons.

Within the body of the HandleItemCommand function, you can analyze the command name and decide the proper action to take.

void HandleItemCommand(Object sender, DataListCommandEventArgs e)
{
    switch (e.CommandName)
    {
        case "Search":
            statusbar.Text = "searching...";
            return;
        case "Execute":
            statusbar.Text = "executing...";
            return;
        case "Load":
            statusbar.Text = "loading...";
            return;
    }
}

The full source for the CommandButtons.aspx application is available on the companion CD.

Special Command Names

The DataList control provides special support for five predefined command names: edit, update, delete, cancel, and select. Any link or button in a DataList control whose command name matches one of these five keywords receives special treatment from the control. The predefined events that fire when the user clicks buttons associated with these strings are listed in Table 1-5.

Table 1-5 Predefined Events of the DataList Control
Event | Description
CancelCommand | Fires when a button named Cancel is clicked.
DeleteCommand | Fires when a button named Delete is clicked.
EditCommand | Fires when a button named Edit is clicked. When a user clicks the Edit button, the item automatically enters edit mode. Edit mode implies use of the EditItem template.
UpdateCommand | Fires when a button named Update is clicked.
SelectedIndexChanged | Fires when a button named Select is clicked. Clicking a Select button automatically deselects the current item and selects the new item.

Edit mode and select mode are graphically rendered through their specific templates and CSS style.

note | Clicking a button named with a predefined keyword generates two events: first the generic ItemCommand event with the CommandName property set to the keyword and then the button-specific event (UpdateCommand).
Relating Graphical and Data Elements

To enable item selection and in-place editing, the DataList control has an internal mechanism that associates graphical items with corresponding elements in the data source. To retrieve this association, you set the DataKeyField property to the name of one data source field. The selected field must have content that allows you to uniquely identify the data item (a primary key). For example, if your data source is a table with information about a company’s employees, you could assign the employeeid field to DataKeyField, as shown here:

<asp:DataList runat="server" DataKeyField="employeeid">

After DataKeyField is set, the control ensures that the DataKeys collection contains all the field’s values for each item the control is currently displaying. You retrieve the key value based on the ordinal position of the item. For example, to obtain the key value of the currently selected item, you would do this:

int nEmployeeID = DataKeys[DataList1.SelectedIndex];
caution | The number of displayed items does not necessarily match the number of items in the data source. In some situations, to handle a special application’s requirements, you might need to programmatically hide some of the items. For example, you have to do this with DataGrid controls when pagination is enabled. (We’ll talk more about DataGrid controls later in this chapter.)

The following code example illustrates how to take advantage of the DataList control to create a “smart” list box, and Figure 1-6 shows the results. (The full source code for the SmartList.aspx application is available on the companion CD.) All items in the data source are rendered as a link button with the command name select. As you know, the DataList control provides special support for select. When the user clicks any of the selectable links, ASP.NET fires a SelectedIndexChanged event and updates the style of the clicked item according to the attributes set through the SelectedItemStyle tag. The appearance of the button is refreshed to reflect the new state. In response to the selection event, additional information about the employee is retrieved and displayed. A new button appears at the bottom of the page to let the user deselect any selected item.

When the page first loads, it retrieves only the fields needed to prepare the list of employees—that is, employeeid, firstname, and lastname. When the user clicks a link button, the DataList control is re-bound and an informative label appears with a message.

public void OnSearch(Object sender, EventArgs e)
{
    DataSet ds = new DataSet();

    String sConn = "server=localhost;uid=sa;Initial Catalog=Northwind;";
    String sText = "SELECT employeeid, firstname, lastname FROM Employees";
    sText += " WHERE employeeid >=" + tbEmpID.Text;
    SqlDataAdapter cmd = new SqlDataAdapter(sText, sConn);
    cmd.Fill(ds, "EmpTable");

    DataList1.DataSource = ds.Tables["EmpTable"].DefaultView;
    DataList1.DataBind();

    theLabel.Visible = true;
    theLabel.Text = "Click to read more.";
}

The DataList control has the following structure:

<asp:DataList runat="server" id="DataList1"
    DataKeyField="employeeid"
    OnSelectedIndexChanged="HandleSelection">

<SelectedItemStyle BackColor="lightblue" />

<HeaderTemplate>
    <h3>Northwind's Employees</h3>
</HeaderTemplate>

<ItemTemplate>
    <asp:linkbutton runat="server" commandname="select"
        Text='<%# ((DataRowView)Container.DataItem)["employeeid"] + " - " +
             ((DataRowView)Container.DataItem)["lastname"]  + ", " +
             ((DataRowView)Container.DataItem)["firstname"] %> ' />
</ItemTemplate>

<FooterTemplate> <hr> </FooterTemplate>
</asp:DataList>

To retrieve additional information about the clicked item, you need to issue another query based on a key value. You retrieve the key value associated with the selected item by using the DataKeys array.

void HandleSelection(Object sender, EventArgs e)
{
    int nEmpID = (int) DataList1.DataKeys[DataList1.SelectedIndex];

    String sConn = "server=localhost;uid=sa;Initial Catalog=Northwind;";
    SqlConnection cn = new SqlConnection(sConn);

    String sText = "SELECT title, hiredate, country, notes FROM Employees";
    sText += " WHERE employeeid = " + nEmpID.ToString();
    SqlCommand cmd = new SqlCommand(sText, cn);
    cn.Open();

    SqlDataReader dr = cmd.ExecuteReader();
    dr.Read();

    theLabel.Text =  "<b>" + dr["title"] + "</b><br>";
    DateTime dtime = Convert.ToDateTime(dr["hiredate"]);
    theLabel.Text += "Hired on " + dtime.ToShortDateString() + " from " +
                     dr["country"] + "<hr>";
    theLabel.Text += "<i>" + dr["Notes"] + "</i>";

    btnUnselect.Visible = true;
    dr.Close();
    cn.Close();
}

Using the SqlDataReader control instead of the DataSet control to retrieve data is more efficient when you have just one row to fetch. After the data has been successfully read, it gets properly formatted and the Unselect button is displayed.

Figure 1-6
The user interface of the application changes significantly when an employee name is selected. Additional information about the employee is retrieved and displayed, and a new button appears to let you deselect the currently selected item.

To enable the user to deselect an item, set the SelectedIndex property to -1.

void RemoveSelection(Object sender, EventArgs e)
{
    DataList1.SelectedIndex = -1;
    theLabel.Text = "Click to read more.";
    btnUnselect.Visible = false;
}

The DataList control and the panel that displays additional employee information are two cells of the same all-encompassing table. By using different settings for the layout properties of DataList, you can obtain a significantly altered design for the links without affecting the core of the code.

As the preceding code example illustrates, the DataList control is much more powerful than the Repeater control, but it is far from being perfect. It still lacks pagination, sorting, and powerful column-based rendering capabilities.


The DataGrid Control

The DataGrid control renders a multi-column, fully templated grid and is by far the most versatile of all data bound controls. The user interface it provides closely resembles a Microsoft Excel worksheet. Despite its rather advanced programming interface and full set of attributes, DataGrid simply generates an HTML table with interspersed links to provide interactive functionality such as sorting and pagination commands.

With the DataGrid control, you can create simple data bound columns that show data retrieved from a data source, template columns that let you design the layout of cell contents, and last but not least, command-based columns that allow you to add specific functionality to a grid. In the next chapters, I’ll delve more deeply into the implementation and the customizable features of the DataGrid control. But I think a discussion of the features that differentiate DataGrid from DataList is useful here.

The DataGrid control uses templates extensively but differently from the DataList and Repeater controls. DataGrid renders tables of data organized in columns, so the templates don’t apply to the control as a whole but rather to specific columns. DataList and Repeater work on a per-item basis and make you responsible for controlling the final layout of the data. DataGrid has just one possible layout—a sequence of columns—and supports the same events as the DataList control plus two more: PageIndexChanged and SortCommand.

Now let’s take a look at the DataGrid control in action. As you’ve learned, the DataGrid control works by displaying columns of data. In most cases, you need to specify which columns you want and how you want them displayed, but when the data to be rendered is uncomplicated, you can leave the control free to automatically generate the columns based on the structure of the data source. When you leave the control in charge, however, you cannot control the heading of each column, which defaults to the column name. The following code arranges a DataGrid control that uses alternating rows and automatically generates the columns. The results are shown in Figure 1-7. The full source code for the DataGrid.aspx application is available on the companion CD.

<asp:DataGrid id="grid" runat="server"
    AutoGenerateColumns="true"
    CellPadding="2" CellSpacing="2" GridLines="none"
    BorderStyle="solid" BorderColor="black" BorderWidth="1"
    font-size="x-small" font-names="verdana">

    <AlternatingItemStyle BackColor="palegoldenrod" />
    <ItemStyle BackColor="beige" />
    <HeaderStyle ForeColor="white" BackColor="brown" Font-Bold="true" />
</asp:DataGrid>

Notice that you can use the style properties to declare CSS styles.

Figure 1-7
The columns displayed in this figure are created using the DataGrid control.

The next code example illustrates how to fill the grid and refresh the user interface.

void OnLoadData(Object sender, EventArgs e)
{
    SqlConnection conn = new SqlConnection(txtConn.Text);
    SqlDataAdapter da = new SqlDataAdapter(txtCommand.Text, conn);
    DataSet ds = new DataSet();
    da.Fill(ds, "MyTable");

    grid.DataSource = ds.Tables["MyTable"];
    grid.DataBind();
}


Conclusion

In this chapter, you learned about the important controls and techniques you can use to bind data and .NET controls. We discussed simple data-binding expressions and then moved on to grid controls, touching also on list and iterative controls. Data binding coverage doesn’t end here, though, as it is too important a technology for building Web solutions with ASP.NET and ADO.NET. In the next chapter, you’ll learn about how to populate a DataGrid control by using data that takes advantage of sophisticated features such as pagination and sorting.


Pageable Data Grids

Chapter 2
Pageable Data Grids

Data reporting is key to Web applications, and the DataGrid control is the ASP.NET control of choice for data reporting. The DataGrid, which dynamically expands to a table, includes a number of features that customize the look and feel of the application and enhance its overall functionality. You set the content of DataGrid by using its DataSource property. As explained in Chapter 1, this content consists of any homogeneous collection of data. You can programmatically access this collection by using the Items property. Any item in the collection is represented by a DataGridItem object.


Constituent Items of DataGrid

The user interface of a DataGrid control comprises several types of items. These item types are grouped in the ListItemType enumeration. Each item plays a clear role and has a precise location in the hierarchy of the control’s user interface, as Figure 2-1 shows.

A DataGrid control is formed by using any combination of eight different items. Each item maps to a template style property of the DataGrid control, but remember from Chapter 1 that templates for DataGrid differ from those of DataList because they apply to the columns rather than to the control. Table 2-1 describes the graphical items that form an ASP.NET DataGrid control.

Figure 2-1
The layout of a DataGrid control.
Table 2-1 Graphical Items of the DataGrid Control
Item type | Description
Header | The grid’s header. This item cannot be data bound. The HeaderStyle property lets you control the look and feel of the header.
Item | A normal data bound row displayed in the grid. The ItemStyle property lets you control the look and feel of the row.
Separator | An item used to separate rows. This item type is not data bound.
AlternatingItem | A data bound row in an odd-numbered position. This item type is useful when you want to use different styles for alternating rows. The AlternatingItemStyle property lets you control the look and feel of the row.
Footer | The grid’s footer. This item type cannot be bound to a data source. The FooterStyle property lets you control the look and feel of the footer.
EditItem | The grid item currently in edit mode. This item type can be data bound. The EditItemStyle property lets you control the look and feel of the edited item.
Pager | The pager bar item you can place at the bottom of the grid to enable scrolling between pages. This item type cannot be data bound. The PagerStyle property lets you control the look and feel of the pager bar.
SelectedItem | The grid item currently selected. This item type can be data bound. The SelectedItemStyle property lets you control the look and feel of the selected item.

Each time an item in Table 2-1 is about to be created, an ItemCreated event is fired. By handling this event, you can customize the way in which the item—for example, a column caption or the pager bar—is rendered. Note that the ItemCreated event is not raised for each column but only once when the DataGrid control has finished creating the whole item. For example, the footer and header events are raised only when the header and the footer for each column have been created. Later in this chapter, you’ll learn to use the ItemCreated event.


Column Types

A DataGrid control is formed by data bound columns. The control has the ability to automatically generate columns that are based on the structure of the data source. Auto-generation is the default behavior, but you can manipulate that behavior by using a Boolean property named AutoGenerateColumns. Set the property to false when you want the control to display only the columns you explicitly add to the Columns collection. Set it to true (the default) when you want the control to add as many columns as is required by the data source. The default algorithm for column generation creates simple data bound columns that use literal controls to display the contents of the corresponding data source fields. The DataGrid control supports additional column types that can render the column data so that it performs an action when clicked. The column types are explained in Table 2-2. Each is derived from the DataGridColumn class.

Table 2-2 Column Types Supported by the DataGrid Control
Column Type | Description
BoundColumn | Displays a column that is bound to a field in a data source. It displays each item in the field as plain text.
ButtonColumn | Displays a command button for each item in the column. The text of the button can be data bound. The command name of the button must be common to all items in the column. When the value for the command name is Select, clicking the column button automatically selects the row.
EditCommandColumn | Displays a button column that is automatically associated with the Edit command. This class receives special support from the DataGrid control, which is manifested as a redrawing of the clicked row using a template. (I’ll talk more about this in Chapter 4.)
HyperLinkColumn | Displays the contents of each item in the column as a link. The link text can be bound to a field in the data source, or it can be static text. Also, the target URL can be data bound. Clicking a link column causes the browser to jump to the specified URL. This column class supports target frames.
TemplateColumn | Displays each item in the column according to a specified template, allowing you to provide custom controls in the column. (I’ll talk more about this in Chapter 3.)


Binding Columns

In real-world scenarios, when you create columns, you need more options than auto-generation offers. Auto-generation does not let you specify the header text, nor does it provide text formatting. Perhaps the strongest reason to opt for manual binding is that auto-generation always displays all columns in the data source in the order in which they are returned.

caution | If you plan to manually bind columns to your DataGrid control, make sure you explicitly set AutoGenerateColumns to false. You can do that either declaratively or programmatically. Otherwise, the DataGrid control will place extra columns—all the automatically generated columns—after the last column you manually create.

You typically bind columns using the <columns> tag in the body of the <asp:datagrid> server control.

<asp:datagrid runat="server" id="grid" ... >

    <columns>
        <asp:BoundColumn runat="server" DataField="employeeid"
            HeaderText="ID" />
        <asp:BoundColumn runat="server" DataField="firstname"
            HeaderText="First Name" />
        <asp:BoundColumn runat="server" DataField="lastname"
            HeaderText="Last Name" />
    </columns>
</asp:datagrid>

Alternatively, you can create a new column of the desired class, fill its members, and then add the class instance to the DataGrid control’s Columns collection by using the Add method. Here is some code that adds a BoundColumn class to a grid:

BoundColumn bc = new BoundColumn();
bc.DataField = "firstname";
bc.HeaderText = "First Name";
grid.Columns.Add(bc);

The order of columns in the collection determines the order in which the columns are displayed in the DataGrid control.

Bound Columns

The BoundColumn class displays in a single column the contents of a field in the data source. The structure of BoundColumn is characterized by the properties in Table 2-3.

Table 2-3 Structural Properties of the BoundColumn Class
Property | Description
DataField | The field name from the data source to which the column is bound.
DataFormatString | A string that specifies the display format for items in the column.
FooterText | The text displayed in the footer section of the column.
HeaderImageUrl | The URL of an image that will appear in the header section of the column.
HeaderText | The text displayed in the header section of the column.
ReadOnly | Indicates whether the items in the BoundColumn can be edited. If set to true, the column won’t be affected by the edit mode.
SortExpression | The sort expression used when the column is selected for sorting.

All the properties listed in Table 2-3 can be declared in the ASP.NET page layout or set programmatically. The names of the class members match the <asp:BoundColumn> tag attributes.

In addition to the properties listed in Table 2-3, the BoundColumn class supports the following style properties:

  • FooterStyle Represents the style properties for the footer

  • ItemStyle Represents the style properties for all column cells

  • HeaderStyle Represents the style properties for the header

The graphical style of a column is primarily influenced by these properties. If you don’t set any of them, the column will inherit the styles set at the level of the DataGrid control.

Button Columns

A button column contains a user-defined command button that corresponds to each row in the column. You specify the button caption by setting the Text property of the ButtonColumn class. Alternatively, you can bind the caption displayed in a command button to a single field in the data source by setting the DataTextField property. Clicking a command button in a column raises the ItemCommand event, which you can programmatically respond to by providing an event handler. Table 2-4 shows the key properties of the ButtonColumn class.

Table 2-4 Key Properties of the ButtonColumn Class
Property | Description
ButtonType | The type of button that will be displayed in the column. Values are defined in the ButtonColumnType enumeration: LinkButton (the default) and PushButton.
CommandName | The name of the command invoked when a user clicks a button in the column.
DataTextField | The field name from the data source to which the column is bound. If you set this property, the caption of each button will reflect the contents of the corresponding data source row.
DataTextFormatString | The display format for the caption in each command button.
FooterText | The text displayed in the footer section of the column.
HeaderImageUrl | The URL of an image that will appear in the header of the column.
HeaderText | The text displayed in the header section of the column.
SortExpression | The sort expression to use when the column is selected for sorting.
Text | The text displayed as the button caption. If you set this property, all buttons have the same text.

A button column allows you to associate an action with the items displayed in a column, but in most cases, you will want to use static text to identify the action (for example, Add, More, Remove). You can also define the button’s caption to depend on the underlying data source by setting DataTextField.

The following code is a simple but realistic example of how to create and use a button column in a DataGrid control. Figure 2-2 shows the corresponding output.

<asp:DataGrid id="grid" runat="server"
    AutoGenerateColumns="false"
    CellPadding="2" CellSpacing="2" GridLines="none"
    BorderStyle="solid" BorderColor="black" BorderWidth="1"
    font-size="x-small" font-names="verdana"
    DataKeyField="employeeid"
    OnItemCommand="HandleCommands">

    <AlternatingItemStyle BackColor="palegoldenrod" />
    <ItemStyle BackColor="beige" />

    <columns>
        <asp:BoundColumn runat="server" DataField="employeeid"
            HeaderText="ID" />
        <asp:BoundColumn runat="server" DataField="firstname"
            HeaderText="First Name" />
        <asp:BoundColumn runat="server" DataField="lastname"
            HeaderText="Last Name" />
        <asp:ButtonColumn runat="server" Text="More"
            CommandName="moreinfo">
            <itemstyle backcolor="lightblue" font-bold="true" />
        </asp:ButtonColumn>
    </columns>
</asp:DataGrid>
Figure 2-2
A DataGrid control with a button column that can display additional information for the clicked data item.

When a user clicks a button column, to execute code in response, you must write an ItemCommand handler and link it to the DataGrid control by using the OnItemCommand attribute.

<asp:datagrid runat="server" ...
    OnItemCommand="ItemCommandHandler">

An ItemCommand handler requires the following prototype:

void ItemCommandHandler(Object sender, DataGridCommandEventArgs e)

The DataGridCommandEventArgs class makes available three key properties:

  • Item Represents the DataGridItem element that was clicked

  • CommandName Represents the command name associated with the clicked button

  • CommandSource Represents the button object that raised the click event

The following code shows how to retrieve and display more information about the clicked data item. The full source code for the CmdColumns.aspx application is available on the companion CD.

void HandleCommands(Object sender, DataGridCommandEventArgs e)
{
    if (e.CommandName == "moreinfo")
    {
        int nEmpID = (int) grid.DataKeys[e.Item.ItemIndex];

        SqlConnection conn = new SqlConnection(txtConn.Text);
        String strCmd = "SELECT * FROM Employees " +
            "WHERE employeeid = " + nEmpID.ToString();
        SqlCommand cmd = new SqlCommand(strCmd, conn);
        conn.Open();
        SqlDataReader dr = cmd.ExecuteReader();
        dr.Read();
        MoreInfo.Text = BuildMoreInfoText(dr);

        dr.Close();
        conn.Close();
    }
}

The expression e.Item.ItemIndex evaluates to the ordinal position of the clicked item. As explained in Chapter 1 when we discussed DataList controls, the DataKeys array can contain a primary key value for each displayed item in the column. As the preceding code illustrates, the event handler retrieves this value and arranges a new query that requests more information for that particular row. This information is then organized into a readable format, as shown in Figure 2-3.

Figure 2-3
Clicking the More column button in this DataGrid control displays additional information about the item.
Link Columns

The HyperLinkColumn class works in much the same way as the ButtonColumn class, but HyperLinkColumn allows you to jump to a different URL within a specified frame. You set the Text property to specify the caption for the link. To specify the actual URL the user will navigate to when the link is clicked, you set the NavigateUrl property—if you set Text, all links in the column will share the same caption. Alternatively, you can bind the caption and URL in the column to a field in a data source by using the property pair DataTextField and DataNavigateUrlField. Table 2-5 shows the key properties of the HyperLinkColumn class.

note | The DataTextField and Text properties are assumed to be mutually exclusive. The HyperLinkColumn class assumes that you never have both set at the same time. If you do, DataTextField takes precedence and Text will be ignored.
Table 2-5 Key Properties of the HyperLinkColumn Class
Property | Description
DataNavigateUrlField | The field name from the data source that determines the URL to jump to when a user clicks a link.
DataNavigateUrlFormatString | Specifies the display format for the URL of the links, letting you create parametric URLs.
DataTextField | The field name from the data source to which the column is bound. If you set this property, the caption of each button will reflect the contents of the corresponding data source row.
DataTextFormatString | The string that specifies the display format for the caption in each command button.
FooterText | The text displayed in the footer section of the column.
HeaderImageUrl | The URL of an image to appear in the header section of the column.
HeaderText | The text displayed in the header section of the column.
NavigateUrl | The URL to jump to when the user clicks a link in the column.
SortExpression | The string that specifies the sort expression to use when the user selects the column for sorting.
Target | The target window or frame where the linked Web page should be displayed.
Text | The text displayed as the button caption. If you set this property, all the buttons have the same text.

For link columns, the formatting capabilities provided by the DataGrid control are extremely helpful because they allow you to arrange parametric URLs. Let’s alter our previous code example and point users that want to know more about a certain employee to a Web page. Instead of a button column, use the following link column:

<asp:HyperLinkColumn runat="server"
    DataNavigateUrlField="employeeid"
    DataNavigateUrlFormatString="moreinfo.aspx?id={0}"
    DataTextField="lastname"
    DataTextFormatString="More on {0}"
    Target="frInfo" />

In .NET, the expression {n} is used to indicate the nth argument of a series in much the same way as the old acquaintance %n is used with the function printf in plain old C programs. When used with the DataGrid control, however, the {n} expression is limited to just one argument. Thus the following assignments have only one placeholder that is silently determined by the DataGrid control:

DataNavigateUrlFormatString="moreinfo.aspx?id={0}"
DataTextFormatString="More on {0}"

DataNavigateUrlFormatString replaces its {0} placeholder with the content of the field pointed to by DataNavigateUrlField. Similarly, DataTextFormatString relies on the services of DataTextField. As a result, when the user clicks the cell of the employee whose ID is 5 (this field is employeeid), the URL that is linked to this is:

moreinfo.aspx?id=5

Figure 2-4 shows an application that uses a Web page (MoreInfo.aspx) and an embedded frame to display more information about a given employee. The full source code for the Hyperlinks.aspx application is available on the companion CD.

Figure 2-4
Additional information about employees can be displayed using an <iframe> tag and link columns.

To redirect the output of the linked URL, you can use the Target property to point to a particular window, frame, or floating frame. Floating frames are a browser-specific feature. Microsoft Internet Explorer renders them by using the <iframe> tag. Netscape browsers instead rely on the <layer> tag. The preceding code uses the following tag declaration:

<iframe frameborder="no" name="frInfo"></iframe>

Notice that for the code to work, you must close the <iframe> tag in the conventional way—that is, by using the closing tag </iframe>. Are you having difficulty seeing the shaded frame behind the white box in Figure 2-4? The rendering of the <iframe> tag is heavily affected by the frameborder attribute, which removes its ugly 3-D frame border, and the following CSS style:

<style>
    iframe {
        border:solid 1px black;
        filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2,
            OffY=2, Color='gray', Positive='true');
    }
</style>

The filter attribute can be used to give a shadow to almost all HTML tags. It works only with Internet Explorer 5 and later versions and is happily ignored on other browsers, so using it is harmless.

note | For a page using child frames, the functionality of button and link columns is similar except when reviewing previously visited pages. If the user clicks a button column, the subsequent action is taken within the same page. If the user clicks a link column, the browser navigates to a new URL.
Programmatic Binding

The columns of a DataGrid control are stored in the Columns collection. The collection is filled out during the page initialization phase. When the OnLoad event for the page occurs, the Columns collection is ready for use. At this time, though, the only columns described in the collection are the columns declaratively linked through the <columns> ASP.NET tag. Can you bind columns programmatically? Can you show and hide columns on demand? The answer to both questions is yes.

Just as any other ASP.NET element, DataGrid’s columns are also rendered through objects. To add a new column programmatically, first create a new column using one of the column classes as appropriate. For example, you can create a data bound column using this code:

BoundColumn bc = new BoundColumn();
bc.DataField = strField;
bc.HeaderText = strHeader;
grid.Columns.Add(bc);

Adding link or button columns is not very different from creating a data bound column except that you have to use a different column class and might need to address a different set of properties.

With programmatic binding, the rub is that the DataGrid control does not persist the information about the new, programmatically bound columns. In fact, only the columns statically linked through the <columns> tag are automatically added to the Columns collection of the DataGrid object when the page posts back to the server. To work around this issue, make sure that you programmatically add your own extra columns in the Page_Load event handler:

public void Page_Load(Object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        // Execute only the first time...

    }
    else
        BindColumnsProgrammatically();
}

In this way, extra columns are added each time the page is requested, ensuring the DataGrid consistency. This solution, though, does not solve the second question raised above. What happens if columns can be added on demand by clicking a button or as the result of a certain operation? In these cases, you don’t have a fixed set of columns to add through a user-defined method as in the previous code snippet.

Dynamic Column Binding

When the number of extra columns cannot be predetermined, to be able to persist them across multiple page requests, you can only track the added column and where in the grid it is added. Whenever the action that adds the column occurs, you must record the action on a persistent device and have enough information to allow you to repeat the column creation the next time the page is refreshed. You could persist column information in the page global ViewState property. However, since this information is eventually serialized into the page’s hidden __VIEWSTATE field, which is sent back and forth between the server page and the client browser, you might want to keep the amount of information as lean as possible to reduce the page payload. Instead of serializing each column, you could store only the information that you need to re-create the column. In the sample application, ProgBinding.aspx, I persist a string made of comma-separated numbers, each of which identifies a particular column. To restore columns, the application retrieves all the column codes from the ViewState and runs a statement like the one shown here:

switch(colCode)
{
    case "1":
        AddBoundColumn("title", "Position");
        break;
    case "2":
        AddBoundColumn("country", "Country");
        break;
    case "3":
        AddButtonColumn("More", "moreinfo");
        break;
}

The full source code for the ProgBinding.aspx application is available on the companion CD. Figure 2-5 shows the dynamically displayed columns.

Figure 2-5
Columns bound programmatically are persisted across multiple page requests.
ViewState vs. Attributes

ViewState is a global, page-scoped repository of persistent information. Each ASP.NET control manages internally its own ViewState repository, which is then poured out into the global ViewState object for the page.

The control-specific ViewState property is a protected member and cannot be accessed from ASP.NET code. As a result, applications cannot store persistent information within the state of a particular control using ViewState. To persist information on a per-control basis, applications could use the control’s Attributes collection. Attributes is one of the properties that controls automatically store in their own ViewState repository, so from the applications’ viewpoint the mechanism of persistence is exactly the same as with ViewState.

Although in practice you can interchangeably use ViewState and Attributes to persist information, they definitely have different design centers and have been introduced to serve clearly different purposes. ViewState is a collection of state values from the page and its controls. Attributes is meant to be the collection of HTML attributes for which ASP.NET controls do not provide an equivalent property. For example, you use Attributes to set the src attribute of an <iframe> tag or to assign a client-side JavaScript function to the onclick attribute of a button. All controls have (and persist) their own Attributes collection. Given its goal, the Attributes collection only accepts strings whereas ViewState can accept any valid .NET type. In addition, bear in mind that all attributes set through the Attributes collection are also written to the HTML page as custom attributes of the corresponding tags. (This is not necessarily a bad thing; just be aware of it.)


Data Pagination

Unlike the DataList control, the DataGrid control supports data pagination, that is, the ability to divide the displayed data source rows into pages. In real-world scenarios, the size of a data source easily exceeds the page real estate. So to preserve scalability on the server and to provide a more accessible page to the user, you display only a few rows at a time.

Pagination, however, requires a certain amount of work behind the scenes. The control must know how many items per page you want to display and then set up a pagination toolbar to let the user move between pages. In addition, the control must be able to download and process only the records that belong in the current page. Finally, the control must track and expose the index of the current page.

Setting up pagination for a DataGrid control is complicated because the control instance is on the Web server, not the client browser. The ASP.NET DataGrid control detects user events on the client, such as clicking a button to move to a new page. These events are automatically posted back to the Web server and processed. As a result, the browser receives a new version of the same page in which a new set of rows is shown.

What does this mean for your code? The Web server needs to know about the data source every time a postback event associated with the grid is generated. This is a key point that I’ll deal with later in the chapter. For now, suffice to say that you have to figure out the best way to track the data source on the server any time processing is required, and you must provide the logic to carry out the desired tasks—paging, sorting, filtering, and so on. Although ASP.NET and ADO.NET classes help you implement a lot of these tasks, they don’t automatically handle them all. For this reason, I like to refer to the pagination feature of DataGrid controls as semi-automatic built-in features.

Semi-Automatic Pagination

Before I discuss any further the programming details of pagination, let me spend some time explaining the overall programming interface of the DataGrid control so that you can understand the rationale behind some apparently odd design choices.

Remember that the DataGrid control—just like any other ASP.NET control—was designed to function on the Web as a bridge between the browser and Web server via postback events. To be effective, postback events need to coordinate state management with the client-side environment. The ASP.NET run time manages to restore the state of the page before actually invoking your initialization code (by using the Page_Load event and setting the IsPostBack property), and then it processes the event. To preserve any significant state information, the run time needs to pack the information, send it to the client, and then retrieve it when the page is posted back for further processing. The control’s view state (set in the ViewState property) is the architectural element responsible for preserving and retrieving state information.

For performance reasons, a DataGrid control does not store the data source within the view state. As a result, you are responsible for retrieving and making available on the server the DataGrid control’s data source whenever it is needed. Possible options for doing this include reloading the data source from the database server each time the data source is needed or storing the data source in global objects such as Session, Cache, Application, or (and why not?) ViewState or in files. What you really need to know at this point in our discussion is that the code of the DataGrid control cannot make assumptions about how the data source is stored. When a postback event occurs, the DataGrid control’s DataSource property is undefined, so you are responsible for filling (or refilling) it properly and then restoring data from the storage medium appropriate for the scalability of the application.

caution | Do not access the DataGrid control’s DataSource property in ASP.NET code without first taking precautionary measures, because the DataGrid control will not necessarily evaluate to a non-null object. Unless you take the appropriate counter-measures discussed later in this chapter, the DataSource property will be undefined when postback events are raised even though you correctly set the property when the page was first accessed. The DataSource property is not persisted.

So as you can see, because the DataGrid control by design cannot make assumptions about how its data source has been persisted on the server (the data source does not travel with the rest of the DataGrid control’s programming interface on the Web), the control cannot implement a totally automatic engine for pagination or sorting.

To enable pagination of the DataGrid control, you need to tell the control about it. You do this through the AllowPaging property. When pagination is enabled, the rendering algorithm of the DataGrid control changes slightly. By default (the default is pagination turned off), the grid renders all the items in the data source in the order of the first item to the last. When pagination is on, the DataGrid control renders only the rows that fall in the range of the current page. The control keeps track internally of the requested page size, the subsequent page count, and the current page index. Basically, it has the built-in capability to page through records for a given page size and data source, but it requires you to correctly set up the data source each time a new page is requested.

Properties for Pagination

Table 2-6 shows the DataGrid control properties that support pagination.

Table 2-6 Pagination Properties for the DataGrid Control
Property | Description
AllowPaging | Enables or disables pagination. This property is false by default.
CurrentPageIndex | Gets or sets the current page index.
PageSize | Gets or sets the current page size.
PageCount | Gets the number of pages according to the current page size.
AllowCustomPaging | Enables or disables custom pagination. This property is false by default.
VirtualItemCount | Gets or sets the total number of items you plan to display when using custom pagination. By contrast, the item count for default pagination is obtained from the data source.

The DataGrid control supports two flavors of pagination: default and custom. With default pagination, you simply provide the whole data source; the grid handles pagination internally. With custom pagination, you are expected to store in the DataSource property only those rows to be displayed on the current page; the DataGrid control renders all the rows it finds in the Items collection and sorts them from first item to last. So you must ensure that the collection corresponds to the current page. Furthermore, the index of the current page must be explicitly set, and the DataGrid control determines the value of the PageCount property by using VirtualItemCount. I’ll cover this in more detail later on.

The Pager Bar

The pager bar is an interesting and complimentary feature offered by the DataGrid control to let users easily move from page to page. The pager bar is a row displayed at the bottom of the DataGrid control that contains links to available pages. When you click any of these links, the control automatically fires the PageIndexChanged event and updates the page index accordingly.

The pager bar offers two display options: it can show a generic pair of Next and Previous buttons or a detailed series of numeric buttons, each of which points to a particular page. (See Figure 2-6.) The next code fragment illustrates these options. You can also thoroughly customize the pager bar—determining its font, size, background and foreground colors, and alignment settings—or even hide the standard bar and roll your own. You control the pager bar by using the PagerStyle property’s Mode attribute. Values for the Mode attribute come from the PagerMode enumeration.

if (UseNumericPages.Checked)
    grid.PagerStyle.Mode = PagerMode.NumericPages;
else
    grid.PagerStyle.Mode = PagerMode.NextPrev;
Figure 2-6
The pager bar’s two default button options.
Page Button Count

When the pager mode is set to NumericPages, you can control the number of buttons displayed. You normally do this to avoid an endless (and thus unhelpful) list of numbers in the case of a very lengthy data source. By setting the PageButtonCount property, you define the maximum number of buttons you want the pager bar to host. The default value for PageButtonCount is 10.

grid.PagerStyle.PageButtonCount = 3;

If the DataGrid control has more pages than are specified in PageButtonCount, ellipsis buttons are displayed in the pager bar. When clicked, the ellipsis button displays the next or previous set of numeric buttons.

Page Button Text

Next and Previous are only the default labels for the pager bar. When the pager bar mode is set to NextPrev, you can set the text you want displayed for both buttons. To do so, you use any valid HTML expressions, including images.

grid.PagerStyle.PrevPageText = "<img src=prev.gif>";
grid.PagerStyle.NextPageText = "<b>Next...</b>";

Changing the labels for numeric buttons is a bit more complicated because you don’t have predefined properties to set, but you can still change them. I’ll discuss how later in this chapter.

Pagination in Action

To enable pagination in DataGrid controls, you must perform several tasks. First you have to enable paging at the DataGrid control level by setting the AllowPaging property to true. You must assign the desired value to PageSize to let the control know how many rows you want displayed for each page. (The default for PageSize is 10.)

Second you must make sure that the data source is properly refreshed whenever the user scrolls back and forth between pages. The DataGrid control raises the PageIndexChanged event each time a user clicks the pager bar to jump to a certain page. You must write a handler for PageIndexChanged and make it accomplish a couple of tasks.

<asp:datagrid runat="server" id="grid"

    AllowPaging="true"
    PageSize = "4"
    OnPageIndexChanged="PageIndexChanged">

    <PagerStyle PageButtonCount="3" Mode="NumericPages" />

</asp:datagrid>

The PageIndexChanged event handler has the following prototype:

void PageIndexChanged(Object sender, DataGridPageChangedEventArgs e)
{
    grid.CurrentPageIndex = e.NewPageIndex;
    // TO DO: Refresh the data source and re-bind
}

In the body of the handler, you must set CurrentPageIndex to the page the user clicked. You get the index of the newly requested page by using the NewPageIndex property of the event. If needed, you can also retrieve a current instance of the button that caused the event by using the CommandSource property of the DataGridPageChangedEventArgs class.

caution | The DataGrid control does not automatically set the new page index. It only notifies the event handler of the index requested by the page. You are responsible for assigning the value to the CurrentPageIndex property. If you omit this step, you will not experience a run-time error but the DataGrid control won’t page.

In addition to updating the page index, the PageIndexChanged event handler must refresh the DataGrid control. The way in which you accomplish this depends completely on your application and how you obtain the data source. Typically, you must reassign the DataGrid control’s DataSource property and invoke the DataBind method to initiate HTML rendering.

grid.DataSource = CreateDataSource();
grid.DataBind();

Figure 2-7 shows a pageable DataGrid control in action.

Figure 2-7
A pageable DataGrid control
tip | Before being assigned the new page index in the body of the PageIndexChanged event handler, the CurrentPageIndex property contains the value of the previously displayed page. You can use this information for implementing special tracking features in your DataGrid control.

You use the CurrentPageIndex property to determine the currently displayed page in the DataGrid control when paging is enabled. You can also use this property to programmatically control which page is displayed. CurrentPageIndex, in fact, is a read-write property, so if you need to display a given page in the DataGrid control without any user intervention, set CurrentPageIndex to the page index you need and then rebind the data to the control. Bear in mind that the CurrentPageIndex property is zero-based.

Customizing the Pager Bar

You can shape the appearance of the pager bar by using the PagerStyle property, which evaluates to a DataGridPagerStyle object. DataGridPagerStyle exposes properties such as BackColor, Font, and BorderStyle, as well as a property for setting CSS styles. Many of these attributes apply to the pager bar as a whole, so what do you do when you want to change the style of the pager bar’s individual buttons? You need to hook into the ItemCreated event.

<asp:datagrid runat="server" id="grid"

    AllowPaging="true"
    OnItemCreated="ItemCreated"
    OnPageIndexChanged="PageIndexChanged">

    <PagerStyle PageButtonCount="3" Mode="NumericPages" />

</asp:datagrid>

The ItemCreated event occurs on the server whenever a constituent element of the DataGrid control is created. By hooking into this event, you can modify on the fly both the HTML structure and the style of the element. Since this event fires for all the elements, the first task you must perform in the handler is to determine whether the item being created is exactly what you were waiting for. You check the type of the element by using the ItemType property. A reference to the item being created is returned by e.Item. The ListItemType enumeration lists all the feasible elements supported by the DataGrid control.

void ItemCreated(Object sender, DataGridItemEventArgs e)
{
    // Get the type of item being created
    ListItemType elemType = e.Item.ItemType;

    // Make sure it is the pager bar
    if (elemType == ListItemType.Pager)
    {
        // The pager bar as a whole has the following layout:
        //
        // <TR><TD colspan=X> ... links ... </TD></TR>
        //
        // Item points to <TR>. The code below moves to <TD>.
        TableCell pager = (TableCell) e.Item.Controls[0];

        // More code goes here
    }
}

In the overall layout of the DataGrid control, the pager bar is rendered by using a TableRow object. Within this TableRow object, you find exactly one TableCell control that spans all columns. The link buttons the user sees and perceives as the full pager bar are the contents of the TableCell control. Let’s review the HTML code that is generated for the first pager bar of Figure 2-8. You can check this code at any time by snooping into the HTML source code of a page that uses a DataGrid control.

<tr style="background-color:PaleGreen;font-weight:bold;">
    <td colspan="2">
        <span>1</span>&nbsp;
        <a href="javascript:__doPostBack(...)">2</a>&nbsp;
        <a href="javascript:__doPostBack(...)">3</a>
     </td>
</tr>

The settings you enter by using PagerStyle apply only to the TableRow control—the ASP.NET counterpart of a <tr> tag. If you need to modify controls placed inside TableRow, you can use only ItemCreated.

TableCell pager = (TableCell) e.Item.Controls[0];

The preceding expression retrieves the TableCell control that represents the <td> tag, inside which are the pager buttons. To access the pager buttons, you need to loop through the Controls collection of the TableCell control. Note that anything generated through ASP.NET is a control, including literal content. The blanks that separate the button captions are controls as well. To skip over them, you can use, for example, a for statement with a step value of 2.

for (int i=0; i<pager.Controls.Count; i+=2)
{
    // i.th control is pager.Controls[i]
}

Now let’s see how to associate a CSS stylesheet with each pager bar button and how to modify the text displayed for each element. To make the pager bar more readable, let’s enclose the previous and next page numbers in square brackets and prefix the text Page to the label of the current page. Figure 2-8 shows the output. The full source code for the CustomPager.aspx application is available on the companion CD.

Figure 2-8
A customized pager bar enhances accessibility.

The content of the pager bar cell is heterogeneous. It contains n elements of which n-1 are clickable link buttons and one is a static label. Of course, this label refers to the current page. You need to distinguish among the control types to apply different settings to each. To check whether a given object is of a particular type, you can use the is operator and then cast the generic object as the needed type.

for (int i = 0; i < pager.Controls.Count; i += 2)
{
    Object o = pager.Controls[i];
    if (o is LinkButton)
    {
        LinkButton h = (LinkButton) o;
        h.Text = "[ " + h.Text + " ]";
    }
    else    // Can only be a Label
    {
        Label l = (Label) o;
        l.Text = "Page " + l.Text;
    }
}
note | The C# is type operator checks whether an object expression is of a particular type. In Microsoft Visual Basic .NET, the Is operator has a different goal. You use it to determine whether two object variables refer to the same instance of an object. In Visual Basic .NET, to obtain the same behavior as the C# is operator, you have to resort to the TypeOf...Is operator.

Once you hold a valid instance of the link button or label, you can set any other property each provides. For example, you can assign each a new CSS style by using the CssClass property.

tip | When defining a new CSS style for a control, don’t be afraid to use DHTML behaviors. Although they are supported only by Internet Explorer version 5 and later versions, all other browsers ignore them, so you’ll experience no ill effects.
Replacing the Standard Pager Bar with Your Own

If for any reason you don’t want your pages to display the standard pager bar, you can replace it with your own. You hide the pager bar by turning off its Visible attribute.

<PagerStyle Visible="false" />

The DataGrid control does not provide you with a built-in mechanism for plugging in a custom pager bar. But changing the structure of the page slightly allows you to add your own. In this example, the DataGrid control and your personal pager bar are two rows of the same table.

<table class="PersonalPager">
    <tr><td>
        <asp:datagrid ...> ... </asp:datagrid>
    </td></tr>
    <tr><td>
        personal pager here
    </td></tr>
</table>

Depending on the look you desire for the grid, some of the settings you applied at the DataGrid control level have to be moved to the outer table level, for example, background color and shadow settings. The outside table also needs to have a border.

The following code shows how to transform the pager bar into a numeric text box with a button enabling the user to jump to a specified page. The user enters the page number in the text box and then clicks the Go button to jump to that page. The full source code for the PersonalPager.aspx application is available on the companion CD.

<asp:panel runat="server">
    <asp:label runat="server" cssclass="stdtext" text="Page" />
    <asp:textbox runat="server" id="GotoPage" cssclass="stdtextbox"
        width="30px" text="1" />
    <asp:button runat="server" cssclass="FlatButton"
        Text="Go" OnClick="OnGotoPage" />
</asp:panel>
note | If you wanted to, you could display both the ordinary pager bar and a personal pager bar. Really, the personal pager bar is any custom toolbar appended to the bottom of the DataGrid control.

Figure 2-9 shows what the personal pager bar looks like on a Web page. Notice in the figure which graphical element has so far been neglected in our discussion.

Figure 2-9
The footer and personal pager bar in a DataGrid control.

Although the DataGrid control and the personal pager bar together form a unique element, each are distinct HTML elements and can be shown individually. To avoid showing the pager bar when the DataGrid control is not shown, you might want to consider displaying both elements at the outer table level. For example, when the DataGrid control is shown only as the result of a postback event, the personal pager bar might be not be hidden and displayed. You can make sure the elements are displayed properly by handling the Visible attribute on the outer table.

<table runat="server" id="theTable" visible="false" class="PersonalPager">

Note, though, that Visible is an ASP.NET attribute, so a runat="server" attribute is also needed.

Using the Footer

In a DataGrid control, the footer is hidden by default. To turn it on, you must set the ShowFooter property to true. Of course, you can also do that declaratively by using the ShowFooter attribute.

<asp:datagrid runat="server"

    ShowFooter="True">

The footer style is subject to full modification via the FooterStyle property, so you can change its font, colors, and borders. One aspect you cannot change declaratively—and indeed often need to change—is the structure of the footer. The footer is rendered via an empty row, so it maintains the same structure as other rows. If this footer structure suits you, use the column’s FooterText property to assign it HTML contents. If you want the footer to have a different number of columns or columns of different widths, you must intervene at the time the footer is created, namely in the ItemCreated event handler.

void ItemCreated(Object sender, DataGridItemEventArgs e)
{
    ListItemType elemType = e.Item.ItemType;

    // Handle other types of items here

    if (elemType == ListItemType.Footer)
    {
        // Footer is just an empty row. Remove all
        // the cells but one and span over all.
        TableCellCollection tcc = e.Item.Cells;
        int nTotalCols = tcc.Count;

        // Use nTotalCols as tcc.Count changes
        // dynamically as you remove cells
        for (int i=0; i<nTotalCols-1; i++)
            e.Item.Cells.RemoveAt(1);

        // Only 1 cell at this time ...
        TableCell c = e.Item.Cells[0];
        c.ColumnSpan = nTotalCols;
        c.Text = grid.PageCount.ToString() + " pages found.";
    }
}

In most cases, you need a footer made up of a single cell that works as a summary row. To obtain this, you must remove at run time all cells but one, as shown in the preceding code example. Next, you set the ColumnSpan property of the remaining cell to the total number of the columns and display the summary text.

Custom Pagination

Every time the DataGrid control moves to a different page, all contents in the associated data source have to be reloaded. No matter how you retrieve the data—whether from a database, an XML file, or the Session object—as long as you use default pagination, you must have all that data stored in memory to successfully refresh the grid. When the data source is very large, storing it can consume a lot of resources on the Web server.

Custom pagination enables you to load only the segment of data needed to display a single page. To enable custom pagination, you set both the AllowPaging and AllowCustomPaging properties to true. Next, you provide code that handles the PageIndexChanged event.

So how is coding actually different when you enable custom pagination? The only significant difference between default paging and custom paging is the way in which the DataGrid control processes what’s stored in the Items property. In the case of default paging, the DataGrid control assumes to have in memory all possible items and, based on the page size and current index, extracts a subset of items that will fit in the selected page. With custom paging, the DataGrid control always displays all contents of the Items property, so you are responsible for loading only the records that fit in the selected page. This approach clearly requires less Web server memory and looks inherently more scalable—at least for large blocks of data. Let’s see custom pagination in action.

Setting the Virtual Item Count

When you use custom paging, you don’t need to handle the pager bar yourself. As long as you inform the DataGrid control about the virtual number of items you plan to display through all pages, you have pager bar functionality served for free, as usual. You let the DataGrid control know about the virtual item count by setting the VirtualItemCount property. You should set the property before the DataGrid control is displayed. A good place to set it is in Page_Load when IsPostBack is False.

if (!Page.IsPostBack)
    SetVirtualItemCount();

If your DataGrid will show the entire contents of a table—say, the Products table of the SQL Server 2000 Northwind database—the virtual item count is simply the total number of records found in the table.

public void SetVirtualItemCount()
{
    // Set up the connection
    String strConn = "DATABASE=Northwind;SERVER=localhost;UID=sa;PWD=;";
    SqlConnection conn = new SqlConnection(strConn);

    // Set up the command
    String strCmd = "SELECT COUNT(*) FROM products";
    SqlCommand cmd = new SqlCommand(strCmd, conn);

    // Execute the command
    conn.Open();
    int nItemCount = (int) cmd.ExecuteScalar();
    conn.Close();

    // Set the grid's virtual item count
    grid.VirtualItemCount = nItemCount;
    return;
}
note | ExecuteScalar is a method of the ADO.NET command classes, including in particular, SqlCommand. It executes a SQL command or a stored procedure, and it returns only the scalar value on column 1, row 1 in the result set. ExecuteScalar returns the value boxed in an Object class, so casting is necessary to obtain, for example, a true integer value.
Obtaining the Page Content

With custom paging, the amount of memory allocated is limited to the number of items that fit in a single grid page. The rub is, how can you get all and only the records that fit in a particular page? Once again, the way you handle this issue is strictly application-specific. The SQL language provides no support for pagination. The only way to get records from a SQL-based data management system is by using the SELECT statement. The only way to restrict the set of rows returned is by using the WHERE clause. Therefore, you must figure out a way to retrieve the contents of a particular page based on some sort of field condition. Because no official SQL-based solution exists, any assumption you can make in your own project given the structure of your own databases is valid. In the remainder of this section, I’ll examine a SQL case scenario. Consider the following SQL statement:

SELECT TOP n Fields FROM Table WHERE Key > page_related_info
caution | Very few databases support the TOP clause in the SELECT statement. The TOP clause was introduced with SQL Server version 7 and has no counterpart in SQL Server version 6.5 and Oracle. Informix SQL does provide a similar behavior through the FIRST clause. Although less flexible, the SET ROWCOUNT statement can be used as a rougher counterpart for TOP when this clause is not supported.

With SQL Server 7 and later versions, SELECT with a TOP clause returns from the specified table the first n rows that match the Boolean comparison in the WHERE clause. It goes without saying that, for our purposes, n is the page size of the DataGrid control.

The role of page_related_info is less defined. Its function has to be such that, based on the page number, you can directly access the first record entered in that page. If the table has a unique key column set with a regular series of numbers, you can calculate the first value for the key field on a given page with a simple arithmetic expression, based on the first record to display.

For example, the Northwind Products table’s ProductID field contains incrementing numbers: 1, 2, 3, and so on. You can then use this field as the key column for your query. The statement that loads the records for page 4, then, looks like this:

SELECT TOP n * FROM Products WHERE productid > (n-1)*4

Page 4 contains records from 31 through 40. Even if your table does not have a column like this, you should consider adding a new one ad hoc. Doing so is not manna from heaven, though, as you must regenerate the column every time a row is deleted. Putting this all together, you can retrieve the rows needed for display as follows:

SqlDataReader dr;
SqlDataReader CreateDataSource(int nPageIndex)
{
    // Page index is assumed to be 0-based
    int nPageSize = grid.PageSize;
    int nBaseProductID = nPageSize * nPageIndex;

    // Set up the connection
    String strConn = "DATABASE=Northwind;SERVER=localhost;UID=sa;PWD=;";
    SqlConnection conn = new SqlConnection(strConn);

    // Set up the command
    String strCmd = "SELECT TOP " + nPageSize + " " +
            "productid, productname, quantityperunit, unitsinstock " +
            "FROM products " +
            "WHERE productid >" + nBaseProductID;
    SqlCommand cmd = new SqlCommand(strCmd, conn);

    // Execute the command
    conn.Open();
    dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    return dr;
}

When you retrieve data frequently, the SqlDataReader class is a better option than SqlDataAdapter because the former results in a structure—the data reader—that is faster to load than the data set. While the data reader is in use, the associated connection is busy. The argument passed to ExecuteReader specifies that the connection must be closed when the SqlDataReader object is closed. CreateDataSource passes the SqlDataReader object to the DataGrid control, which stores the object in its DataSource property. At this point in the code, the data reader and the connection are still open. Setting DataSource does not automatically transfer the content of the data source to the DataGrid control’s memory (the Items property). Until the content is bound, you cannot close the data reader and release the database connection. The following code performs this binding and closes the data reader:

grid.DataSource = CreateDataSource(0);
grid.DataBind();
dr.Close();

Figure 2-10 shows a snapshot from the sample application illustrating custom paging. The full source code for the CustomPaging.aspx application is available on the companion CD.

Figure 2-10
Custom pagination was used to create the scrolling mechanism in this database.
Using Generic SQL

Let’s explore two more general approaches to implementing custom pagination that are not dependent on any application-specific assumption or precondition. One is based on T-SQL cursors and the FETCH statement. The other is based on a generic and parametric SQL command made of three nested SELECT statements.

The first approach, which is based on server-side cursors, has two major drawbacks. First and foremost, server-side cursors cause a loss in performance, which is why they are a last resort solution. The second drawback is that each row you fetch via cursors is stored in a separate result set. To bind these rows to the DataGrid control, you need to merge the result sets into a single DataTable object.

A much better (but less generic) approach can be inferred from the following SQL statement, which retrieves the rows displayed on page 3, where pages have 2 records each and are sorted by the lastname field:

SELECT TOP 2 * FROM
    (SELECT TOP 2 employeeid, lastname FROM Employees
        WHERE lastname IN
        (SELECT TOP 6 lastname FROM Employees ORDER BY lastname)
        ORDER BY lastname DESC) AS tmp
    ORDER BY tmp.lastname

As you can see, the statement comprises three nested SELECT commands. Despite the apparent complexity, the statement takes significant advantage of the query optimization engine that comes with SQL Server 2000. The key operations that the query performs can be more easily described in the temporary tables shown below:

SELECT TOP 6 employeeid, lastname INTO t1 FROM Employees ORDER BY lastname
SELECT TOP 2 employeeid, lastname INTO t2 FROM t1 ORDER BY lastname DESC
SELECT * FROM t2 ORDER BY lastname

The first statement, which corresponds to the innermost nested SELECT command, copies into the temporary table t1 a subset of rows that, once sorted, will contain the rows on the page to be displayed. For example, if 2 is the page size, and you want to access page 3, then the last 2 rows of the first 2*3 rows are what you need. These 2 rows are moved to another table t2 in the reverse order and then returned.

Using temporary tables is not really effective and, more important, can create additional conflicts in a Web scenario where many concurrent requests must be processed. Nested queries provide the same outcome with much better performance, though at the price of a less readable SQL syntax.

To create a more generic SQL statement, you can parameterize three elements: the number of rows to return, the number of rows to fetch, and the field to sort by. The number of rows to return is the grid’s page size (or less if the total number of rows doesn’t exactly match a multiple of the page size). The number of rows to fetch is calculated by multiplying the page size by the 1-based index of the page to show. The TOP and the ORDER BY clauses of the T-SQL language do not accept parameters, so a DataGrid that wants custom pagination should prepare its query command through string formatting rather than regular SQL parameters:

// Prepare the SQL command
StringBuilder sb = new StringBuilder("");
sb.Append("SELECT TOP {0} * FROM ");
sb.Append("(SELECT TOP {0} employeeid, firstname, lastname ");
sb.Append("FROM Employees WHERE {1} IN ");
sb.Append("(SELECT TOP {2} {1} FROM Employees ORDER BY {1}) ");
sb.Append("ORDER BY {1} DESC) AS tmp ");
sb.Append("ORDER BY {1}");
String strCmd = sb.ToString();

The {n} placeholders are replaced with actual data through the String.Format method.

int nRowsToDisplay = grid.PageSize;
int nMod = grid.VirtualItemCount % grid.PageSize;
if (nPageIndex == grid.PageCount && nMod > 0)
    nRowsToDisplay = nMod;
strCmd = String.Format(strCmd,
    nRowsToDisplay, strCurrentSortExpr, grid.PageSize * nPageIndex);

If the page to show is the last one, then the number of rows to display is the page size or the modulus of the total number of items and the page size. Finally, the sort expression is the current sort expression of the DataGrid, or the primary key if you don’t support sorting. The full source code for the UsingGenericSql.aspx application is available on the companion CD.


Sorting Columns

The DataGrid control does not actually sort rows, but it provides good support for sorting as long as the sorting capabilities of the underlying data source are adequate. The data source is always responsible for returning a sorted set of records based on the sort expression selected by the user through the DataGrid control’s user interface. The built-in sorting mechanism is triggered by setting the AllowSorting property to true.

<asp:datagrid id="grid" runat="server"

    AllowSorting="True"
    OnSortCommand="SortCommand">

When AllowSorting is set to true, the DataGrid control offers a link button for rendering the column caption. When users click one of these sortable columns, the SortCommand event is fired. In the body of the SortCommand event handler, you have to refresh the contents of the DataGrid control so that it reflects the requested sort.

Setting Up Sorting

You make a column sortable by setting the SortExpression property. You can set it declaratively in the ASP.NET page layout or you can set it programmatically. SortExpression evaluates to any expression that the data source understands as a sort command. Typically, SortExpression is the name of a field in the data source. If needed, though, you can sort by multiple fields by separating them with a comma. You can specify DESC (or DESCENDING) and ASC (or ASCENDING) to indicate a direction for sorting. By default, sorting is ascending.

<asp:BoundColumn runat="server" DataField="lastname"
    HeaderText="Last Name" SortExpression="lastname, firstname" />
<asp:BoundColumn runat="server" DataField="country"
    HeaderText="Last Name"
    SortExpression="country DESC, lastname, firstname" />

After you set up the columns to sort by and the relative expressions, you’d better take care of the SortCommand event handler. The typical logic for this handler is to sort the list of items and then rebind the data to the DataGrid control.

void SortCommand(Object sender, DataGridSortCommandEventArgs e)
{
   ViewState["SortExpression"] = e.SortExpression;
   UpdateDataView();
}

The SortCommand event handler knows about the sort expression through the SortExpression property, which is provided by the DataGridSortCommandEventArgs class. In the preceding code, the sort information is persisted because it is stored in a slot in the page’s ViewState collection. The current sort expression, in fact, is yet another piece of information that the DataGrid control does not automatically persist in the control’s view state. If you need it to be persistent, you must do the work yourself. Figure 2-11 shows sorting in action. The full source code for the SortColumns.aspx application is available on the companion CD.

Figure 2-11
A DataGrid control with sortable columns.

The DataGrid control in Figure 2-11 shows rows sorted by the lastname field, but you can’t figure that out easily just by looking at the grid, even with such simple data. You would probably want to modify the DataGrid control’s user interface to indicate the column used for sorting.

Auto-Reverse Sorting

The typical way to identify the sorted column is by means of a little glyph in the header. This glyph also reflects the direction of the sort (ascending or descending). To add such a glyph to the column header, you again need to use our old acquaintance ItemCreated and look up the Header item type. A marker that quickly identifies the sort column makes especially good sense when you implement an auto-reverse sort. In an auto-reverse sort, the column reverses the direction of the data when the user clicks the marker twice in sequence. In this situation, persisting the current sort expression across multiple page requests is vital. To implement the auto-reverse sorting feature, the SortCommand handler changes slightly, as follows:

void SortCommand(Object sender, DataGridSortCommandEventArgs e)
{
    // Caches the current information
    String strSortBy = (String) ViewState["SortExpression"];
    String strSortAscending = (String) ViewState["SortAscending"];

    // Sets the new sort expression
    ViewState["SortExpression"] = e.SortExpression;

    // If you click on the sorted column, the order reverses
    if (e.SortExpression == strSortBy)
        ViewState["SortAscending"] = (strSortAscending=="yes"
            ? "no" :"yes");
    else
        // Defaults to ascending order
        ViewState["SortAscending"] = "yes";

    UpdateDataView();
}

The DataGrid control now stores two pieces of information in ViewState: the sort expression and a yes/no flag that indicates whether or not the direction is ascending. When the sort expression of the column just clicked matches the current sort expression, the same column has been clicked twice consecutively, and the order reverses. Note that this procedure has not performed the actual sorting. The ViewState collection has just been holding up-to-date information about how to sort data.

Sorting would not be possible without the help of the ADO.NET DataView class. The DataView class is a view class built on top of a DataTable class. It enables filtering and sorting. You can obtain a default, unfiltered, and unsorted view object from any DataTable object simply by accessing the DefaultView property. Once you have a DataView object, you just set the Sort property, assign it to the DataGrid control, and rebind.

void UpdateDataView()
{
    DataSet ds = (DataSet) Session["MyDataSet"];
    DataView dv = ds.Tables["MyList"].DefaultView;

    // Apply sort information to the view
    dv.Sort = (String) ViewState["SortExpression"];
    if (ViewState["SortAscending"] == "no")
        dv.Sort += " DESC";

    // Rebind data
    grid.DataSource = dv;
    grid.DataBind();
}

The Sort property is set with the sort expression taken from ViewState. Then the sort expression is appended with DESC if the required order is descending. By default, the DataView class sorts in ascending order.

note | If you don’t use the DataView object to sort, both the sort expression and the use of DESC and ASC can be radically different, depending on the query syntax of the database you use for sorting. Although I don’t wholeheartedly recommend requerying the database directly for performance reasons, using it to sort data is still a valid option.

Notice that in our code example, I used the Session object to persist on the Web server the DataGrid control’s data source. Later in the chapter, I’ll review the various options available for data persistence.

Marking the Sort Column

To add a glyph near the column caption, you handle the ItemCreated event and look for an element type of ListItemType.Header. The ItemCreated event fires when the grid heading has been completely set up. To locate the column to mark, you have no other choice but to loop through the Columns collection.

if (elemType == ListItemType.Header)
{
    String strSortBy = (String) ViewState["SortExpression"];
    String strSortAscending = (String) ViewState["SortAscending"];
    String strOrder = (strSortAscending=="yes" ? " 5" : " 6");

    for (int i=0; i<grid.Columns.Count; i++)
    {
        // Draw the glyph to reflect sorting
        if (strSortBy == grid.Columns[i].SortExpression)
        {
            TableCell cell = e.Item.Cells[i];
            Label lblSorted = new Label();
            lblSorted.Font.Name = "webdings";
            lblSorted.Font.Size = FontUnit.XSmall;
            lblSorted.Text = strOrder;
            cell.Controls.Add(lblSorted);
        }
    }
}

The column to mark is the column whose SortExpression matches the current sort expression. When the column is found, you create a new label control, set its font to Webdings, select the font size and text (usually the fifth and the sixth characters), and finally add the label to the table cell that hosts the column heading. Figure 2-12 shows the results. The full source code for the AutoReverse.aspx application is available on the companion CD.

Figure 2-12
A DataGrid control with the auto-reverse sort feature.

Despite appearances, the code in AutoReverse.aspx has severe drawbacks, not the least of which is that it assumes you never use ASC in the sort expression to indicate ascending order. I will explain what else is wrong with the code in the next section.

Sorting Multiple Fields

Auto-reverse sorting, at least as we’ve implemented it so far, poses a serious issue when you use it with expressions that involve multiple columns. Suppose that in a database of products you have a column sorted by unitprice, productname. When the user reverses the direction, the user expects to sort by this:

unitprice DESC, productname DESC

The grid does sort the data correctly in ascending mode, but when the user reverses the order, the sort expression becomes rather anomalous and clearly wrong:

unitprice, productname DESC

The incriminating code is located in the UpdateDataView subroutine and is as follows:

if (ViewState["SortAscending"] == "no")
    dv.Sort += " DESC";

The code clearly assumes that the sort expression is composed of exactly one field, even without a trailing ASC sort order. Let’s see how to fix it to fully support both auto-reverse and any valid sort expression with as many fields and sort orderings as you need.

Auto-Reverse Sorting for Multiple Columns

To work around the insidious bug I just mentioned, you need to step back from the details of sorting and look at the bigger picture. In particular, you need to keep separate the fields to sort by and their respective directions. A simple string comparison between the clicked columns and the current sort expression is insufficient to determine whether the order has to be reversed. Due to the sorting syntax supported by DataView objects and the majority of database systems, the sort expression must have direction information embedded. On the other hand, auto-reverse just means that if the fields involved in the sorting are the same, you change their order. This clearly implies a separation between fields and directions.

When the SortCommand event occurs, you process the sort information, taking into account auto-reverse sorting. The sort expression is composed from two comma-separated strings that are both stored in ViewState. The first string contains the names of the columns involved in the sorting. In the simplest case, this string contains a single field name, as in the preceding example. The second string contains, for each column, the sort order direction. The ASC sort order is always used whether or not you specified it in the sort expression. You modify your SortCommand event handler as follows:

void SortCommand(Object s, DataGridSortCommandEventArgs e)
{
    // Processes the sort expression. Determines whether auto-reverse is
    // needed and stores in ViewState two comma-separated strings. One
    // contains the names of the involved columns and one contains – in the
    // same order – the respective sort order directions.
    ProcessSortExpression(e.SortExpression);

    // Refreshes the view
    UpdateDataView();
}

The ProcessSortExpression routine performs boilerplate code that stores in ViewState a comma-separated string with the names of the fields involved in the sort, and a comma-separated string with the order required for each field by the current state of the grid (auto-reverse). The former string uses a slot named SortingFields, and the latter string uses a slot named SortingOrders. (Of course, these names are totally arbitrary.) ProcessSortExpression also fills a third slot, named ColumnSortExpression, that contains the original sort expression associated with the clicked column, that is, e.SortExpression. This information will be useful later when you add a glyph to the column’s header.

When the grid is going to be refreshed, you process the information stored in ViewState and end up with a tailor-made string that the DataView object can easily understand.

void UpdateDataView()
{
    DataSet ds = (DataSet) Session["MyDataSet"];
    DataView dv = ds.Tables["MyList"].DefaultView;

    // Apply sort information to the view
    dv.Sort = PrepareSortExpression();

    // Rebind data
    grid.DataSource = dv;
    grid.DataBind();
}

The PrepareSortExpression routine retrieves from ViewState the information about sorting fields and orders, and it merges that information in a string that the DataView object knows how to process. The following is a sample string:

unitprice asc, productname desc

This final sort expression is also explicitly stored in ViewState in light of another future enhancement that I’ll discuss in a moment. When you add the glyph, you recognize the column that is used to sort the DataGrid control by comparing the column’s sort expression to the string stored in ViewState ["ColumnSortExpression"]. Next, you get the right WebDing character (indicating ascending or descending order) based on the order requested for the primary column in the sort expression. If a column has to be sorted by unitprice, productname, you might want to consider the order of only the first primary column to decide which glyph to display. Figure 2-13 shows that multicolumn, auto-reverse sorting now is working just fine.

Figure 2-13
A DataGrid control that supports multicolumn, auto-reverse sorting.
Showing Sorting Information

Showing a little bitmap in the column heading helps users understand how the column is sorted, but users of more sophisticated applications might find this contrivance insufficient. When you need to reveal more information, you can provide a tooltip that shows the column’s sort expression. In this tooltip, you can also distinguish between the currently sorted columns and all the other sortable ones. Let’s see how to accomplish this, bearing in mind that you can use the next trick to show any type of information, not just information about sorting.

You add a tooltip to an ASP.NET control by setting its ToolTip property. The tooltip cannot be added to the column as a whole; instead it must be defined only for the link button used to render the caption. To get your hands on this control, once again you have to resort to the ItemCreated event handler.

if (elemType == ListItemType.Header)
{
    for (int i=0; i<grid.Columns.Count; i++)
    {
        // Grab the cell with the link button
        TableCell cell = e.Item.Cells[i];

        // Add a tooltip with the sort expression
        if (grid.Columns[i].SortExpression != "")
            cell.ToolTip = "Sort by: " + grid.Columns[i].SortExpression;

            // Draw the glyph to reflect sorting
            String strSortBy = (String)ViewState["ColumnSortExpression"];
            if (strSortBy == grid.Columns[i].SortExpression)
            {
                cell.ToolTip = "Sorted by: " +
                    ViewState["ColumnSortExpression"]ToString();

                // Code that draws the glyph ...
            }
    }
}

Figure 2-14 shows the final result. The full source code for the MfieldSorting.aspx application is available on the companion CD.

Figure 2-14
Tooltips let users know about the sort expression of each column.

When you use custom pagination, you are responsible for providing all the records that fit in every page. This task is even more complex if you have to provide sorting too.


DataGrid Controls and Data Persistence

ASP.NET applications are modeled after the Web Forms model, which is the typical Visual Basic form-based, client/server model of interaction delivered on the Web. The ASP.NET run time shields you from the structural differences between the two models. It takes care of serializing and deserializing the state of the form. Any server-side processing takes place in an environment that maintains the state of the client browser. When you use DataGrid controls, you need to retrieve and process the control’s data source every time you execute postback code on the server. As mentioned earlier in the chapter, the DataGrid control does not cache its data source in the control’s view state. This makes sense because the data source can be too large to be effectively transferred back and forth between the Web server and the browser. Bear in mind that all the information Web controls store in their ViewState properties makes the size of the HTML page larger. This information is then posted back from the browser to the server when a postback event occurs. Basically, you have two options for repopulating the DataGrid control’s DataSource property:

  • Cache the data source, as a whole or in part, on the Web server and read it back

  • Reload all the records from the physical data storage (typically, a database)

When you cache the data source, data is retrieved from storage only once, stored in a cache, and subsequent postback events read from that cache. You can use in-memory global objects such as Session and Cache, or alternatively you can use XML files stored on the Web server or another accessible share.

If you plan to reload the records each time a postback event occurs, consider that using a DataReader class is more efficient than using a data adapter. Don’t forget to close both the reader and the connection as soon as possible, as I previously explained.

Scalability? What Was That?

The way in which you decide to retrieve the grid’s data source might seriously affect the overall scalability of the application. However, years of real-world experience should have taught you scalability is affected by many factors. Scalability is precious like a diamond and, like a diamond, can have many facets that contribute to its value. Scalability can be described as the system’s ability to maintain, or improve, its responsiveness as the number of clients grows. The theory of queuing states that a queue forms itself when the frequency of the requests tend to overtake the system’s response time. For the sake of the application, you cannot take measures to reduce the user requests, but you can try to lessen the response time.

You normally adjust the scalability level of Web applications by mixing together, in application-specific doses, heterogeneous and even contradictory measures such as the following:

  • Limiting the number of calls to the database

  • Delegating as many tasks as possible to the database

  • Limiting the occupation of the Web server’s memory

  • Using relatively simple and stateless components

Writing fast and optimized code would also certainly help a lot! For a good result, each ingredient is extremely important and, with the right doses, even otherwise lethal ingredients are acceptable. Limiting the number of calls to the database implies that you are not delegating data processing to it and are therefore placing a load on the Web server. Limiting the server memory occupation implies that you don’t cache data and, consequently, that you have to call the database whenever data is required. And the list could go on and on. Scalability is a sort of philosopher’s stone, and programmers, like medieval alchemists, can only try remedies again and again, learning from their errors and fine-tuning their skills.

Let’s review the most common options you have for persisting the DataGrid control’s content on the server.

Using the Session Object

In ASP and ASP.NET, the Session object is a global repository for data and objects that belong to the session. The visibility of the data is limited to the pages invoked within the session. Using the Session object is critical any way you look at it. It guarantees quick and prompt access to data and returns ready-to-use objects, but all the session data is duplicated per each active session and connected user. In general, you should be extremely careful when it comes to using the Session object in production code. But my advice about being careful does not mean that Microsoft would have been better off dropping the Session object. Using Session is still the fastest way to access session-specific data, but try to keep the amount of data stored in Session under strict control.

The ASP.NET Session object has two major advantages over its ASP counterpart. First, any .NET object can now be safely stored in a Session slot. This overcomes the thread-affinity problem you might have experienced with ASP and Visual Basic COM components. Second, the Session object is the programming interface for a module—the Session Manager—that can work in-process and out-of-process, and can even rely on SQL Server for data storage. This is probably the best reason to opt for Session: it now works well with Web farm architectures.

Using the Cache Object

The majority of ASP.NET applications will take advantage of the Cache object for all of their caching needs. The Cache object is new to ASP.NET and provides unique and powerful features. It is a global, thread-safe object that does not store information on a per-session basis. In addition, the Cache object is designed to ensure it does not tax the server’s memory. If low memory does become an issue, the Cache object will automatically purge its least recently used items based on a priority defined by the developer. Like the familiar Application object, the Cache object does not share its state across the machines of a Web farm. In terms of the programming interface, using the Cache object is not at all different from using Session or Application objects.

What really differentiates the Cache object from Application is its ability to automatically remove least-used items when the memory is low. To help the built-in scavenging routines of the Cache object, you can assign some of your cache items with a priority and even a decay factor that lowers the priority of the items that have limited use. When working with the Cache object, you should never assume that an item is there when you need it. Always be ready to handle exceptions caused by null or invalid values. If your application needs to be notified of an item’s removal, then register for the OnRemove event. You can do this by creating an instance of the CacheItemRemovedCallback delegate and passing it to the Cache object’s Insert or Add method.

In addition, some of the items stored in the Cache object can be bound to the timestamp of one or more files or other cached items. When any of these linked resources change, the cached item becomes obsolete and is removed from the cache. By using a try/catch block you can catch the invalid item and reload it from persistent storage.

Aside from Web farms, in resource-constrained scenarios you might want to consider alternatives to Cache. Even when you have large data sets to store on a per-session basis, storing and reloading them from memory will be much faster than any other approach. With many users connected at the same time, each storing large blocks of data, you might want to consider helping the Cache object do its job better. An application-specific, layered caching system built around the Cache object is an option to evaluate. In this case, hot and sensitive data will go into the Cache and be efficiently managed by the ASP.NET run time. The rest of the data could be cached in a slower, but memory-free, storage such as session-specific XML files.

Using XML Files

ADO.NET classes, and the DataSet class in particular, are tightly integrated with XML. This means that saving the content of DataSet to a disk-based XML document is a snap. Also, rebuilding a living instance of a DataSet object from a persistent XML file is not particularly hard. If you don’t want to re-read the DataGrid control’s data out of a database every time it’s needed and if you don’t want to load the data only once and leave it stored to Session or Cache, persisting the data to XML files is an interesting option to consider. The more memory the Web server has, the more quickly it can serve new requests.

Earlier in the chapter, I showed a page that based persistence of the DataGrid control’s content on the Session object. Let’s rewrite the code to use XML files instead.

<script runat="server">
void Page_Load(Object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        DataFromSourceToMemory("MyDataSet");
        UpdateDataView();
    }
}

void DataFromSourceToMemory(String strDataSessionName)
{
    // Gets rows from the data source
    DataSet oDS = PhysicalDataRead();

    // Stores it in the session cache
    SerializeDataSource(oDS);
}

When the page is first loaded, the control calls DataFromSourceToMemory which, in turn, performs a physical read from the storage medium and then, instead of storing information to Session, calls a new routine named SerializeDataSource.

void SerializeDataSource(DataSet ds)
{
    String strFile = Server.MapPath(Session.SessionID + ".xml");
    XmlTextWriter xtw = new XmlTextWriter(strFile, null);
    ds.WriteXml(xtw);
    xtw.Close();
}

The code creates a new XML file whose name is based on the session ID. The file is populated by using the DataSet object’s WriteXml method. Reading information back is easy, too. The task is accomplished by the DeserializeDataSource function.

private DataSet DeserializeDataSource()
{
    String strFile = Server.MapPath(Session.SessionID + ".xml");
    XmlTextReader xtr = new XmlTextReader(strFile);
    DataSet ds = new DataSet();
    ds.ReadXml(xtr);
    xtr.Close();
    return ds;
}

Instead of restoring DataSet from Session, your code would use DeserializeDataSource.

Using Data Readers and Adapters

Another, more radical choice for data persistence is re-reading the data from the database whenever needed. This choice is good when you have to deal with a very large data set and plan to implement a custom pagination service. Now might be a good time to refresh your memory about the differences between ADO.NET data adapters and data readers. You should use adapters when you want to work on your data in a disconnected manner. You should use readers if you don’t plan to persist the retrieved data.

A data adapter populates a DataSet structure, which provides the main tools you need to work in the absence of a database. It provides for filters, sorting, indexing, searching, cloning, and even in-memory relations. Given its arsenal of programming tools, the DataSet class is not optimized for a simple read of a few records. The DataSet class can be persisted, either on disk or in memory (it does not matter), and survive across multiple page requests.

But if your goal is to read only the records that fill up one grid page, you are much better off using any of the data reader classes. Not only does ADO.NET provide a data reader for each supported .NET–managed data provider, but every .NET data provider is expected to expose a data adapter and a data reader class. So you use SqlDataReader when your target database is SQL Server version 7 and later versions. You use OleDbDataReader when your target is an OLE DB provider. You use OdbcDataReader when you are working against the ODBC .NET–managed data provider. The data reader works like a firehose—it provides an open channel through which read-only records flow, and the client reads them, one after the next, in a forward direction.

The Paradox of Pagination

Let’s review what happens when you decide to retrieve records to populate a DataGrid control at every postback event using a data adapter object. You typically use a CreateDataSource function that returns a DataSet object, and you assign the result to the grid and rebind. If you don’t cache DataSet in any way, you end up loading all the records available in the data source for each page scrolled. Get the point? Now do you see the paradox? You load, say, 100 records to display only the 10 or fewer that fit in the page real estate. And this happens for each page and for each postback event triggered. You probably architect things this way to exploit the DataGrid control’s built-in pagination but, in doing so, you pay for it without ever using it. The only concrete gain is that the DataGrid control selects for you the 10 or fewer records to display for the current page—and that is at least something.

note | The paradox of pagination described here applies to both data readers and adapters. The only difference is that data readers are more lightweight objects that, if not closed properly, can cause even more serious damage to the scalability of the application.


Conclusion

So what is the moral of the story? Always try to cache the data of the DataGrid control on the server. The Session and Cache objects have been fine-tuned to work well in the most common programming scenarios, such as when dynamically changing information is tossed into and out of shopping carts. In addition, the ASP.NET Session object can work without cookies and support both Web farm and Web garden architectures.

If you don’t want to use in-memory caching, consider caching to a disk. In this case nothing is better than XML, not because it’s cool and trendy, but because it has excellent support from ADO.NET classes. You can use the Session ID to keep each user’s information separated. If you can’t afford caching and applications built according to the data pagination paradox, custom paging is the only way to go.

In this chapter, you received an in-depth exposure to basic DataGrid control issues. You learned how to bind columns and exploit the built-in pagination services. Paging without sorting, and sorting without auto-reverse, is simply inadequate for today’s applications. You now know how to code sorted columns with DataGrid controls. But the saga of the DataGrid control does not end here. In the next chapter, you learn about templates.


Templated Controls

Chapter 3
Templated DataGrid Controls

As you learned in the first two chapters, the DataGrid control has a table-based structure and is flexible enough to let you determine the structure and contents of columns. Specifically in Chapter 2, you saw DataGrid controls in action and learned how to use their basic yet powerful characteristics. You also learned how to bind special columns representing actions and links. In this chapter, I’ll show you how to use HTML templates to take item customization to the next level. You’ll learn how to design the content of a column so that it closely reflects the meaning and the structure of your data.

Templated columns enable you to use a combination of HTML and server controls to design a custom layout for a column. The controls in a templated column can be bound to any combination of fields in the data source. You can group multiple fields in a single expression and even embellish fields with HTML attributes such as bold or italic. Templates are column-specific and cannot be applied to auto-generated columns. If you want more than one column to share the same template, you can either duplicate the code in the ASP.NET page for each column, or save some code by loading the template from an external file.


Binding Templated Columns

A templated column is recognized by the <TemplateColumn> tag. The body of the tag contains from one through four different templates: ItemTemplate, EditItemTemplate, HeaderTemplate, and FooterTemplate. You can tell from these names that the tag lacks a specific template for the alternating item and selected item. Alternating and selected items typically differ from other items only in terms of graphical settings such as background and foreground color and font styles, and such graphical settings can be easily set for any item at the DataGrid control level by using the <AlternatingItemStyle> and <SelectedItemStyle> tags. You will typically not need to change the layout of a column when one of its cells is selected or when it needs to be redrawn with alternating items. But when you do, for this sort of “genetic” manipulation of the grid, nothing works better than hooking the ItemCreated event.

The following code shows how to bind a templated column to a DataGrid control. Notice that a templated column, like any other column type, can have header text as well as a sorting expression. It does not, however, have an explicit data source field to bind to. Among the members of the TemplateColumn class, you will not find any DataField or DataTextField properties.

<asp:TemplateColumn runat="server"
    HeaderText="heading" SortExpression="field">
    <itemtemplate>
        HTML and/or ASP.NET layout code
    </itemtemplate>
</asp:TemplateColumn>

To bind a template column to one or more data fields, you use a data-binding expression and the DataBinder class, which was fully described in Chapter 1. To render a column, you could use a Label control that has the Text property, but you could also choose a drop-down list control (more on this later) or an image, both of which do not have anything like the Text property. As a result, you must always use data-binding expressions to bind data, which provides you unprecedented flexibility, albeit with more verbose code. The following code snippet is valid content for an item template:

<asp:label runat="server" Text='<%#
    DataBinder.Eval(Container.DataItem, "lastname") %>' />

By using DataBinder.Eval, you can access any number of fields in the currently bound data source. In addition, you can combine them in any order to obtain an expression that would otherwise be impossible using a simple bound column or button column. The key advantage of templated columns is that any ASP.NET control can be used to populate the column’s cells. How those controls are filled is completely up to you, providing an advantage over binding to data using only the rigid DataField or DataTextField properties.

ItemTemplate is the property that lets you define the layout and the contents of each cell in the column. Other templates let you define the structure of the column header (HeaderTemplate) and footer (FooterTemplate). You can determine the behavior and appearance of the column when a cell is being edited via the EditItemTemplate template. (I’ll have more to say about in-place column editing inChapter 4.) Table 3-1 summarizes the column template properties supported by the DataGrid Web control and hints for using them.

Table 3-1 Column Template Properties Supported by the DataGrid Control

Template Property

Usage

ItemTemplate

The template for the items in a DataGrid control’s column.

<ItemTemplate>
    <asp:label runat="server"
        text= '<%# ... %>' />
</ItemTemplate>

You can use any combination of HTML and ASP.NET controls to populate the column.

EditItemTemplate

Controls the contents of the item selected for editing in the DataGrid control’s column.

<EditItemTemplate>
    <asp:textbox runat="server"
        text= '<%# ... %>' />
</EditItemTemplate>

HeaderTemplate

Contains information for the heading section.

<HeaderTemplate>
    <asp:label runat="server"
        text= "Header" />
</HeaderTemplate>

If you omit this template, the column header is rendered with a label or with a link if sorting is enabled. When you specify a custom template, you are responsible for providing the user interface tools to enable sorting on the column.

FooterTemplate

Contains information for the footer section of the column. This property value is null by default.

<FooterTemplate>
    <asp:label runat="server" text= "..."  />
</FooterTemplate>

The footer is displayed only if the ShowFooter property of the DataGrid control is set to true.

caution | Unless you need special functionality, you are better off leaving the header template as is. Changing the template is an easy and smooth process as long as you don’t need to establish a direct interaction between the elements in the header and the user. More on this in a moment.

The template properties in Table 3-1 are exposed by the TemplateColumn class as data members of a type that inherits from the ITemplate interface. Template-based properties are declaratively set with plain text in the layout of ASP.NET pages. During page processing, the ASP.NET run time takes care of loading that text into a data member of the proper type.

In addition to template properties, the TemplateColumn class provides a few style properties—ItemStyle, HeaderStyle, and FooterStyle—which you can use to customize the appearance of items in individual columns. You use these properties in the same way you use the properties for the column types discussed in Chapter 2.


Templated Columns in Action

As powerful as it is, a DataGrid control handles data only in terms of columns, and it can display only information that can be expressed by using a data field name. More often than not, these restrictions are too limiting to build a really user-friendly interface. For example, suppose you have three fields to display: first name, last name, and a title of courtesy, such as "Mr.", "Mrs.", or "Dr." The query selects three distinct fields, and your simplest option is to display that data in a DataGrid control that uses three distinct columns. Figure 3-1 shows the results.

Figure 3-1
The DataGrid control must display even simple information in a column format.

Figure 3-1 is clear, but the full name looks unusual spread over three columns, and the layout does not transmit that feeling of informality that makes users—especially nonexpert users—feel comfortable with the application. (You’ll see a marked improvement in Figure 3-2, which I discuss later.) A better approach would be tying together the three fields in a single string, such as "Ms. Davolio, Nancy".

You can ask the database to return a string that is already in the desired format, but such a request typically results in increased network traffic and more work for the database engine. It can also leave you with less useful information at hand. For example, if you return only the concatenated string, you lose any ready-to-use information such as the first and last names and, among other options, your ability to sort by these fields.

In Chapter 1, I discussed the pros and the cons of the database returning data fields in a format that is ready to be displayed. That discussion focused on the CheckBoxList control, but the pros and cons are really the same for the DataGrid control. You obtain preformatted data more effectively by building precalculated columns that are created in memory in a DataTable object that you then associate with a grid.

When you have to create expressions based on data fields, using templates gives you more flexibility than any other solution. Unfortunately, templated columns do not come free. The TemplateColumn class does have parsing costs and occupies more memory than, say, the BoundColumn class. My advice is to avoid using templated columns for relatively simple tasks that you can accomplish in other ways. For example, if you have a cached DataTable object that already has all fields in a displayable format, the page will regenerate even faster if you use BoundColumn only. Let’s review the code for creating and using templated columns.

Concatenating Data Fields

The following code is template code that mimics the standard behavior of bound columns:

<asp:TemplateColumn runat="server" HeaderText="Last Name">
    <itemtemplate>
        <%# DataBinder.Eval(Container.DataItem, "lastname") %>
    </itemtemplate>
</asp:TemplateColumn>

You can also use a Label control and fill its Text property with any string, data bound or not. The following code creates a column like the Employee Name column in Figure 3-2. The full source code for the TemplateColumns.aspx application is available on the companion CD.

<asp:TemplateColumn runat="server" HeaderText="Employee Name">
    <itemtemplate>
        <asp:label runat="server" Text='<%#
            DataBinder.Eval(Container.DataItem, "TitleOfCourtesy") +
            "<b> " +
            DataBinder.Eval(Container.DataItem, "LastName") +
            "</b>" + ", " +
            DataBinder.Eval(Container.DataItem, "FirstName") %>' />
    </itemtemplate>
</asp:TemplateColumn>
Figure 3-2
The Employee Name column is created by combining multiple fields.

The three fields—TitleOfCourtesy, LastName, and FirstName—have been merged into a single string containing some additional HTML attributes. Compared with Figure 3-1, you can see that the resulting single column displays logically related information in a more readable, user-friendly way.

tip | Every page element in ASP.NET evaluates to a Web control, but this does not mean that all controls work in the same way and have the same impact on overall page performance. The LiteralControl control is the most lightweight of all controls because it does not require any special server-side processing. Use literals instead of labels whenever they happen to provide you with the same capabilities. In particular, you use them to render static text that you never update across postback events.
Sorting Templated Columns

Sorting templated columns is similar to sorting any other type of column. You just set the SortExpression property to a comma-separated list of fields and make sure the DataGrid control is properly configured for sorting. (See Chapter 2 for more information on this.)

Of course, you can sort the template column by one or more data source fields, so in the case of Figure 3-2, the Employee Name column can be sorted by any combination of columns present in the grid’s data source but not according to the plain text displayed. If you need to sort the text as it appears in the column, you need to have an in-memory column with that content. Figure 3-3 shows the Employee Name column sorted by lastname. The full source code for the SortableTemplateColumns.aspx application is available on the companion CD.

Figure 3-3
The Employee Name column sorted by the lastname field.
caution | If you omit setting the HeaderText attribute for a column or set it to a blank string, sorting will not be activated on that column. This could become a rather significant issue if you plan to use templated headers.
Grouping Columns Under a Single Header

Figure 3-3 demonstrates a little bug in the output of the grid: the last names are not aligned, resulting in a display problem. Nothing is wrong with the code—this is the natural consequence of the string format. Is there a way to ensure that, within the same column, a given data field begins at an absolute position? Using a single-row table to wrap up all the data fields just doesn’t work.

<itemtemplate>
    <table><tr>
        <td>
            <asp:label runat="server" Text='<%#
                DataBinder.Eval(Container.DataItem, "TitleOfCourtesy") %>'
            /> </td>
        <td>
            <asp:label runat="server" Text='<%#
                "<b>" +
                DataBinder.Eval(Container.DataItem, "lastname") %>' +
                "</b>, " +
                DataBinder.Eval(Container.DataItem, "firstname") %>'
            /> </td>
    </tr></table>
</itemtemplate>

The alignment problem arises between rows, and only a table that encompasses all the cells in the column can solve it effectively. Using child tables poses the subsequent problem of bubbling down font and color settings. A much better approach is using distinct columns placed under the umbrella of a common header. Figure 3-4 demonstrates what I mean.

Figure 3-4
A DataGrid control that groups two columns under the same heading section.

To group even more columns under the same header, you need to manipulate the grid structure—something that can be accomplished only by handling the ItemCreated event. Here’s what the grid’s collection of columns looks like in code:

<asp:DataGrid id="grid" runat="server"

    OnItemCreated="ItemCreated">

<columns>
    <asp:BoundColumn runat="server" DataField="employeeid" HeaderText="ID">
        <itemstyle backcolor="lightblue" font-bold="true" />
    </asp:BoundColumn>
    <asp:BoundColumn runat="server" DataField="titleofcourtesy" />
    <asp:TemplateColumn runat="server" HeaderText="Employee Name">
        <itemtemplate>
            <asp:label runat="server" Text='<%# "<b> " +
                DataBinder.Eval(Container.DataItem, "LastName") +
                "</b>" + ", " +
                DataBinder.Eval(Container.DataItem, "FirstName") %>'
                />
        </itemtemplate>
    </asp:TemplateColumn>

</columns>
</asp:DataGrid>

The columns to group are the BoundColumn column that is currently linked to the titleofcourtesy field, and the templated column whose caption is Employee Name. The BoundColumn column has an empty caption and occupies the second position in the collection.

The ItemCreated event is fired when the control is done preparing the given item. You check the item type by using the ItemType property of the event data structure, as the following code shows. (The full source code for the GroupingColumns.aspx application is available on the companion CD.)

public void ItemCreated(Object sender, DataGridItemEventArgs e)
{
    ListItemType lit = e.Item.ItemType;
    if (lit == ListItemType.Header)
    {
        // Each cell corresponds to a column header
        TableCell cell;

        // One cell must be dropped because we have one too many.
        // It can be the second or the third, one of the two we
        // want to incorporate.
        cell = (TableCell) e.Item.Cells[1];
        e.Item.Cells.Remove(cell);

        // The second heading (the first of the two) spans to
        // cover two columns
        cell = (TableCell) e.Item.Cells[1];
        cell.ColumnSpan = 2;
    }
}

After you make sure the event fired because the header of the DataGrid control is being processed, you access the cells that form the header. The cells are contained in the e.Item.Cells collection. Your next goal is to fuse the headers of columns 2 and 3. To do this, you must remove the extra cell from the table row and then create a header cell that spans the other two columns. You can delete the header of either column 2 or column 3. (Bear in mind that the Cells collection is 0-based, just like any other collection in the .NET Framework.) So you might want to drop the column without a caption to save an extra line of code that you would have needed to restore the caption for the column on the left—in this case, column 2. Once the extra cell has been dropped from the table row, you take what is now column 2 and span it over two table columns by using the ColumnSpan property.

Adjusting Column Margins

To make the page more readable, you might want to add a few empty pixels to both horizontal sides of the cell text. The DataGrid control provides the CellPadding property to handle this adjustment. The CellPadding property, which maps to the cellpadding attribute of the HTML <table> tag, has two significant effects, however: it indiscriminately applies to all cells in the grid, and it provides both horizontal and vertical padding.

Other cell formatting options include CellSpacing, which indicates the space between columns, and GridLines, which lets you specify whether you want horizontal lines or vertical lines, or both. By combining the values of properties such as CellPadding, CellSpacing, and GridLines, you end up with a nice report table in which the cell text is easily readable. Figure 3-5 shows the result of setting CellPadding to 5, CellSpacing to 0, and leaving GridLines to its default value of Both.

Figure 3-5
A good mix of values for cell padding, cell spacing, and grid lines improves readability.

Having empty pixels between columns is helpful, especially when you have right-aligned columns. If you set CellSpacing to a nonzero value, the result is worse, as Figure 3-6 shows.

Figure 3-6
Setting CellSpacing to a nonzero value gives you a more cluttered effect.

The root of the problem is that both the CellSpacing and GridLines properties apply to all cells horizontally and vertically. The ideal solution is to set both properties to 0 and pad as needed at the cell level by using the various margin CSS styles, as shown in this example:

style="margin-left:5;margin-right:5;"

The preceding code sets the horizontal distance between two side-by-side cells to 5 pixels. As you know, the DataGrid control is a table made of <tr> and <td> elements. These tags are not affected by the value of the margin CSS style. The margin style produces an effect only if applied to the content of the cell. This behavior is hard coded in the CSS definition; the margin is calculated from the parent control. As a result, if you want the cell text drawn 5 pixels from the left border, you have to wrap the cell text in an HTML tag and set the margin style attributes for the tag.

There is a trick to accomplish this in a declarative way for all columns without resorting to templates. When you use templates, padding text is trivial because the TemplateColumn class requires you to define the exact layout of the cells, as the following code shows:

<asp:TemplateColumn runat="server" HeaderText="...">
<itemstyle ... />
<itemtemplate>
    <span style="margin-left:5;margin-right:5;">
    <%# DataBinder.Eval(...) %>
    </span>
</itemtemplate>
</asp:TemplateColumn>

Notice that using a <span> tag is more efficient than using a Label control because the tag evaluates to a LiteralControl control and therefore does not require any server-side processing. For other types of columns, the key to padding the text effortlessly is to use the DataFormatString property:

<asp:BoundColumn runat="server"
    DataField="employeeid" HeaderText="ID"
    DataFormatString="<span style=\"margin-left:5;\">{0}</span>">
</asp:BoundColumn>

The default cell text is represented by the {0} placeholder, which is wrapped by a <span> tag that has appropriate margin values set. The DataFormatString property is not available (and not needed) for templated columns.

Customizing Column Headers

A templated column allows you to define a custom layout for the header and the footer sections. Changing the layout of the header can be problematic if you need to sort that column by an expression. The sorting mechanism is triggered by a HyperLink control that the DataGrid control automatically embeds in the column heading. The href attribute rendered by this HyperLink control generates a postback event when the user clicks the element. The target of the link is a piece of client-side JavaScript code whose internals have not been fully documented yet.

<a href="javascript:__doPostBack(...)" style="color:White;">
    Caption of the column goes here
</a>

What this code does, at the highest level of abstraction, is clear: it posts back to the Web server the form contents of the ASP.NET page. In doing so, it passes to the HTTP runtime some extra information—the parameters of __doPostBack—which is responsible for processing the page. At the time this book is being written, you cannot safely make assumptions about the structure and the role of these parameters.

In summary, if you want to change the template of a column heading, by all means do so as long as you don’t need sorting capabilities. If you can’t just eliminate the sorting, use the ItemCreated event to add extra controls to the header. The following code dynamically adds a drop-down list to the header of a template column, allowing you to choose the expression to sort by. Figure 3-7 shows the final result.

public void ItemCreated(Object sender, DataGridItemEventArgs e)
{
    ListItemType lit = e.Item.ItemType;
    if (lit == ListItemType.Header)
    {
        // Create and fill a drop-down list control
        DropDownList dd = new DropDownList();
        dd.ID = "ddSort";
        ListItem li1, li2, li3;

        // ListItem constructor takes Text and Value for the item
        li1 = new ListItem("Title of courtesy", "titleofcourtesy");
        dd.Items.Add(li1);

        li2 = new ListItem("Last Name", "lastname");
        dd.Items.Add(li2);

        li3 = new ListItem("First Name", "firstname");
        dd.Items.Add(li3);

        // Select the item, if any, that was selected last time
        dd.SelectedIndex = Convert.ToInt32(grid.Attributes["FieldIndex"]);

        // Add the drop-down list to the header of the 2nd column
        TableCell cell = (TableCell) e.Item.Controls[1];
        cell.Controls.Add(dd);
    }
}
Figure 3-7
A custom layout for a column header. You pick up a sort expression from the drop-down list and click Sort By to sort.

Customizing the header is useful when you have templated columns that group together several fields. The preceding code creates a dynamic drop-down list with the available sort expressions. Next it retrieves the currently selected expression when users click the header’s link. The header text of the template column must be set to a non-empty string so that the standard infrastructure for sorting can be built to work properly.

<asp:TemplateColumn runat="server"
    HeaderText="Sort by" SortExpression="*">

Notice the unusual value assigned to the SortExpression property. It plays a critical role in that it allows the sort command handler to recognize that the user clicked a column requiring special treatment for sorting. The SortExpression attribute, in fact, is the only element you have for recognizing the clicked column.

public void SortCommand(Object sender, DataGridSortCommandEventArgs e)
{
    // Code that retrieves the grid's data source GOES HERE

    if (e.SortExpression != "*")
        dv.Sort = e.SortExpression;
    else
    {
        // Retrieves the drop-down list control through its ID
        DataGridItem dgi = (DataGridItem) e.CommandSource;
        DropDownList dd = (DropDownList) dgi.FindControl("ddSort");

        // Retrieves the sorting expression from the list
        dv.Sort = dd.SelectedItem.Value;

        // Persists the currently selected drop-down item
        ViewState["FieldIndex"] = dd.SelectedIndex.ToString();
    }

    // Refreshes the grid
    grid.DataBind();
}

The expression you assign to SortExpression does not matter as long as it allows you to identify the column.


Creating Templates Dynamically

Typically, the layout code of templated columns is defined at design time, but you might face situations in which using predefined templates is not the optimal solution and you need a dynamic template. For example, if you know in advance that a lot of changes must be applied at run time via events such as ItemCreated and ItemDataBound, there is no reason to define a static template, forcing the control to support a double effort: processing the template first and the changes next. Also, when users can change among different views of the same data, a dynamic template is preferable. Whatever your reason for adding templated columns dynamically, you face the problem of how to create a template programmatically. In such a situation, using an external ASCX file can help.

Loading Templates from Files

A template for a column property is a class that implements the ITemplate interface. An instance of such an object can be created using the LoadTemplate method of the Page class. LoadTemplate takes only one argument: the name of the text file that describes the template. The file must have an ASCX extension—ASCX is the typical extension of user control files, formerly known as pagelets. You create a file-based template column using the following code:

TemplateColumn tc = new TemplateColumn();
tc.ItemTemplate = Page.LoadTemplate("template.ascx");

The template file can be written in any .NET language and not necessarily in the language of the page. The Page.LoadTemplate method can be used to load the layout code for any template property of the column, including EditItemTemplate and HeaderTemplate.

note | There is no way to load a column template from an external file at design time. The <ItemTemplate> ASP.NET tag does not support an src attribute or an attribute similar to src.

The ASCX user control you use to populate the templates of a column defines the HTML and the ASP.NET controls you want to employ. The following code shows the ASCX file that concatenates TitleOfCourtesy, LastName, and FirstName:

<%@ Language="C#" %>
<%#
    DataBinder.Eval(((DataGridItem) Container).DataItem, "TitleOfCourtesy")
    + " " + "<b>" +
    DataBinder.Eval(((DataGridItem) Container).DataItem, "LastName") +
    "</b>, " +
    DataBinder.Eval(((DataGridItem) Container).DataItem, "FirstName")
%>

The user control must indicate its language, even if the language is the same one being used in the hosting page. The following code shows the same user control written in Visual Basic .NET. You can use this code interchangeably with the previous code.

<%@ Language="VB" %>
<%#
    DataBinder.Eval( _
        CType(Container, DataGridItem).DataItem, "TitleOfCourtesy") + _
        "<b>" + _
    DataBinder.Eval( _
        CType(Container, DataGridItem).DataItem, "LastName") + _
        "</b>, " + _
    DataBinder.Eval( _
        CType(Container, DataGridItem).DataItem, "FirstName")
%>
Managing Multiple Views for a Column

The ability to dynamically load templates from disk can be exploited to build an interesting application that lets users change the view of a templated column. Figure 3-8 shows what I mean.

Figure 3-8
This application uses dynamically loaded templates to allow users to change the column view.

The DataGrid control in Figure 3-8 shows a footer that has been dynamically modified to span all columns. The footer contains a drop-down list with the available views for the templated column. The user selects the desired view and then clicks the Apply link button to enable it.

Setting Up the Footer

You can place the controls for selecting the view mode anywhere on the page. A good place is in the footer of the templated column. In this case, you can use the <FooterTemplate> tag:

<FooterTemplate>
    <b>View:</b>
    <asp:dropdownlist runat="server" id="ddViews" />
    <asp:linkbutton runat="server"
        Text="Apply" CommandName="ApplyView" />
<FooterTemplate>

By default, the DataGrid control’s footer is disabled, and you turn it on by setting the ShowFooter attribute to true. Usually the footer is a blank row added at the bottom of the grid’s current page and needs a different structure from the rest of the rows. Let’s say you want the footer to span all the columns in the grid to become a table row with a single cell. Once again you have to resort to the ItemCreated event to accomplish this kind of structural manipulation of the grid items. The following code shows the strategy for creating the footer shown in Figure 3-9.

if (lit == ListItemType.Footer)
{
    // Remove 1st and 3rd columns in the original schema
    e.Item.Cells.RemoveAt(0);
    e.Item.Cells.RemoveAt(1);
    e.Item.Cells[0].ColumnSpan = 3;

    // Populate the drop-down list with available views
    // Each view corresponds to an ASCX file in the current folder
    DropDownList ddViews = (DropDownList) e.Item.FindControl("ddViews");
    ListItem l;
    l = new ListItem("Ms. Surname, Name", "courtesylastfirst.ascx");
    ddViews.Items.Add(l);
    l = new ListItem("Name Surname - (Ms.)", "firstlastcourtesy.ascx");
    ddViews.Items.Add(l);

    // Select the previously selected element, if any
    // Need to preserve list state across postback events
    ddViews.SelectedIndex = Convert.ToInt32(ViewState["ViewIndex"]);
}
Figure 3-9
In this grid, you use the ItemCreated event to create a footer that becomes a table row with a single cell.
Applying the View

The view in Figure 3-9 is enabled when the user clicks the Apply link button. This button has been assigned a CommandName property. Any clickable element within the body of the DataGrid control raises an ItemCommand event when the user clicks it. This event is handled in much the same way it is for the DataList control, which I covered in Chapter 1.

When you handle the ItemCommand event, you distinguish among the various elements that might have fired the event by using the CommandName attribute. In the example of Figure 3-9, I defined a link button with a command name of ApplyView. To handle the button click, you need code such as the following:

public void ItemCommand(Object sender, DataGridCommandEventArgs e)
{
    if (e.CommandName == "ApplyView")
    {
        DropDownList ddViews = (DropDownList)e.Item.FindControl("ddViews");
        String strFile = ddViews.SelectedItem.Value;

        ViewState["CurrentViewFile"] = strFile;
        ViewState["ViewIndex"] = ddViews.SelectedIndex.ToString();
        UpdateView();
    }
}

The ItemCommand handler is registered through the OnItemCommand attribute of the <asp:datagrid> tag.

<asp:datagrid id="grid" runat="server"

    OnItemCommand="ItemCommand"

/>

You first retrieve the instance of the drop-down list control. The control ddViews is not accessible at the page level because of the implementation of ASP.NET templates. Each template runs in a separate naming container that makes it impossible for the run time processing the ASP.NET page to retrieve the drop-down list control by name within the page scope. So you cannot use ddViews, which is the ID of the drop-down list, as the programmatic identifier of the control in the page code. The ID ddViews makes sense only in the context of the template—a sort of child control with its own namespace—that contains it. To retrieve a valid instance of the drop-down list control, then, you need to resort to the following code:

DropDownList ddViews = (DropDownList) e.Item.FindControl("ddViews");

Notice that FindControl called from the Page object or the DataGrid object will not work, because FindControl knows how to locate a control only in the current naming container. In the body of ItemCommand, when the command name is ApplyView, e.Item represents the footer. Calling FindControl within the range of the footer turns out to be successful.

In ItemCommand, once you hold a reference to the drop-down list control, you pick up the name of the currently selected view and store it, as well as its index, in the Attributes collection of the grid. The drop-down list’s Value property contains the name of the ASCX file that represents the view, whereas its Text property points to a display name. The name of the file and its index need to be persisted across multiple invocations of the same page to guarantee that the control’s state can be correctly restored.

Refreshing the View

Once you know the name of the file to load the template from, you are pretty much finished. The only remaining work is letting the DataGrid control know about the new template for one of its columns. In our running example, the templated column is the second column. The next code snippet explains how to change the item template dynamically. The full source code for the ColumnView.aspx application is available on the companion CD.

TemplateColumn tc = (TemplateColumn) grid.Columns[1];
tc.ItemTemplate = Page.LoadTemplate((String)ViewState["CurrentViewFile"]);
grid.DataBind();

The final call to DataBind causes the grid to redraw its user interface, taking into account all the changes to the columns.

note | The working directory for LoadTemplate is the root of the current Web application, so you don’t need to indicate a fully qualified URL. However, LoadTemplate requires a virtual path and throws an exception if you pass it an absolute file system path with drive and directory information.
Loading Templates from Strings

Unlike many other methods that are expected to read from disk, the Page object’s LoadTemplate method does not support streams and writers. If this support were possible, then in-memory strings could have been used to create dynamic templates. If you don’t want to, or can’t afford to, make your application dependent on external ASCX files, how can you create dynamic templates? Typically, you don’t want to deal with ASCX files because having several file names hard coded in the source is too restrictive. Another good reason to avoid disk-based templates is that it results in the template code being just one of the configuration parameters of the application. In this case, in fact, you might have all this information stored in a centralized medium such as a SQL Server table or an XML file.

Is there a way to dynamically create a template from a string? I haven’t found any documentation that says it is possible or how to do it, but it may be that the documentation hasn’t been written yet. After looking at the programming interface of the involved classes, I haven’t been able to discover a way to do this yet. However, nothing really prevents you from creating a temporary file, writing the string that represents the layout, and loading a template from the temporary file. When you create temporary files from within an ASP.NET application, make sure that the file name is really unique for each concurrent session. For this purpose, use the Session ID or create a unique temporary file through the static method, Path.GetTempFileName. Bear in mind that the LoadTemplate method assumes it has been given a virtual path. On the other hand, stream and writer classes require absolute paths and don’t know how to cope with virtual paths. As a result, you come up with the following code to create and load a string-based template.

// Create the column object
TemplateColumn bc = new TemplateColumn();

// Create the temp file and write the template code
String tmp = Session.SessionID + ".ascx";
StreamWriter sw = new StreamWriter(Server.MapPath(tmp));
sw.Write(strLayoutCode);
sw.Close();

// Load the template from the temp file and add the column
bc.ItemTemplate = Page.LoadTemplate(tmp);
grid.Columns.Add(bc);

// Delete the temp file
File.Delete(Server.MapPath(tmp));

You need to use Server.MapPath to map a URL from a virtual to a physical path. You need to do this only when working with streams and files.

tip | Use the StringBuilder class to build the text of the template instead of concatenating strings with the plus sign (+) operator. The Append method (part of the StringBuilder class) really appends a given string to the internal buffer, whereas the plus sign (+) operator creates a brand new string that is the sum of the two.
Implementing ITemplate

When the ASP.NET run time processes a template, it parses the string, extracting the definition of the various controls that actually make it. These controls are then instantiated and added to the Controls collection of the naming container—typically a DataGridItem object. You can implement this pattern yourself by writing a made-to-measure class that inherits from ITemplate. A living instance of this class can then be assigned to any template property such as ItemTemplate.

The ITemplate interface has only one method, which is named InstantiateIn. The method is called to populate the user interface of the container control with instances of child controls in accordance with the expected template. You can certainly come up with a flexible and configurable class that reads from an external source the controls to create and bind to data. More simply, though, you might want to write ad-hoc classes—one for each needed template. Figure 3-10 shows just this.

Figure 3-10
Write the template text and generate a new column dynamically.
Structure of ITemplate Classes

The ITemplate class can be defined in the <script> section of an ASP.NET page as well as in separate C# or Visual Basic .NET class files that you link to the project. Another good place for this kind of code is in the code-behind file for the ASP.NET pages that use it. An ITemplate-based class looks like in the following code.

class LastFirstNameTemplate : ITemplate
{
    public void InstantiateIn(Control container)
    {...}

    private void BindLastName(Object s, EventArgs e)
    {...}

    private void BindFirstName(Object s, EventArgs e)
    {...}
}

In the body of InstantiateIn, you create instances of controls and add them to the specified container. For DataGrid controls, the container is an object of type DataGridItem. It will be DataListItem for a DataList control. In general, a container is any class that implements the INamingContainer interface.

If the control being added to the container’s Controls collection has to be bound to a data source column, then you also register your own handler for the DataBinding event. When the event occurs, you retrieve the text out of the data source and refresh the user interface of the control. When defined for a server control, the DataBinding event handler is expected to resolve all data-binding expressions in the server control and in any of its children. Let’s consider the layout that we repeatedly encountered earlier:

<%# "<b>" + DataBinder.Eval(Container.DataItem, "lastname") +
    "</b>, " + DataBinder.Eval(Container.DataItem, "firstname") %>

The following code demonstrates a template class that obtains the same results.

public class LastFirstNameTemplate : ITemplate
{
    public void InstantiateIn(Control container)
    {
        container.Controls.Add(new LiteralControl("<b>"));

        Label lblLName = new Label();
        lblLName.DataBinding += new EventHandler(this.BindLastName);
        container.Controls.Add(lblLName);

        container.Controls.Add(new LiteralControl("</b>, "));

        Label lblFName = new Label();
        lblFName.DataBinding += new EventHandler(this.BindFirstName);
        container.Controls.Add(lblFName);
    }

    private void BindLastName(Object sender, EventArgs e)
    {
        Label l = (Label) sender;
        DataGridItem container = (DataGridItem) l.NamingContainer;
        l.Text = ((DataRowView) container.DataItem)["lastname"].ToString();
    }

    private void BindFirstName(Object sender, EventArgs e)
    {
        Label l = (Label) sender;
        DataGridItem container = (DataGridItem) l.NamingContainer;
        l.Text =
            ((DataRowView) container.DataItem)["firstname"].ToString();
    }
}
The DataBinding Event Handler

A DataBinding event handler accomplishes two tasks. If programmed correctly, it should first take and hold the underlying data item. Second, it has to refresh the user interface of the bound control to reflect data binding.

A reference to the involved control can be obtained through the always declared, but not frequently used, sender parameter. The container that hosts the control is returned by the NamingContainer property of the control itself. At this point, you have all that you need to set up and use another well-known ASP.NET expression: Container.DataItem. The type of the data item depends on the data source associated with the DataGrid. In most real-world scenarios, it will be DataRowView. What remains is to access a particular column on the row and set the control’s bound properties.

note | Nearly identical code can be used to dynamically create templates for the DataList and the Repeater controls. The only difference is in the type casting required in the DataBinding event handlers.

The full source for the ITemplateClass.aspx application can be found on the companion CD.


Adapting Columns to Data

Templates are good at implementing special formatting features for columns, so you should take full advantage of their flexibility to adapt column layouts so that they really reflect the nature of your data. In most cases, plain text is good enough when you want to represent information from the data source. But in certain situations, you’ll want to use a custom template composed of controls whose structure is more closely aligned with the intent of your data. Want some examples of these controls? Consider Booleans, images, and arrays.

Showing Boolean Values

A Boolean field can take two possible values: true or false. In spite of its simplicity, or maybe because of it, a Boolean value does not have a unique graphical representation. So Booleans can look more user-friendly in applications when they are rendered with a yes/no or 0/1 pair. If you don’t want to render Booleans with plain text, check boxes are probably the most effective alternative. Let’s examine a concrete example.

The Employees table in the SQL Server 2000 Northwind database has a column named ReportsTo that contains numeric values indicating whether a given employee reports to someone. In particular, the content of the field is the employee ID of the boss and is null if the employee reports to no one. The following ASP.NET code demonstrates a templated column that uses a check box to render the content of the ReportsTo field. (Notice that this code snippet uses an “unknown” field name, boss, instead of the expected ReportsTo. This name is not a typo, and I’ll have more to say about it in a moment.)

<asp:TemplateColumn HeaderText="Reports">
    <itemtemplate>
    <asp:checkbox runat="server" enabled="false" checked='<%#
        HasBoss((int)DataBinder.Eval(Container.DataItem, "boss")) %>' />
    </itemtemplate>
</asp:TemplateColumn>

The CheckBox control is disabled to prevent users from clicking it and altering the state. The Checked property takes a data-binding expression that evaluates to a Boolean value returned by the user-defined function HasBoss. Figure 3-11 shows the output of the sample page.

bool HasBoss(int bossID)
{
    if (bossID != 0)
        return true;
    return false;
}
Figure 3-11
Using check boxes to render Boolean data.

Employing a user-defined function within the data-binding expression for the Checked property is not strictly necessary in our example. What really matters is that Checked is assigned a Boolean value. The ReportsTo field contains integer data, however, so casting is mandatory to satisfy the requirements of the strong typing characteristic of .NET. The following simpler code obtains the same effect:

checked='<%#
    Convert.ToBoolean(DataBinder.Eval(Container.DataItem, "boss")) %>

A subtle point to consider is that the ReportsTo field can contain nulls, but null values can’t be converted to Booleans. To work around this potential problem, I created an expression-based field named boss, which I return instead of the original ReportsTo. The query command that populates the grid of Figure 3-11 looks like this:

SELECT employeeid, titleofcourtesy, firstname, lastname,
    title, ISNULL(reportsto,0) AS boss
    FROM Employees

The contents of the ReportsTo field are filtered by the ISNULL T-SQL function. The preceding expression replaces each null entry in the given field with 0, making the conversion to the Boolean type possible and safe. The full source code for the Booleans.aspx application is available on the companion CD.

Showing Images

The template of a column can also contain images that you can use in many ways, not the least of which is to make the user interface more attractive. If you have a field that stores image URLs, you can insert a <asp:image> tag in the template of the column and make the ImageUrl attribute of the element point to the field content.

In the previous example, I disabled the check boxes to prevent users from clicking them. But now they don’t appear to represent valid pieces of information, and graying them out makes the user interface look inconsistent. I will fix this using images. Here’s how you do it.

To start off, you create two little GIF files, each containing the bitmap that browsers use to represent checked and unchecked controls.

Next, you use these little pictures in lieu of disabled check box controls. The code you need looks like the following:

<itemtemplate>
    <asp:image runat="server"
        imageurl='<%# GetProperGifFile(
             (int) DataBinder.Eval(Container.DataItem, "boss")) %>'
    />
</itemtemplate>

As shown in Figure 3-12, the final result of the code is decidedly attractive and effective. (The full source code for the Images.aspx application is available on the companion CD.) The look and feel of the user interface is significantly improved because the check boxes are not grayed out. In addition, images cannot be clicked or selected and are not even a valid target for the tab. The correct GIF file is selected by using a rather straightforward function:

String GetProperGifFile(int bossID)
{
    if (bossID != 0)
        return "checked.gif";
    return "unchecked.gif";
}
Figure 3-12
Even though the images look like check boxes, they are elements that cannot be clicked or selected.
Showing Arrays

Master-detail schemas are common in Web development: users select a record and expect to see more details about it. Typically, providing details in this way is implemented using two interrelated DataGrid controls or a single DataGrid control to list the primary records and a child form to display additional information.

In certain situations, however, you will need to display only an array of logically related information for each record in the table—for example, all the territories covered by an employee. Because a one-to-many relationship exists between employees and territories, a master-detail schema makes some sense, but setting up a master-detail schema to show only an array of strings (which is what the covered territories actually are from the code’s perspective) seems like overkill. Templated columns allow you to elegantly handle this display of related information by using drop-down list controls and ADO.NET data relations.

Let’s create a templated column that features a drop-down list control. How can you populate this control? Based on the structure of the Northwind database, the following SQL command returns a result set with two fields containing the ID of the employee and one of the territories she covers:

SELECT et.employeeid, t.territorydescription
    FROM EmployeeTerritories AS et
    INNER JOIN Territories AS t ON t.territoryid = et.territoryid

Figure 3-13 shows the structure of the result set.

Figure 3-13
This query displays the sales territories covered by an employee.

The concept behind the code is binding the drop-down list control to the subset of the result set that applies to a given employee. The following code associates the data source of the drop-down list with a user-defined function named GetTerritories:

<asp:TemplateColumn HeaderText="Territories">
<itemtemplate>
    <asp:dropdownlist runat="server" width="120px"
    datasource='<%# GetTerritories((DataRowView) Container.DataItem) %>'
    />
</itemtemplate>
</asp:TemplateColumn>

The GetTerritories function takes an argument of type DataRowView. GetTerritories is passed Container.DataItem, which is a view of the data row associated with the current row in the DataGrid control. The DataGrid control, as usual, is associated with a DataTable object that stems from a query on the Employees table.

DataSet ds = new DataSet();
String strCmd = "SELECT * FROM Employees";
SqlDataAdapter da = new SqlDataAdapter(strCmd, conn);
da.Fill(ds, "EmployeesTable");
grid.DataSource  = ds.Tables["EmployeesTable"];

GetTerritories takes a DataRowView object, but it needs a quick way to access another DataTable object’s rows that are logically related to the grid’s data source via the employee ID field.

You add a second DataTable object to the same DataSet object used to populate the DataGrid control. This table, named TerritoriesTable, contains the results of the previous query on the Territories table.

StringBuilder sb = new StringBuilder("");
sb.Append("SELECT et.employeeid, t.territorydescription ");
sb.Append("FROM EmployeeTerritories AS et ");
sb.Append("INNER JOIN Territories AS t ");
sb.Append("ON t.territoryid = et.territoryid");

// Append a second table to the same DataSet
da = new SqlDataAdapter(sb.ToString(), conn);
da.Fill(ds, "TerritoriesTable");

At this point, you create a DataRelation object to relate the two tables. A data relation is a logical, in-memory link that you set between two tables in the same DataSet object. A relation is established similarly to the way it is when using the INNER JOIN SQL command—the relation is based on the values of a common field.

note | The DataRelation object is a specific part of the ADO.NET object model. It has no direct counterpart in the ADO object model but shares a certain functional resemblance with ADO’s data shaping technology.

The following code shows how to create a relation between EmployeesTable and TerritoriesTable according to the values of the employeeid field.

DataColumn dc1 = ds.Tables["EmployeesTable"].Columns["employeeid"];
DataColumn dc2 = ds.Tables["TerritoriesTable"].Columns["employeeid"];
DataRelation drel = new DataRelation("EmployeesAndTerritories", dc1, dc2);
ds.Relations.Add(drel);

A DataRelation object must have a name (EmployeesAndTerritories, in this case) and, to be effective, must be added to the Relations collection of the DataSet object. Once the data relation has been successfully set, you call the GetChildRows method of the DataRow object and obtain the array of rows that matches the current value of the key field. GetChildRows returns an array of DataRow objects. GetTerritories just exploits an existing data relation to do its job.

ArrayList GetTerritories(DataRowView drv)
{
    // Extract the underlying row from the DataRowView object
    DataRow dr = drv.Row;

    // Retrieve the child rows according to the data relation
    DataRow[] adr = dr.GetChildRows("EmployeesAndTerritories");

    // Create and return an array made of the values in the
    // TerritoryDescription column
    ArrayList a = new ArrayList();
    foreach(DataRow tmp in adr)
        a.Add(tmp["territorydescription"]);
    return a;
}

As discussed in Chapter 1, an array object is good at populating a drop-down list control. Figure 3-14 shows the final result. The full source code for the Arrays.aspx application is available on the companion CD.

Figure 3-14
A templated column using a drop-down list control to show an array of related information.


Conclusion

The TemplateColumn class used in a DataGrid control lets you create a column with a customized control layout. This is extremely helpful in many circumstances. You use templates when you want to apply special formatting features not available through more direct channels such as CSS styles or the DataFormatString property of other column classes. You also use templates to combine several fields in a single layout to improve readability and user-friendliness. Templates are also the key to a meaningful graphical representation of data.

So far you know how to retrieve and present data. In Chapter 4, we’ll delve deep into the editing capabilities of the DataGrid control.


Editing Controls

Chapter 4
Editing DataGrid Controls

If you use DataGrid controls in your .NET Web application, chances are good that what you really need is an editable, tabular component that works in much the same way as an Excel worksheet. Typically, Excel worksheets appear to be read-only until the user points to a particular cell by using the cursor via the mouse or keyboard. In that moment of pointing to a cell, the user interface of the worksheet changes. The clicked cell is rendered through an interactive input control—such as a text box or a combo box. The user enters the new text and then closes the input operation by clicking a button. Clicking another button cancels the user’s operation, restoring the original text in the cell.

This user interface perfectly matches the functionality of the DataGrid control’s in-place editing feature. The output of the DataGrid control is an HTML table in which all cells are rendered as literal controls, links, or a custom layout defined by templated columns. Regardless of the column structure, all item rows in the resulting table are drawn in the same way. When the user triggers an event that begins the editing phase, the visible part of the grid is redrawn and—like Excel’s cells—the row selected for editing is rendered in a different way, using text box controls instead of literals and labels. The DataGrid control completes its own user interface with a couple of link buttons to allow the user to commit or roll back changes.

This suite of functions looks like a rather complex feature, but it’s yours for free—almost. All you have to do is set up a few attributes to enable editing and make all changes persistent. But is the in-place editing feature efficient and effective? That’s another story, indeed a rather interesting story, but with a happy ending. Let me tell you about it.


The In-Place Editing Feature

In-place editing in a DataGrid control is ruled by a special type of column named EditCommandColumn. You need to add exactly one column of that type to the grid to enable in-place editing. The text displayed in each cell of the column cannot be a data-bound or templated expression. It must be a literal string such as "Edit" or something you can define by using HTML tags, such as an image. You configure the EditCommandColumn class by specifying the text displayed for the Edit command button and for the link buttons that will let the user save or cancel an operation. For these three events—edit, update, and cancel—you must also provide handlers that control how the in-place editing begins, commits, or rolls back, respectively.

The DataGrid control provides a built-in mechanism for editing rows, but you are still responsible for supplying the actual code that updates the underlying data source. You also must provide any start-up or cleanup operation that needs to be accomplished when the editing begins or is canceled. I’ll say more about this in a moment.

You need to be acquainted with the following EditCommandColumn properties: EditText, UpdateText, and CancelText. They let you configure the appearance of the column that users click to switch to edit mode. The text you assign to these properties is typically rendered as a link button. You can change the button style from a link button to a push button by using the ButtonType property. An EditCommandColumn also features graphical properties typical of other column types, including HeaderText, HeaderStyle, FooterText, FooterStyle, and ItemStyle.

Enabling In-Place Editing

You enable in-place editing in a DataGrid control by adding an EditCommandColumn column anywhere in the grid. The following code snippet adds a captionless column with a yellow background. As shown in Figure 4-1, the display text is Edit for all items.

<asp:EditCommandColumn runat="server"
    EditText="Edit"
    UpdateText="OK"
    CancelText="Cancel">
    <itemstyle BackColor="yellow" />
</asp:EditCommandColumn>
Figure 4-1
A DataGrid control with a captionless column.

When the user clicks the Edit command button, the link button raises a server-side event. The page is posted back to the Web server and processed. The page is then sent back to the client with the clicked row rendered in a different manner—the Edit link button in Figure 4-1 is replaced by two link buttons: one to save changes and one to cancel any updates.

Registering Event Handlers

To edit in a DataGrid control, you must register three event handlers. You can do this declaratively by using the OnEditCommand, OnUpdateCommand, and OnCancelCommand attributes.

<asp:DataGrid id="grid" runat="server"

    OnEditCommand="EditCommand"
    OnUpdateCommand="UpdateCommand"
    OnCancelCommand="CancelCommand">

You can start with the following basic code when writing these handlers in your applications. Figure 4-2 shows how the grid changes as a result of this code.

public void EditCommand(Object sender,
                        DataGridCommandEventArgs e)
{
    // Set the current item to edit mode
    grid.EditItemIndex = e.Item.ItemIndex;

    // Refresh the grid
    UpdateView();
}

public void UpdateCommand(Object sender,
                          DataGridCommandEventArgs e)
{
    // TODO: Retrieve new text and update the data source

    // Reset the edit mode for the current item
    grid.EditItemIndex = -1;

    // Refresh the grid
    UpdateView();
}

public void CancelCommand(Object sender,
                          DataGridCommandEventArgs e)
{
    // Reset the edit mode for the current item
    grid.EditItemIndex = -1;

    // Refresh the grid
    UpdateView();
}
Figure 4-2
A DataGrid control in edit mode.

In Figure 4-2, notice that the Edit link button has been replaced by a pair of OK and Cancel link buttons. You control the text of these link buttons by using the EditText, UpdateText, and CancelText properties.

The DataGrid control’s EditItemIndex property gets or sets the index of the item currently being edited. When the in-place editing begins, you set this property to a valid index, which is the index of the item responsible for the postback event. When the editing phase terminates—no matter the reason—you set EditItemIndex to -1.

The value of the EditItemIndex property affects primarily the DataBind method. The DataBind method scrolls the grid’s Items collection and draws the corresponding table row for each element that falls in the range of visible items. If an item’s index matches the value in EditItemIndex, the corresponding row is rendered in edit mode by using text boxes instead of literals. If EditItemIndex is set to -1, no item is rendered in edit mode.

Understanding Read-Only Columns

Notice in Figure 4-2 that not all fields are editable. In particular, the ID and the Employee Name columns are not affected by in-place editing. The default editing mechanism does not work with all types of columns; it applies only to columns of the BoundColumn type. In the sample code we’re examining, the Employee Name column is a templated column. The ID column is explicitly marked as a read-only column.

<asp:BoundColumn runat="server"
    DataField="employeeid" HeaderText="ID" Readonly="true">
    <itemstyle BackColor="lightblue"
        Font-Bold="true"
        HorizontalAlign="right" />
</asp:BoundColumn>

Button and link columns are not editable, although in-place editing can be performed on their contents if you define them to use templates. You must define the editing layout of templated columns to make them updatable. I’ll cover this in detail in the next section.

Changing the Style of the Row

The DataGrid control’s EditItemStyle property lets you modify the default appearance of the row being edited. For example, you can set a contrasting background color, change the font family, or even adjust the text alignment. Look at Figure 4-3 to see the effect of the following declaration:

<EditItemStyle BackColor="yellow" Font-Bold="true" />
Figure 4-3
The EditItemStyle property changes the appearance of the edited row.

Notice that in Figure 4-3, the font style of the Employee Name cell is replaced by boldface and the ID cell in the selected row is unaffected by the new font style. A column that is marked read-only (in this case, the ID cell) is ignored when new styles are applied. A templated column, as well as any other type of column, is subject to the precedence rules for styles. The EditItemStyle has the highest priority in a grid, so it ends up overriding any other common settings for all columns. From highest to lowest priority, other styles follow like this: SelectedItemStyle, AlternatingItemStyle, ItemStyle.

Using Images

The value of the text properties EditText, UpdateText, and CancelText can include any HTML tag, and this can lead to a number of interesting application enhancements. For example, you can employ small images instead of, or together with, plain text to let users know where to click to begin editing. The following code shows how to proceed. Figure 4-4 illustrates the final result.

<asp:EditCommandColumn runat="server"
    EditText="<img src=edit.gif border=0 align=absmiddle
        alt='Edit this item'>"
    UpdateText="<img src=ok.gif border=0 align=absmiddle
        alt='Save changes'>"
    CancelText="<img src=cancel.gif border=0 align=absmiddle
        alt='Cancel editing'>">
    <itemstyle BackColor="yellow" HorizontalAlign="center" />
</asp:EditCommandColumn>

For better graphical results, the <img> tag’s border attribute has been set to 0 and the image alignment to absmiddle.

Figure 4-4
An editable grid that uses images and tooltips.
note

You can associate a tooltip with an editable item when EditText and other properties contain raw text. Use the following HTML expression:

EditText="<span title='Edit this item'>Edit</span>"
Reading Text from Edited Fields

So far you know how to set up an editable row and how to control the events that govern the overall editing process. In most cases, you don’t need to execute code in the EditCommand and CancelCommand event handlers that is more complicated than the basic code shown earlier in this chapter. Where you certainly do need to add significant code is in the UpdateCommand event handler, which runs when the user clicks the OK button to save changes. To add this code, you must accomplish two tasks. The first is to retrieve the text that is currently in the text boxes. The second is to update to the data source.

To retrieve the new text that users enter in the text boxes, you have to access the text boxes generated by the DataGrid control. Usually this is an easy task that can be elegantly solved by using the FindControl method on the page. The FindControl method, however, requires you to pass the control’s ID. But what’s the ID of a control that has been automatically generated?

You don’t really need to know the ID to locate a control in a grid row. The HTML structure of the generated row is pretty simple and, more important, can be easily understood from the source code of the client page.

<td><input name="..." value="..."></td>

From within a grid event handler, you access a particular cell of the current item using the following expression. Of course, nColumnIndex represents the 0-based index of the column.

e.Item.Cells[nColumnIndex]

With this expression, you address the <td> tag that refers to the column you want to read from. This <td> tag contains only one control—just the <input> tag you are searching for. You get the corresponding ASP.NET text box control by using this code:

e.Item.Cells[nColumnIndex].Controls[0]

The UpdateCommand event handler for the sample application we are examining changes as follows. Figure 4-5 shows the effect of this code.

public void UpdateCommand(Object sender,
                DataGridCommandEventArgs e)
{
    // Retrieve the new text
    int nColPositionIndex = 2; // 0-based position of the
                               // column
    int nColFromIndex = 3;     // 0-based position of the
                               // column

    TextBox txtPosition, txtFrom;
    txtPosition = (TextBox) e.Item.Cells[nColPositionIndex].Controls[0];
    txtFrom = (TextBox) e.Item.Cells[nColFromIndex].Controls[0];

    // Show the current text
    StringBuilder sb = new StringBuilder("");
    sb.Append("<b>[Position] will contain:</b> ");
    sb.Append(txtPosition.Text);
    sb.Append("<br>");
    sb.Append("<b>[From] will contain:</b> ");
    sb.Append(txtFrom.Text);
    lblCurrent.Text = sb.ToString();

    // Update the underlying data source
    // TODO

    // Reset the edit mode for the current item
    grid.EditItemIndex = -1;

    // Refresh the grid
    UpdateView();
}
Figure 4-5
After the user edits a row and clicks the OK button, the grid is restored to its original appearance. The label at the bottom of the grid explains that new text was successfully detected.

After the new text has been successfully read, you should proceed with the update and then make sure the grid’s data source reflects the current state of the underlying data store.

Updating the Data Source

To update the data source, you can use a single SQL statement or update the grid’s data set in memory and proceed with a batch update later. Whatever route you take has little to do with the DataGrid control itself. Choosing between single SQL statements or a batch update is more an application choice. Frequent and numerous changes, together with few other clients updating the table, suggest you opt for a batch update. Other scenarios—for example, a high level of concurrency—suggest you do the opposite.

In both cases, for the sake of consistency in the DataGrid control, make sure you properly update the data source that the grid is bound to. This might require a trip to the database. If you know for certain that no trigger, expression-based column, or auto-increment column is part of the database, you can avoid querying again. If there are no other side effects to updating the database that you are aware of, you can update the grid’s data source directly in memory with the text read from the text boxes.

Using Stored Procedures

As an alternative to the approaches for updating the data source described in the previous section, you could use a stored procedure with one or more output parameters that returns the updated values read directly from the database. For example, the following stored procedure sets the Title and Country fields of the specified employee in the Employees table of Northwind. (There are only two editable columns in the grid.) In addition, the stored procedure re-reads and returns the content from the fields. If any server-side code modifies the fields when the fields are updated, the changes are instantly detected and returned to your ASP.NET code.

CREATE PROCEDURE MySampleUpdate (
    @nEmpID int,                      -- EmployeeID
    @sPosition nvarchar(30),          -- Value to update field
                                      --  TITLE
    @sCountry nvarchar(15),           -- Value to update field
                                      --  COUNTRY
    @NewPosition nvarchar(30) OUTPUT, -- returns current value
                                      --  of TITLE
    @NewCountry nvarchar(15) OUTPUT)  -- returns current value
                                      --  of COUNTRY
AS

UPDATE Employees SET
    Title=@sPosition,
    Country=@sCountry
WHERE employeeid=@nEmpID

SELECT
    @NewPosition = Title,
    @NewCountry = Country
FROM Employees WHERE employeeid=@nEmpID
GO

You use this procedure from ASP.NET in the following manner:

SqlConnection conn = new SqlConnection(txtConn.Text);
SqlCommand cmd = new SqlCommand("MySampleUpdate", conn);
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter p1, p2, p3, po1, po2;

// Set the EmployeeID param to identify the row to update
p1 = new SqlParameter("@nEmpID", SqlDbType.Int);
p1.Direction = ParameterDirection.Input;
p1.Value = grid.DataKeys[e.Item.ItemIndex];
cmd.Parameters.Add(p1);

// Set the Position param to pass the new value
p2 = new SqlParameter("@sPosition", SqlDbType.NVarChar, 30);
p2.Direction = ParameterDirection.Input;
p2.Value = txtPosition.Text;
cmd.Parameters.Add(p2);

// Set the Country param to pass the new value
p3 = new SqlParameter("@sCountry", SqlDbType.NVarChar, 15);
p3.Direction = ParameterDirection.Input;
p3.Value = txtFrom.Text;
cmd.Parameters.Add(p3);

// Output param to get the new value for POSITION
po1 = new SqlParameter("@NewPosition", SqlDbType.NVarChar, 30);
po1.Direction = ParameterDirection.Output;
cmd.Parameters.Add(po1);

// Output param to get the new value for COUNTRY
po2 = new SqlParameter("@NewCountry", SqlDbType.NVarChar, 15);
po2.Direction = ParameterDirection.Output;
cmd.Parameters.Add(po2);

// Executes the procedure
conn.Open();
cmd.ExecuteNonQuery();
conn.Close();

The contents of the stored procedure’s two output parameters can be accessed by using their Value properties. Once you know the values stored in the table, you can avoid a full reloading of the grid’s data source to show current data. To summarize, when you are going to reload the whole data set after the update, use a simple SQL statement and don’t worry about possible triggers or anything else that could modify the data you submitted. Requerying for data will fix it all. This approach is shown in the EditRowsWithUpdate.aspx application, the full source code for which is available on the companion CD.

When you want to optimize the code and avoid reloading the data every time a row is updated, use the MySampleUpdate stored procedure we examined as your starting point. You perform the update and obtain the current value for the fields the user attempted to change. You then modify the cached grid’s data source and rebind. As a result, you have perfectly synchronized data without reloading anything from the database. The following code shows a possible way to update cached data. The full source code for the EditRowsWithUpdateSP.aspx application is available on the companion CD.

void UpdateCachedData(int nEmpID, String sNewPosition,
                        String sNewCountry)
{
    // Retrieves the grid's data source from Session or from
    // someplace else you cached it
    DataSet ds = (DataSet) Session["MyData"];
    DataTable dt = ds.Tables["MyTable"];

    // Retrieves the row to update based on the primary key
    // Only one row is expected
    DataRow[] adr = dt.Select("employeeid=" + nEmpID);

    // Updates the row and persists changes with the DataTable
    adr[0]["title"] = sNewPosition;
    adr[0]["country"] = sNewCountry;
    dt.AcceptChanges();

    // Caches the updated DataSet for further use
    Session["MyData"] = ds;
}
Using Triggers

A typical circumstance in which the data you send can be changed on-the-fly in the database involves update triggers. Let’s see how the two approaches I described in the preceding section—using output parameters with a stored procedure and then updating the local cache of data, and reloading data directly from disk—work with triggers. Consider the following code:

CREATE TRIGGER HandleUpdate ON Employees FOR UPDATE
AS
IF UPDATE(Country)
    UPDATE Employees SET Country='United Kingdom'
        WHERE Country = 'UK'

The trigger simply replaces 'UK' with 'United Kingdom' during each update. The final result does not change for either approach. The trigger does its job and the DataGrid control is correctly updated, as Figure 4-6 shows.

Figure 4-6
The database modified the original text the user entered. The DataGrid control shows the correct data set.
A Long List of Drawbacks

In-place editing doesn’t require much from you. The only aspect you have to worry about is updating. Despite what I say, I’m very hesitant about promoting this type of in-place editing for a wide range of applications. It definitely has a number of issues that I don’t consider minor, such as the following:

  • Only visible fields are updatable.

  • The default text displayed by the text boxes is the plain contents of the cell. With precalculated columns, you end up modifying a column that does not map exactly to a field in the target database.

  • If you used special column formatting via the DataFormatString property, the extra text will appear in the text box.

  • No validation is possible on the client.

  • You cannot control the way in which the data entry takes place. What if you want to let users pick a value from a list?

  • You can update only BoundColumn columns. What if you want to change the data-bound URL of a link column? Or the path of an image?

  • Retrieving the new text is a bit impractical because you have to walk through a forest of array indexes.

You can work around all these issues by using edit templates. In other words, any editable column features a special template that provides the custom cell layout when in editing mode. This approach isn’t free from problems either, but the problems do not in any way limit your programming power. Only templated columns can have a template for editing. This means that if you want a special editing template for a column originally designed as a bound or link column, you need to turn that column into a templated column.


Editing In Place Using Templates

For columns belonging to the TemplateColumn class, you can define a special template that is applied when one cell of such a column is being edited. In previous examples, I built a templated column that displayed data obtained by merging the first and last names with the title of courtesy. When you need to edit the content of a heterogeneous column like that, provide a template that includes each input control. For example, you might want a drop-down list supplying all the possible values for the titleofcourtesy column. You also must use separate text boxes for the first and last names. The following code creates a possible edit template:

<edititemtemplate>
    <asp:dropdownlist runat="server" id="ddTitles"
        DataSource='<% # aTitlesOfCourtesy %>' />
    <asp:textbox runat="server" width="80px" id="txtFirstName"
        Text='<%# DataBinder.Eval(Container.DataItem,
            "firstname") %>' />
    <br>
    <asp:textbox runat="server" width="140px" id="txtLastName"
        Text='<%# DataBinder.Eval(Container.DataItem,
            "lastname") %>' />
</edititemtemplate>

The presence of the EditItemTemplate template automatically makes a templated column editable. Figure 4-7 shows what the edit template looks like.

Figure 4-7
An editable templated column.

The drop-down list of titles is filled with the contents of a global array that is declared as follows:

String [] aTitleOfCourtesy = new String[4]
                            {"Ms.", "Mr.", "Mrs.", "Dr."};

Other fields are rendered via text boxes initialized with data-bound contents. As you can see, using edit templates gives you the fullest flexibility because you can directly control any single step of the editing process: you can give the desired ID to the constituent controls, you can choose the controls that best suit your data, and you can place any ASP.NET validation control between the controls you choose. For example, you can use the ASP.NET class RegularExpressionValidator to make sure that the text typed in a text box matches a given mask.

Working with Template Controls

Templates simplify the way you retrieve instances of the controls involved with the edit. You don’t have to use the position-based approach examined earlier, because templates let you use the ID to identify controls. The use of the ID is not an option with edit templates—it’s an absolute necessity. Using unambiguous IDs is the only reliable way to retrieve a control during the edit phase.

Let’s continue with our edit template example. The following code shows how to retrieve run-time values for the controls in the template of the Employee Name column:

TextBox txtFirstName = (TextBox) e.Item.FindControl("txtFirstName");
TextBox txtLastName = (TextBox) e.Item.FindControl("txtLastName");
DropDownList ddTitles = (DropDownList) e.Item.FindControl("ddTitles");

Notice that the templates are rooted in a separate naming container, which affects how FindControl works. First, you cannot call FindControl at the page or grid level. Instead you must resort to using the FindControl method of a templated DataGrid control item. Second, you cannot use any of those IDs in grid-level code. For example, using ddTitles directly in your code will result in a compile-time error. After you access all the template’s controls, you can go on with the data source update as shown earlier.

Initializing Controls

Look carefully at Figure 4-7. You should notice an anomaly. The selected item in the drop-down list of possible titles does not match the correct title of the employee. In the figure, the record of employee Michael Suyama is being edited. The Title drop-down list defaults to Ms. instead of matching the current value. This matching issue introduces the more general topic of how to initialize the status of child controls in edit templates.

The first place I try to assign ad-hoc starting values to controls is in the EditCommand event (the user-defined procedure that enables in-place editing). The problem with this approach is that EditCommand is raised before the edit template is created. EditCommand is expected to set the EditItemIndex property, which causes the grid to draw in edit mode. To access controls at creation time, you should once again rely on the ItemCreated event.

public void ItemCreated(Object sender, DataGridItemEventArgs e)
{
    ListItemType lit = e.Item.ItemType;
        if (lit == ListItemType.EditItem)
    {
        // Retrieve the drop-down list control to set up
        DropDownList ddTitles;
        ddTitles = (DropDownList) e.Item.FindControl("ddTitles");

        // Get the item data as a DataRowView object
        DataRowView drv = (DataRowView) e.Item.DataItem;

        // Select the index that matches the title
        // in the current row
        if (drv != null)
        {
            String strTitle = drv["titleofcourtesy"].ToString();
            int nIndex = Array.IndexOf(aTitleOfCourtesy, strTitle);
            ddTitles.SelectedIndex = nIndex;
        }
    }
}

Make sure that the item type being created is EditItem. Then retrieve both the current instance of the drop-down list control and the data that is bound to the current grid item. This information depends on the kind of data you associated with the grid. For example, if you used a DataTable object, the item’s data consists of a DataRowView object. Finally, you select the array element that matches the row value in the titleofcourtesy column and assign it to the SelectedIndex property of the drop-down list.

Stripping Off Unneeded Formatting Text

To make the cells of your data grid look more user-friendly, you can employ the DataFormatString attribute of BoundColumn, which will add style information that you cannot otherwise add. For example, suppose you want to pad a cell only horizontally. You must resort to the margin CSS attribute, which you have to apply directly to the content of the cell. You can’t achieve this effect with any style property that the grid makes available. So you define DataFormatString as follows:

DataFormatString="<span style='margin-left:5;
                   margin-right:5'>{0}</span>"

The value of the cell is wrapped by a <span> tag. The drawback is that when the user attempts to edit the column, the text box defaults to the whole HTML expression that constitutes the contents of the cell, as shown in this example:

<span style='margin-left:5;margin-right:5'>USA</span>

To work around this editing issue, you hook up the ItemCreated event, retrieve the text box control, and explicitly set the control to the contents of the data-bound field.

Adapting Layouts to Data

When you use a DataGrid control, you rarely display all the fields in the underlying table. When you edit a row, however, you might want to give users a chance to enter information in fields that are not in the grid’s Columns collection. Templates give you this functionality. Just add some extra controls to one of the edit templates you define for a column.

The flexibility of templates also lets you adapt the edit layout to the data you are handling. For example, images and links aren’t rendered through plain literal controls and require special rendering support from the grid. However, a database field might exist behind them, and to edit its contents, you use a template. Let me show how the process works for images.

Editing Columns That Point to Images

Suppose you have a table with a field named photo. The field does not contain pixels of an image; it contains only a relative path name. You can define an image column as you work by using the following template. Figure 4-8 shows the result of this code.

<asp:TemplateColumn runat="server" HeaderText="Photo">
    <itemtemplate>
        <img runat="server" width="50"
            src='<%# DataBinder.Eval(Container.DataItem,
            "photo") %>' />
    </itemtemplate>
</asp:TemplateColumn>
Figure 4-8
A DataGrid control that uses a templated column to show images.

To edit the underlying image path, use an edit template. All you have to do is adapt the grid’s user interface to the data you display. You display image files by using pictures and edit the corresponding database column by using text boxes. Following is the code for an edit template that produces the results in Figure 4-9.

<edititemtemplate>
    <asp:textbox runat="server" id="txtPhoto"
        Text='<%# DataBinder.Eval(Container.DataItem, "photo")
    %>' />
    <hr>
    <img runat="server" width="100"
        src='<%# DataBinder.Eval(Container.DataItem, "photo")
    %>' />
</edititemtemplate>
Figure 4-9
An edit template allows editing of an image’s path name.

Sometimes when you use image-based columns, you won’t have an image to display. When no image is found, you might want to resort to a different user interface. For example, you could employ a No picture available label instead of the image control. You could look at this example and decide that you just need dynamically loaded templates (discussed in Chapter 3), but this is a simple case and can be solved with a data-binding expression, as shown in the following code. Figure 4-10 shows the results of the code.

<itemtemplate>
    <img runat="server" width="50"
        visible='<%# IsFile(DataBinder.Eval(Container.DataItem,
        "photo")) %>'
        src='<%# DataBinder.Eval(Container.DataItem, "photo")
    %>' />
    <asp:label runat="server"
        text="<i><small>No picture available.</small></i>"
        visible='<%#
        !IsFile(DataBinder.Eval(Container.DataItem, "photo"))
    %>' />
</itemtemplate>

The template makes the <img> tag and the label mutually exclusive by hiding the one that does not apply.

private bool IsFile(String strFile)
{
    return File.Exists(Server.MapPath(strFile));
}
Figure 4-10
When you don’t have an image to display in an image-based column, you can use a data-binding expression to display a label instead.
Editing Link Columns

You handle the editing of columns containing a URL in almost the same way you handle the editing of image-based columns. If, for example, the information in a column containing a URL has to be considered read-only, you can use ad-hoc columns descending from the HyperLinkColumn class. If the contents of the field will be edited at run time, you are better off dropping ad-hoc columns in favor of templated columns. In this case, you can define a specific edit template.

<asp:TemplateColumn runat="server" HeaderText="URL">
    <itemtemplate>
        <a runat="server" target="myframe"
            href='<%# DataBinder.Eval(Container.DataItem,
            "url") %>'>
        Go</a>
    </itemtemplate>
    <edititemtemplate>
        <asp:textbox runat="server" id="txtUrl"
            Text='<%# DataBinder.Eval(Container.DataItem,
            "url") %>' />
    </edititemtemplate>
</asp:TemplateColumn>
Editing Non-Displayed Columns

The layout for the edit template of a given column is rendered by ASP.NET code that you can devise and implement. You must use the ASP.NET controls that best fit the characteristics of the data being edited. You can also add extra controls to the template of a given column. These extra controls could, for example, refer to columns in the database that are not displayed in the normal view of the DataGrid control. You can take this approach even further and use the template’s in-place editing features to build full-fledged interactive forms that users update with current data.

In Figure 4-8, you saw a DataGrid control featuring three templated columns—Employee Name, Position, and Photo. Employee Name and Position are clearly made of more fields than the Photo column is. A user in edit mode, then, would reasonably expect to see several controls ready to receive her input. The Employees table actually has many more fields than are typically displayed. Some of these fields—say, the notes and hiredate fields—can also be edited in edit mode. For editing purposes, non-displayed columns must be hosted by other templated columns. The way you group editable columns is arbitrary and ruled only by your common sense for and sensitivity to user-friendliness.

The following edit template falls under the Employee Name column and accommodates the editing of four fields: titleofcourtesy, firstname, lastname, and hiredate. The hiredate column is edited through a special server-side control: the Calendar control.

<edititemtemplate>
    <asp:dropdownlist runat="server" id="ddTitles"
        DataSource='<% # aTitleOfCourtesy %>' />
    <asp:textbox runat="server" width="80px" id="txtFirstName"
        Text='<%# DataBinder.Eval(Container.DataItem,
            "firstname") %>' />
    <br>
    <asp:textbox runat="server" width="140px" id="txtLastName"
        Text='<%# DataBinder.Eval(Container.DataItem,
            "lastname") %>' />
    <hr>
    <asp:calendar runat="server" id="cHireDate"
        BackColor="white" ForeColor="black" Font-Size="9px"
        DayHeaderStyle-Font-Bold="True"
        VisibleDate='<%# DataBinder.Eval(Container.DataItem,
            "hiredate") %>'
        SelectedDate='<%# DataBinder.Eval(Container.DataItem,
            "hiredate") %>' />
</edititemtemplate>
note | The Calendar control requires you to set both VisibleDate and SelectedDate to the same DateTime value if you want a particular date to appear selected. The VisibleDate property ensures that a given date is visible in the displayed page of the calendar. The SelectedDate property draws the date using a special style to indicate the current selection.

The preceding code will generate a run-time error if the hiredate field is null. To avoid the null value, do the following:

VisibleDate='<%# IsDate(
    DataBinder.Eval(Container.DataItem, "hiredate")) %>'
SelectedDate='<%# IsDate(
    DataBinder.Eval(Container.DataItem, "hiredate")) %>'

The function IsDate will simply check whether the passed object is null and, in our case, default to the current date.

private DateTime IsDate(Object o)
{
    if (o is DBNull)
        return DateTime.Now;

    return Convert.ToDateTime(o);
}
caution | The Calendar control originates a round-trip for each selection the user makes. Although this control is cool and powerful, for a better performance, you might also want to provide a plain text box so that the user can type in dates manually.

The code that follows illustrates an edit template for the Position column. It embeds a multiline text box control so that the user can edit the notes field.

<edititemtemplate>
    <asp:textbox runat="server" id="txtPosition"
        Text='<%# DataBinder.Eval(Container.DataItem, "title")
    %>' />
    <br>
    <asp:textbox runat="server" id="txtCountry"
        Text='<%# DataBinder.Eval(Container.DataItem,
        "country") %>' />
    <br>
    <asp:textbox runat="server" id="txtNotes"
        textmode="multiline" rows="9"
        Text='<%# DataBinder.Eval(Container.DataItem, "notes")
    %>' />
</edititemtemplate>

As Figure 4-11 shows, edit templates can transform a powerful (but sometimes anonymous) HTML table into an awesome repository of features and functionality. The full source code for the EditWithImages.aspx application is available on the companion CD.

Figure 4-11
Using all the features of templates gives you so much flexibility when working with in-place editing in DataGrid controls.


Adding New Rows

You probably want your DataGrid controls to serve as a replacement for desktop worksheets. One feature we haven’t discussed yet is adding rows dynamically. Before going any further, though, let me shed some light on the reasons that row insertion is not part of the DataGrid control’s set of functionality.

The DataGrid control is already complex. It comes with a wide range of events that, properly hooked up, offer extensive functionality. Adding even more events would have led to an overly complex and unwieldy control.

There are no serious reasons to drop the traditional implementation of the insertion functionality. Enabling the user to click a button somewhere on a page so that an input form pops up ready to collect and submit data is still an excellent way of solving the insertion issue. The add-row facility for DataGrid controls, described later in the chapter, is simply another insertion option. I find it particularly suitable when I’m working with tables that have a limited number of columns. In other words, it is great for tables that do not deserve a full-fledged update module. When all is said and done, an add-row DataGrid control is really just the Web counterpart of the grid pane that both the SQL Server Enterprise Manager and Microsoft Access use to show table rows.

Setting Up the Add-Row Feature

Your user wants to be able to click a button and add a new blank row to the data grid (that is possibly already in edit mode), continue the editing process, and save any changes entered. While the user is working, you want to keep the underlying data store and the grid’s Items collection perfectly synchronized.

The link button that enables the add-row functionality can be placed anywhere in the page. It makes sense to place it within the grid—for example, in the footer. To place the button in the footer, set the grid’s ShowFooter property to true. Next, define a footer template for one of your templated columns. Finally, hook up the ItemCreated event and remove some cells from the footer row. (This technique was fully demonstrated in Chapter 3.)

The footer template might look like the following:

<footertemplate>
    <asp:linkbutton runat="server" OnClick="AddNewRow"
        Text="Add new row..."
        Enabled='<%# IsLastPage() %>' />
</footertemplate>

The link button is not enabled all the time; the DataGrid control can be made of several pages, but only on the last page can the user click the button to add a new row. Figure 4-12 shows this link button activated only on the last page. You can easily rework this decision in your implementation. (Of course, this implementation does not need to be changed and remains fully functional if you use grids without pagination.)

private bool IsLastPage()
{
    // CurrentPageIndex is 0-based...
    if (grid.CurrentPageIndex+1 == grid.PageCount)
        return true;
    return false;
}

The IsLastPage function returns true or false, depending on whether the user is positioned on the last page.

Figure 4-12
The link that lets you add a new row is active only on the last page of the grid.
Adding Blank Rows

To add a row, a new DataRow object must be appended to the DataTable object that the grid is bound to. To wed seamless development and performance, the insertion must be done in memory first, and then submitted to the database server only when the changes have been committed by the user. I highly recommend that the DataTable object be cached locally to avoid accessing the database too many times. However, this aspect of the coding has more to do with the overall structure of the page than with the details of the add-row feature. As a general guideline, bear in mind that “cached is better.” In the code that follows, I used the Session object to cache the DataSet object that is used to populate the grid. Refer to the discussion in Chapter 2 under “Using the Session Object” to understand the pros and the cons of this choice.

public void AddNewRow(Object sender, EventArgs e)
{
    // Grab the dataset
    DataSet ds = (DataSet) Session["MyData"];
    DataTable dt = ds.Tables["MyTable"];

    // Add a blank row
    DataRow dr = dt.NewRow();
    dt.Rows.Add(dr);

    // If needed, assign default values
    // dr["column_name"] = ...

    // Update the in-memory DataSet. This step is needed to
    // refresh the grid properly, given the structure of the
    // UpdateView method.
    Session["MyData"] = ds;

    // Index of the new item in the page: last +1
    int nNewItemIndex = grid.Items.Count;

    // If the page is full, move to next page. In this
    // case, first item.
    if (nNewItemIndex >= grid.PageSize)
    {
        grid.CurrentPageIndex ++;
        nNewItemIndex = 0;
    }

    // Turn edit mode on for the newly added row
    grid.EditItemIndex = nNewItemIndex;

    // Refresh the grid
    UpdateView();
}

After a new blank row is generated and added to the DataTable object, you update the cached copy of the data. This allows the grid to refresh properly, given the structure of the UpdateView method.

private void UpdateView()
{
    DataSet ds = (DataSet) Session["MyData"];
    grid.DataSource = ds.Tables["MyTable"];
    grid.DataBind();
}

Notice that in this sample code, the UpdateView method works by retrieving the DataSet from Session. As shown in Figure 4-13, any changes must be saved to become effective.

Figure 4-13
Changes must be saved before they are applied.
Switching to Edit Mode

Let’s see how to set the newly added row so that it works in edit mode automatically. You can activate edit mode for any row in the current page of the grid by setting the EditItemIndex property. The index of the new row by design is the last row in the page. Considering that the index is 0-based, the actual number of items in a page is provided by the following expression:

int nNewItemIndex = grid.Items.Count;

But a subtle problem arises. What if the last page is full and adding a new row violates the grid’s page size setting? In this case, a new page must be added containing only the new row.

if (nNewItemIndex >= grid.PageSize)
{
    // The new row is the first of the new page
    grid.CurrentPageIndex ++;
    nNewItemIndex = 0;
}

// Turn edit mode on for the newly added row
grid.EditItemIndex = nNewItemIndex;

You don’t have to worry about the new page. It will be generated automatically by the DataGrid control during the execution of the DataBind method, which is invoked by UpdateView. Properly setting the CurrentPageIndex and EditItemIndex properties is critical. Their values will determine the page displayed and the item rendered in edit mode. Figure 4-14 illustrates this example.

Figure 4-14
A new editable row has been added to the DataGrid control.
Updating the Table and the Grid

At this point, the DataGrid control has a new blank row. If you want to fill certain fields with default values, you can do that after creating the row. Your next step is to remove the row if editing is canceled or save the changes to the database if changes are committed. To understand how to accomplish this step, let’s review how the internal state of a DataSet object changes when you add, delete, or modify rows.

The DataSet Object Commit Model

The DataSet, DataTable, and DataRow objects maintain a local cache of changes. When a row is modified, deleted, or added, its state changes to one of the values of the DataRowState enumeration. (See the .NET Framework documentation for details.) Similarly, when a row is added, modified, or deleted from a table, the internal state of the table is altered, resulting in pending changes for the affected rows. When a table in a DataSet object has pending changes, the whole state of the DataSet object is modified to show pending changes. For all these objects, you detect changes by calling the HasChanges method. This method is overloaded to let you ask for specific changes—for example, only insertions or deletions. The GetChanges method returns a table with all the current changes.

Pending changes can be either accepted or rejected. You can do that at any level:

  • If you accept or reject changes for a DataSet object, all changes in all tables are processed.

  • If you accept or reject changes for a DataTable object, all rows in the table are processed.

  • If you accept or reject changes for a single row, only that row is processed.

Accepting a pending change means that the row (changes always involve a row) updates are committed to the table. Rejecting a pending change rolls back the state of the table, and the table appears as though the change never occurred. By default, a data adapter returns a DataSet object in a committed state, that is, with no pending changes.

When you add a new row, you alter the state of the DataSet object, which now has one pending change. This change can be either rejected or accepted and is automatically handled by the DataGrid control and the data container objects. What you have to do is simply call the proper method at the proper time.

Canceling the Update

If the user cancels editing, the newly added row must be removed from the grid. You can easily accomplish this by using the RejectChanges method on the DataRow object.

public void CancelCommand(Object sender,
                DataGridCommandEventArgs e)
{
    // Reset the edit mode for the current item
    grid.EditItemIndex = -1;

    // Reject changes on the last added row
    DataSet ds = (DataSet) Session["MyData"];
    DataTable dt = ds.Tables["MyTable"];
    DataRow drLast = dt.Rows[dt.Rows.Count-1];
    if (drLast.RowState == DataRowState.Added)
    {
        drLast.RejectChanges();

        // Move to previous page if the rejected row is the
        // only row in the current page
        if (grid.Items.Count == 1)
            grid.CurrentPageIndex--;
    }

    // Refresh the grid
    UpdateView();
}

You retrieve the DataRow object just added and then reject it. If the row is the only one in the current page, an error would occur because the CurrentPageIndex property points to an empty page. To prevent this error, you move back to the previous page. After the pending change is rejected, the edit item index is reset to -1, refreshing the grid and restoring a consistent situation.

Saving Changes with SQL Statements

Your solution for saving changes can differ according to the update mechanism you choose—direct SQL statements or a batch update. Let’s consider direct SQL statements for now. For SQL statements, you don’t need to accept the pending change. In fact, after you update the underlying database, reloading the data set gives you an up-to-date table of records. So you can reject the change, execute the update, and then reload the data from the source. This approach ensures that your set of rows is a true snapshot of the database and takes into account any server-side changes due to triggers or auto-increment fields.

As discussed earlier in this chapter, to update the database, you can employ a stored procedure that returns output parameters for fields that are subject to server-side modifications. In this case, you might want to execute the update command first, then update the new row with data returning from the database, and finally accept the changes.

In the UpdateCommand event handler, you should determine whether you have to update a row or insert a new one. You can use the HasChanges method to see whether the last row in the table has been added. The full source code for the AddRows.aspx application is available on the companion CD.

DataTable dt = ds.Tables["MyTable"];
DataRow drLast = dt.Rows[dt.Rows.Count-1];
if (drLast.RowState == DataRowState.Added)
{
    // Drop the last row from memory (if added)
    drLast.RejectChanges();

    // Build the INSERT command string
    StringBuilder sb1 = new StringBuilder("");
    sb1.Append("INSERT Employees (firstname, lastname, ");
    sb1.Append("titleofcourtesy, title, country) VALUES(");
    sb1.Append("@sFirstName, @sLastName, @sTitle, @sPosition,
                @sCountry)");
    cmd.CommandText = sb1.ToString();

Once again, this approach works fine only when you are not using batch update. Notice that I don’t explicitly add a value for the employeeid field because it is marked as an identity column whose values are automatically generated by SQL Server. Figure 4-15 shows how the add-row feature works.

Figure 4-15
The add-row feature lets the user add and edit a new row.
Saving Changes with Batch Updates

Batch update is an ADO.NET feature that lets you add, delete, and update records in memory and then submit all the changes in a single shot to the database server. A batch update is particularly useful in low-contention scenarios in which the likelihood of conflicting changes—namely that the field you are going to update has been already updated since your last read—is relatively low. (For a more detailed discussion of the pros and cons of ADO.NET implementation of the batch update, see Chapter 7 and Microsoft ADO.NET (Core Reference) by David Sceppa, Microsoft Press, 2002.)

If you support batch update in your application, chances are good that, at any time, you have several pending changes in addition to the one generated when the last row was added. For example, the list of pending changes could include previously added rows as well as deleted or updated rows. In this case, you should handle RejectChanges with extreme care.

To cancel an added row, you extract the last row from the DataTable object and reject only that row. To save the contents of a newly added row, you modify only the row with the data read from the input controls and then refresh the grid. You should not accept changes, however. This is a fundamental issue for the batch update. When you call the data adapter’s Update method to submit changes to the server, only pending changes are considered. Accepting changes by using AcceptChanges will, at all levels (DataSet, DataTable, and DataRow), clear the pending state of changes so that no changes are sent to the server for updating.

A batch update presents another subtle problem you have to cope with. If the table has an auto-increment field (for example, an identity column such as employeeid in the Northwind’s Employees table), how can you learn its true value? This value is generated by the database, and you cannot know it until you execute an update command on the server. So when you insert a new row in the grid, and you use a batch update in the page, what value will you be placing in the auto-increment field? Well, your only solution is to execute a physical insertion in the database. There are a few workarounds, though.

Normally, leaving the field blank does not pose practical problems, but you and your users have to accept the fact that the grid contents are not synchronized with what will become the database contents. To mark this pending state, you can fill the auto-increment field with a negative value and draw the row in a special fashion. The altered appearance of the row suggests to users the volatile nature of the changes. The value you use in the grid does not affect the update operation because that field will not be part of the update. Leaving an auto-increment field blank creates an error when an in-memory primary key is based on the column with that field. Note that in-memory indexes on a DataTable object have to be set explicitly through the PrimaryKeys collection. If this isn’t done and if the DataTable object with the AddWithKey option isn’t loaded, you can safely leave a key field blank. AddWithKey is one possible value for the data adapter object’s MissingSchemaAction property.

note | You could work around the intricacies of auto-increment columns with GUID columns. In this case, the value is guaranteed to be unique throughout the table. Additionally, you don’t need to rely on SQL Server to generate the value. One slight drawback is that a GUID column is 16 bytes long.

To discover the value assigned to an auto-increment field without reloading the data set after an update, you can use a stored procedure with an output parameter. If knowing this value in advance is really vital for your application, however, you can try the following:

  1. Insert an empty row in the database from the UpdateCommand handler.

  2. Get the auto-increment value, for example, by using the output parameter of an ad-hoc stored procedure.

  3. Accept the changes for the corresponding row in the grid.

  4. Modify the row with the auto-increment value.

  5. Submit the changes.

Accepting the changes and then modifying at least one field will modify the status of the row from Added to Modified. In this way, the data adapter object’s Update method will try to submit the in-memory contents of the row to the database rather than insert the row.

tip

You can also try to guess the next auto-increment value for a table using the IDENT_CURRENT T-SQL function. This function returns the last identity value generated for a specified table in any session and any scope. The next value is the last one plus the identity increment for the column—usually 1. Of course, you don’t obtain a reliable value because the return value is subject to insertions that might occur in the meantime. You obtain the last identity for a table with the following code:

SELECT IDENT_CURRENT('table_name')


Conclusion

In this chapter, I reviewed the DataGrid control’s built-in mechanism for in-place editing. You can use it most effectively when you take advantage of edit templates, which require you to use template columns but provide you with unprecedented flexibility. You also learned how to add new rows to the DataGrid control and about the serious update issues that arise.

In general, when it comes to data editing you have a couple of options to choose from, one of which is grid-based editing that mimics Microsoft Excel functionality. The second option is a more traditional approach based on separate pages, or frames, that display ad-hoc pages. Although powerful and effective, ADO.NET data binding is a sort of one-way data binding where loading is better supported than storing. In fact, ADO.NET data binding does not support server cursors, nor does it support special two-way binding between in-memory structures and database columns with automatic updates. One of the consequences of this is the lack of a general and reliable mechanism to detect the unique ID of auto-increment columns. Either you get this functionality through an extra connection to the database server, or you resort to GUID columns whenever the database makes them available. A GUID value is algorithmically proven to be unique in time and space, and more important, can be determined by the client.

Now you have a complete, annotated description of almost all the DataGrid control functionalities. Chapter 5 is about techniques for effective ASP.NET programming, with a particular emphasis on reusability and custom controls. I’ll return to grids in Chapter 6, when I illustrate practical approaches to building really advanced reports.


Code Reusability in ASP.NET

Part II
Smart and Effective Data Access and Reporting
Chapter 5
Code Reusability in ASP.NET

So far in this book, we’ve built complex pages that employ sophisticated DataGrid and interoperable controls. Although the .NET Framework is a slim and elegant hierarchy of classes for building Web projects, many of you will end up with unwieldy blocks of code and layout information, and when the complexity of a software component grows beyond a certain threshold, reusability and encapsulation become serious issues. Years ago, someone coined a phrase to describe this programming phenomenon: spaghetti code.

Originally, spaghetti code referred to the messy tangle of lines of BASIC that resulted from unsparing use of GOTO statements and a modular programming style. It’s also an apt description of unstructured, overly complex ASP pages. One key to the ASP technology’s rapid adoption is the immediate interpretation and execution of the <%...%> script code within the flow of an ASP page. However, in the long run, it has also been the determining factor preventing the technology from scaling properly to meet the needs of sophisticated real-world projects.


Layers of ASP.NET Pages

ASP.NET certainly doesn’t encourage spaghetti code. It is rigorously object-based and builds atop a family of easily extensible server-side controls. The structure of an ASP.NET page is extremely modular, with three distinct sections—page directives, code, and page layout.

  • Page directives Page directives set up the environment in which the page will run, specify how the HTTP run time should process the page, and determine which assumptions about the page are safe to make. Directives also let you import namespaces to simplify coding, load assemblies not currently in the Global Assembly Cache (GAC), and register new controls with custom tag names and namespace prefixes.

  • Code section The code section, with the misleading tag name of <script>, includes any source code that is pertinent to the page. You typically place event handlers and helper routines in this code section, which by itself can be extremely long. It can even grow pretty long in toy projects that do little more than say hello to the world.

  • Page layout The page layout represents the skeleton of the page, which you flesh out a bit with code.

As you can see, the structure of an ASP.NET page resembles lasagne rather than spaghetti. You have three noodles with rich sauce and various meats and <script> cheeses in the middle. While extremely tasty, however, lasagnes are not ideal for healthy eating. They are pretty dense in calories and end up leaving a significant memory footprint in your digestive run time system.

From Spaghetti Code to Lasagna Code

If you’ve ever served a good dish of lasagna, you know that the taste lies not in the number of noodles but in what you find between them. The directive noodle is relatively lightweight, as is the layout noodle. Both noodles are necessary for holding the ASP.NET lasagne together.

The code noodle is what determines, for the most part, the nutrition and the calories of the dish you are serving. A very lengthy code section can make your ASP.NET page very hard to manage, nonmodular, and prone to code duplication across the pages of the project. This trend grows linearly with the size of the project, and it soon reaches the critical threshold that triggers unexpected reactions. Keeping the code noodle as thin as possible is vital to the health of your application.

The layout noodles, too, can be kept somewhat thin. My recommendations here are not so much to enhance raw performance, as any ASP.NET tag maps to a run-time control that relies on compiled and managed code. Rather, my focus is more on design. Slim, modular, even stateless programmable components are much easier to manage. More components means task pooling between the members of the team. And separation between noodles means that each single cook can prepare his or her preferred sauce using the finest ingredients.

Slimmer Pages

There are basically two ways to make the ASP.NET lasagna pages more easily digestible: you can take the whole code section out of the ASP.NET page, and you can try to simplify the page layout by grouping individual controls in a new, single, wrapped-up composite control, which you would embed in the page as a user control. Smarter and more effective ASP.NET pages rely heavily on pieces of extensible and reusable code. In ASP.NET, the demand for reusable code finds three possible answers: the code-behind approach, user controls, and custom Web controls.


The Code-Behind Approach

The primary goal of the code-behind file is to enable you to write more modular code so that you can keep the code and layout physically separate. By keeping the code and layout separate, you can exploit the native object orientation of the .NET Framework to build a hierarchy of page classes that provide more and more specialized functions. As a result, you also have a little bit of page-level reusability, although you can’t reuse any elements of the layout.

With code-behind, you store directives and the page layout in one .ASPX file, and the code for them in a separate file written in Microsoft Visual Basic or Visual C#. The link to the code-behind file is defined by using an attribute of the @ Page directive.

<%@ Page Language="C#" Inherits="MyBasePageClass" Src="MyBasePage.cs" %>

When users point their browsers to the .ASPX page, the code-behind class file runs and dynamically produces the output for the page. When the page is requested, the code-behind file is loaded and treated as an integral part of the page.

A practical advantage of this approach is that it allows two programmers or programming teams to work on the development of a Web Forms page simultaneously. After the structure of a page is defined in terms of the number, names, and types of the constituent server controls, HTML designers can work on the appearance of the .ASPX file, determining the position of the controls, their colors and fonts, and even the client-side scripting. At the same time, Visual Basic and C# programmers can develop the code and the event handlers required for the page to work properly and fulfill user’s expectations.

note | Visual Basic 6 uses the same approach to form development. In Visual Basic 6, a form is divided into two distinct parts: the graphical layout, and the code that interacts with controls. However, this logical distinction is lost when the form serializes to disk, because you always have a single FRM file with both code and layout information. The Visual Basic 6 integrated development environment (IDE) lets you see code and layout independently but does not allow different people to work on the same form at the same time.
Enabling Code-Behind

You enable the ASP.NET code-behind feature by adding a couple of attributes, namely Inherits and Src, to the @ Page directive.

<%@ Page Language="C#" Inherits="MyBasePageClass" Src="MyBasePage.cs" %>

The Inherits attribute contains the name of the code-behind class from which the current page inherits. The code-behind class can be any class derived from the Page class. The Src attribute represents the URL of the source file defining the class. When the ASP.NET HTTP run time processes a page, it first analyzes the @ Page directive. If Src is present, the code-behind file is loaded and compiled. Then the run time uses the contents of Inherits to get the name of the class to instantiate so that it can serve the ongoing page request.

Writing Code-Behind Pages

Any existing ASP.NET page can migrate to code-behind with very few changes. First you must write a class that inherits from Page. Next you add to the class, as a protected or public member, any control declared in the page layout that has the runat attribute set to server. Also, convert any global variable or procedure in the code section of the original ASP.NET page into a new member of the class with the scope and visibility you desire.

caution | In the layout of an ASP.NET page, you declare controls using tag names. For those controls that you want to manipulate programmatically, when you switch to code-behind, you have to choose the control type and they must be non-private members of the class. You have to use the correct control class behind a given tag, for example, HtmlImage for <img>.

The following ASP.NET page has no <script> tag, yet it works. Figure 5-1 shows the page.

<%@ Page Inherits="BWSLib.CodeBehindPage" Src="CodeBehindPage.cs" %>
<html><title>Code-behind Page</title>
<body bgcolor="ivory" style="font-family:arial;font-size:small">
<form runat="server">
    <b><asp:label runat="server" Text="First Name"/></b>
    <asp:textbox runat="server" id="fname" />
    <br>
    <b><asp:label runat="server" Text="Last Name"/></b>
    <asp:textbox runat="server" id="lname" />
</form>
</body>
</html>
Figure 5-1
A simple code-behind page.

The behavior of the page is determined by the code-behind file. The following code shows the code-behind file for Figure 5-1:

namespace BWSLib
{
    using System;
    using System.Web.UI;
    using System.Web.UI.WebControls;

    public class CodeBehindPage : Page
    {
        protected TextBox fname, lname;
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            if (!IsPostBack)
            {
                // Set the style for the text boxes
                fname.Style["font-family"] = "verdana";
                fname.Style["border"] = "solid 1px black";
                fname.Style["background-color"] = "beige";
                lname.Style["font-family"] = "verdana";
                lname.Style["border"] = "solid 1px black";
                lname.Style["background-color"] = "beige";
            }
        }
    }
}

Notice that the text box controls have been declared as protected members of the class to make them programmable within the class. Notice also the role of the base keyword that lets you invoke a method on the parent class.

note | The base keyword is used in C# to access members of the base class from within a derived class. In Visual Basic .NET, you use the keyword MyBase.

Using code-behind does not mean that you have to renounce the <script> tag in the .ASPX file. Code-behind has two pleasant side effects. First, it allows you to isolate in a separate component all the boilerplate code for the page. Second, you can use different languages to build the page, and the languages used to write the code-behind file and the inline <script> tag can be different, as the following script and Figure 5-2 show:

<script language="VB" runat="server">
Public Sub Page_Load(s As Object, e As EventArgs)
    fname.Text = "Dino"
    lname.Text = "Esposito"
End Sub
</script>
Figure 5-2
This page was created using code-behind with the <script> tag.

By choosing the code-behind approach, you can control the base class of the page and also make it automatically inherit from previously compiled classes. This is the first step toward visual inheritance for ASP.NET pages.

Visual Inheritance for ASP.NET Pages

Let’s consider the following rather minimal Web page, which defines only a title and the code-behind information for Figure 5-3:

<%@ Page Language="C#" Inherits="BWSLib.MyBasePage"
     Src="MyBasePage.cs" %>
<html>
<title>Page Title</title>
<body bgcolor="ivory" style="font-family:arial;font-size:small">
</body>
</html>
Figure 5-3
In spite of an empty body, the page displays the output of some controls.

The output you see in Figure 5-3 is produced by controls that operate behind the curtain of the code-behind file. Let’s look at the source code:

public class MyBasePage : Page
{
    public Label TopBarTitle;
    private Label lblURL;

    protected override void OnInit(EventArgs e)
    {
        // Use AddAt() to make sure these controls are added
        // at the top of the page. This simply serves the
        // purposes of this demo and is not a general rule.
        Controls.AddAt(0, new LiteralControl("<h2>"));
        TopBarTitle = new Label();
        TopBarTitle.Text = "Top bar title";
        Controls.AddAt(1, TopBarTitle);
        Controls.AddAt(2, new LiteralControl("</h2>"));

        Controls.AddAt(3, new LiteralControl("Current path: "));
        lblURL = new Label();
        lblURL.ForeColor = System.Drawing.Color.Blue;
        lblURL.Font.Size = FontUnit.Small;
        lblURL.Text = Request.Url.ToString();
        Controls.AddAt(4, lblURL);

        Controls.AddAt(5, new LiteralControl("<hr>"));

        // Call the page's OnLoad handler
        base.OnLoad(e);
    }
}

The MyBasePage class has a public Label control representing the page’s top bar and a private Label control displaying the current URL. During the OnInit event, the class creates the tree of controls it needs. Next the class adds these child controls to the page’s Controls collection. Finally the parent page’s OnLoad event is called so that any code in the page’s Page_Load handler is run.

Deriving an .ASPX page from the MyBasePage class gives you a kind of visual inheritance because you automatically inherit the page structure built by the base class. Public properties are programmable. For example, the following code added to the previous .ASPX code produces Figure 5-4:

<script runat="server">
public void Page_Load(Object sender, EventArgs e)
{
    if (!Page.IsPostBack)
    {
        TopBarTitle.Text = "This is my page";
    }
}
</script>
Figure 5-4
This page structure was created with code inherited from the base class.


The Code-Behind Approach

Embeddable Web Forms

User controls represent another level of page reusability and turn out to be more employable because of their smaller granularity. With user controls, the reusability applies to input forms rather than to whole pages. User controls are also referred to as pagelets in some existing documentation. I’ll be using both terms interchangeably.

A user control can be seen as an embeddable Web form. You define a user control in much the same way you create a Web Forms page. When you’re finished with code and layout, you give the resulting file an ASCX extension. You can use the control with any ASP.NET page. Pages see the control as a programmable component and work with it in the same way they work with any other system Web control. Are you familiar with Dynamic HTML (DHTML) scriptlets? User controls are just their ASP.NET counterparts.

Writing User Controls

Like a Web Forms page, a user control has layout information and code blocks. The layout represents the user interface of the component—say, the controls making the form. The internal code ties together the various constituent controls and implements their business logic. User controls can expose methods, properties, and events. The following code demonstrates a simple yet effective pagelet:

<%@ Control ClassName="LabelTextBox" Language="C#" %>
<script runat="server">
    public Label TheLabel {
        get {return lblTheLabel;}
    }
    public TextBox TheTextBox {
        get {return txtTheTextBox;}
    }
</script>

<asp:label runat="server" id="lblTheLabel" font-bold="true"
    style="font-family:verdana" />
<asp:textbox runat="server" id="txtTheTextBox" backcolor="ivory"
    style="font-family:verdana" />

The user control defined by the preceding code is composed of a label and an accompanying text box control. Each control implements some style information and has a unique ID. In the code section, you find two public properties: one wrapping the label control and one wrapping the text box. These constituent controls are exposed as read-only, but callers can set any of their properties.

note | To expose any of the constituent controls as programmable objects, you must use a distinct public property. All controls in the pagelet’s layout are inaccessible to external callers because of their class protection level. To work around this, you define a public property (preferably as read-only) for each control you want to expose. Make sure that properties have names different from the IDs of the controls they mirror. Otherwise you will get a compile-time error because of member names duplication.

Let’s see how to use the pagelet in an ASP.NET page. To start off, you assign the new user control a tag and a namespace prefix within the hosting page. The @ Register directive lets you do this. The tag name and the prefix will be used to insert an instance of the user control in the page. Figure 5-5 shows the results.

<%@ Register TagPrefix="expo" TagName="LabelBox"
     Src="labelbox.ascx" %>
<html>
<title>Testing the LabelBox Pagelet</title>
<body>
    <form runat="server">
        <expo:labelbox runat="server" id="thePagelet" />
    </form>
</body>
</html>

The LabelBox user control is made of a label and a text box control, both of which are programmable. You can access them as follows:

<script language="C#" runat="server">
public void Page_Load(Object sender, EventArgs e)
{
    thePagelet.TheLabel.Text = "This is the label";
    thePagelet.TheTextBox.Text = "This is the text";
}
</script>
Figure 5-5
A page containing a simple user control.

Codewise, the <expo:labelbox> control works like a Web control but, in terms of structure and layout, it is more like an embedded and programmable form.

Programming User Controls Declaratively

When the public properties of a user control map to simple data types (for example, to strings and dates but not to controls or user-defined types), you can easily set the properties through the attributes of the tag that represents the control:

<expo:labelbox runat="server" id="theLabelBox" Property="simplevalue" />

Let’s enhance the user control like this:

<%@ Control Language="C#" %>
<script runat="server">
    public String LabelText="";
    public String EditText="";

    public Label TheLabel {
        get {return lblTheLabel;}
    }
    public TextBox TheTextBox {
        get {return txtTheTextBox;}
    }
    public void Page_Init(Object sender, EventArgs e)
    {
        txtTheTextBox.Text = EditText;
        lblTheLabel.Text = LabelText;
    }</script>
<asp:label runat="server" id="lblTheLabel" font-bold="true"
    style="font-family:verdana" />
<asp:textbox runat="server" id="txtTheTextBox" backcolor="ivory"
    style="font-family:verdana" />

Two more public properties are defined, LabelText and EditText. They were created to accept text for the label and the text box. The values read from these properties are used within the control’s Page_Init event to initialize the Text property of both constituent controls. You can now set the desired text in the calling ASP.NET page through declarative attributes.

<expo:labelbox runat="server" id="thePagelet"
    LabelText="Label" EditText="Text" />

What happens when the calling page also handles the Page_Load event? Any change entered at that time that affects the user control overrides the current state:

public void Page_Load(Object sender, EventArgs e)
{
    thePagelet.TheLabel.Text = "This is the label";
    thePagelet.TheTextBox.Text = "This is the text";
}

So with the user control we just created, the last word on the state of the constituent controls is up to the page’s Page_Load event handler.

Coding ASP-Style Code Blocks

Unless you need server-side manipulation of the control, using HTML tags that are not marked with the runat attribute results in faster code because the HTTP run time does not have to process the tag. Typically you can apply this simple optimization rule to label controls, because, in the case where the label text doesn’t change, using a label control simply does not make sense. Let’s see how this consideration applies to pagelets.

When you don’t plan to use any Label control property other than Text, you can replace the <asp:label> control with a plain old <span> tag to greater effect. In this case, how would you display the current text? You would use ASP-style code blocks. Replace the Label control in the preceding pagelet with the following:

<span style="font-family:verdana;font-weight:bold">
<%=LabelText%></span>
Loading Pagelets Programmatically

Just as you can programmatically create an instance of an ASP.NET server control, you can do the same with a pagelet. The pagelet’s method that allows you to do this is LoadControl. A pagelet is dynamically wrapped in a class whose name is determined by the ClassName attribute of the @ Control directive. User controls that enable the programmatic creation of their instances include the following directive:

<%@ Control ClassName="MyControlClass" Language="C#" %>

The hosting page must reference the control through the @ Reference directive. It indicates via a declaration that a user control should be dynamically compiled and linked with the page.

<%@ Reference Control="Message.ascx" %>

The following code shows how to create a dynamic instance of one pagelet. The full source code for the PageletDynamicBinding.aspx application is available on the companion CD.

Message statusbar;
public void Page_Load(Object sender, EventArgs e)
{
    Control ctl = Page.LoadControl("Message.ascx");
    statusbar = (Message) ctl;
    Page.Controls.Add(statusbar);
}
public void OnConnect(Object sender, EventArgs e)
{
    statusbar.Color = "red";
    statusbar.Text = "<b>User Connected: </b>" +
        lname.Text + ", " + fname.Text;
}

The @ Reference directive allows you to use the user control’s class name in the body of the page. If the reference is missing, you cannot employ strongly typed code. In other words, you can manipulate the pagelet only through the generic programming interface of the Control class. You can display the control in the page but you cannot code with it.

Preparing to Share User Controls

When you want to share your ASP.NET page functionality with one or more applications, you need to make a few moves to transform the page into a reusable and programmable user control.

First make sure that the pagelet has none of the following tags: <html>, <body>, or <form>. Because the layout of the pagelet will be merged with the layout of the hosting page, you want to avoid harmful and unnecessary nesting, which these tags can cause. Second, after all unnecessary HTML tags are removed, rename the file with an ASCX extension. This renaming is key to enabling a special treatment of the file. Finally, if the page you’re converting into a pagelet contains the @ Page directive, change it to the @ Control directive.

The @ Control and @ Page directives are nearly identical. The only significant difference between them is that @ Control does not support the Trace attribute. If you need tracing, enable it at the page level and it will automatically be extended to any child control. The @ Control directive allows you to use different languages for the pagelet and the hosting page. For example, you can use Visual Basic in the pagelet and C# in the calling page. Using the ClassName attribute for controls is not strictly necessary but is highly recommended.

Creating User Controls in Code-Behind Files

You can use code-behind files to create user controls, not just ASP.NET pages. The technique for creating both is the same except for a few minor details. To bind the external file, you use the @ Control directive instead of @ Page.

<%@ Control Language="C#" Inherits="BWSLib.MyControl" Src="MyControl.cs" %>

As you can probably guess, the base class of code-behind user controls cannot be Page, as it is for ASP.NET pages. User controls will inherit from UserControl. Furthermore, when you declare the ID attribute for each server control in the layout, be sure ID matches the name of the instance that you create for it in the code-behind file. For example, suppose you have the following control in the ASCX file:

<asp:textbox id="Name" runat="server"/>

The code-behind file must have a declaration such as the following. The public qualifier is arbitrary, so you can change it.

public TextBox Name;
Creating a New DateBox Control

ASP.NET supplies a calendar control that I used in Chapter 4 to edit a date field. The main drawback of this control is that it requires a round-trip each time the user clicks it to select a day or a month. The Calendar control is certainly useful, especially to display a read-only date, but to enter or update a date you might want to employ a simpler and equally effective interface.

Writing a DateBox user control that lets the user type in a date is not particularly difficult. The layout of the DateBox control comprises three text boxes representing the month, day, and year, and they are separated with literals representing the date separator.

<asp:textbox runat="server" id="txtMonth" columns="2" maxlength="2"
    style="font-family:verdana;border:0px; " />
<%=Separator%>
<asp:textbox runat="server" id="txtDay" columns="2" maxlength="2"
    style="font-family:verdana;border:0px;" />
<%=Separator%>
<asp:textbox runat="server" id="txtYear" columns="4" maxlength="4"
    style="font-family:verdana;border:0px;" />

The text boxes use standard styles and expose background and foreground colors as programmable properties. Table 5-1 shows the full list of DateBox control’s properties.

Table 5-1 Properties of the DateBox Control
Properties | Description
BackColor | Gets or sets the background color for the constituent text boxes.
ForeColor | Gets or sets the foreground color for the constituent text boxes.
SelectedDate | Gets or sets a string that represents a valid date.
Separator | Gets or sets the character used as the date separator.

The DateBox user control also fires the DateChanged event whenever the user changes the currently displayed date.

Property Accessors

Some of the properties in Table 5-1 are exposed by using public read-write members, so users can read and write them directly without the mediation of get and set accessors.

public String Separator = "/";
public String BackColor = "white";
public String ForeColor = "black";

Property accessors are small pieces of code that a control executes internally when a given property is read or set. The get accessor governs the retrieval of the current value of the property. The set accessor manages the way in which a given value is assigned to the property. When you omit the get accessor, the property becomes write-only. When you omit the set accessor, the property is read-only. As shown in the following code, the DateBox control uses accessors only for the SelectedDate property:

public String SelectedDate {
    get {
        return m_date.ToString("MM" + Separator + "dd" +
            Separator + "yyyy");}
    set {
        try {m_date = Convert.ToDateTime(value);} catch {}
        RefreshDate();
        // Raise DateChanged event here
    }
}

When the SelectedDate property is read, the currently set date is formatted into a mmddyyyy string with the current separator. When a user attempts to assign a value to SelectedDate, the input string is converted to a valid date object. Next the string gets stored internally in the m_date private member, which contains information about the current date as a DateTime object. The set accessor is responsible for refreshing the control’s user interface to reflect the new date. Figure 5-6 shows the DateBox control in action.

Figure 5-6
The DateBox control in action.
Some Special Effects

Each text box in Figure 5-6 is borderless. I created this effect by using an explicit CSS style, but you can use the BorderSize property of the ASP.NET TextBox control to achieve the same look. Consider that not all browsers provide good support for CSS styles, so unless the user is using Internet Explorer 5 or a later version, he can expect the UI to appear different. The following figure shows how Figure 5-6 might look in other browsers.

HTML provides tooltips for several tags through the title attribute. Be aware that not all browsers, especially old browsers, support the title attribute. For example, IE 2 and Netscape browsers, prior to version 6, do not support it.

The width of the text boxes is expressed in characters. You set it using the Columns property on the TextBox control, which evaluates to the HTML Size attribute. If the browser-specific implementation of the text box allows you to indicate the font in use, the actual size of the textbox is calculated using the maximum width of the font’s characters. So it happens that, say, “ww” fits perfectly in the text box width while “ii” leaves a lot of trailing blank space. To avoid that, use a fixed-size font like the Courier New—which is also the only font accepted by text boxes in some downlevel browsers.

Notice in this figure that you can give a link button a special symbol such as the Down Arrow (C) character instead of normal text by using text strings rendered with the Webdings font family. The C character corresponds to a text string of "6". The A symbol corresponds to "q".

The A link button causes the control to update the internal state when the date is manually edited. The update properly formats the elements of the date (that is, it appends a leading 0, if needed) and refreshes the internal members, including the child calendar control.

A Child Calendar

When the user clicks the C button, a child Calendar control appears, letting the user select the new date. The Calendar control is a native part of the user control’s layout but is initially hidden.

<asp:calendar runat="server" id="myCalendar" Visible="false"
    BackColor="white" ForeColor="black"
    Font-Name="verdana" Font-Size="9px"
    TitleStyle-BackColor="#33ddff"
    TitleStyle-ForeColor="black"
    TitleStyle-Font-Bold="True"
    SelectorStyle-BackColor="#99ccff"
    SelectedDayStyle-BackColor="Navy"
    SelectedDayStyle-Font-Bold="True"
    DayHeaderStyle-Font-Bold="True"
    OnSelectionChanged="SelectionChanged" />

The Calendar control exposes a SelectionChanged event that fires whenever the date changes. By hooking this event, the control updates the text boxes when the user selects a new date through the calendar. The calendar’s visibility attribute is toggled on and off by clicking the Down Arrow (C) button. The Calendar and TextBox controls are placed on two distinct rows of an all-encompassing table, as shown in Figure 5-7.

Figure 5-7
The DateBox user control with the calendar open. The top picture shows what you see through IE 6.0. The bottom picture displays the output of an old version of Netscape Navigator.
note | The DateBox control does not validate any data entered by the user, which can result in run-time errors when values do not evaluate to dates. You can implement active error checking by using try/catch blocks. You can implement passive error checking—that is, warning the user about the error—by using validator controls.
Firing Events

In addition to defining properties, user controls can define methods and fire events. To define a method, you simply define a public procedure. Exposing an event is a bit trickier.

When exposing an event, the first question you should ask yourself is whether you plan to return event-specific data. The passing or not passing of data influences the signature of the event handler that both the control and its clients will be using to process the event. If you don’t pass data, you can use the standard event handler EventHandler. In this case, a public event named DateChanged is declared as follows:

public event EventHandler DateChanged;

You also need a helper function that calls the user-defined event handlers from within the control. This normally takes the form of the following:

protected virtual void OnDateChanged(EventArgs e)
{
    if (DateChanged != null)
        DateChanged(this, e);
}

During the control’s activity, when the situation described in the event occurs, the code invokes the user-defined handler as follows:

OnDateChanged(EventArgs.Empty);

In .NET, events are driven by delegate types—a kind of type-safe function pointer. Delegates describe the signature of the functions you write to handle a certain event. The prototype shown here identifies the standard event handler.

public delegate void EventHandler(Object sender, EventArgs e);
Using Custom Event Arguments

An event such as DateChanged isn’t very useful without passing made-to-measure data—for example, the old date, the new date, and a Boolean flag denoting whether the data change occurred via typing on the keyboard or clicking the calendar buttons. When custom data is involved, you define an ad-hoc data structure that simply extends the base class EventArgs. Take a look at this example, which defines the DateChangedEventArgs class:

public sealed class DateChangedEventArgs : EventArgs
{
    public bool FromCalendar;
    public DateTime OldDate;
    public DateTime NewDate;
}

A new delegate type using this data structure is also needed. Let’s call it DateChangedEventHandler.

public delegate void DateChangedEventHandler(
    Object sender,
    DateChangedEventArgs e);

Finally, the event exposed by the control must be declared as follows:

public event DateChangedEventHandler DateChanged;

The event is raised in two circumstances, namely in response to the calendar’s SelectionChanged event and when the text boxes are refreshed to reflect a new date. The following code shows how I handle changes in the calendar, bubbling a slightly modified event up to the calling page:

private void SelectionChanged(Object sender, EventArgs e)
{
    // Store the new date
    DateTime dtNew = myCalendar.SelectedDate;

    // Create and fill the argument's data structure
    DateChangedEventArgs dce = new DateChangedEventArgs();
    dce.FromCalendar = true;
    dce.OldDate = m_date;
    dce.NewDate = dtNew;

    // Refresh the control's user interface
    m_date = dtNew;
    RefreshDate();

    // Fire the event
    OnDateChanged(dce);
}

Notice that the SelectionChanged event of the Calendar class does not use a custom structure for arguments. As you can see in the preceding code, SelectionChanged still relies on EventArgs. So when do you really need a custom structure? And what data should you pass using it?

A client page handling events can still access the control that raised the event and instantly read the value of public properties. So you should avoid passing as event arguments any information that is already available through public properties and methods. The typical data used as event arguments is any information that is potentially useful but not accessible otherwise, for example, information that is stored through private members, or volatile information that the control does not cache and expose in any way.

Applying these rules to the DateChanged event, you notice the caller has no way to know about how the change was brought about. So the FromCalendar field is really an added value. The same can be said for OldDate but not necessarily for NewDate, because the new date can be accessed—at least as a string—by using the DateBox control’s SelectedDate property.

note

If your class raises multiple events, you can optimize the way they are stored by using event properties instead of event fields, as shown so far in this chapter. For event fields, the compiler generates one field per instance of an event delegate, which might not be acceptable for a large number of infrequently used events. Event properties consist of event declarations accompanied by event accessors.

public event DateChangedHandler DateChanged {
add { Events.AddHandler(eventDateChanged, value); }
remove { Events.RemoveHandler(eventDateChanged, value); }
}

When multiple clients register to handle the same event, all their names will be stored in the eventDateChanged structure:

private static readonly object eventDateChanged = new object();

For more information and samples about event handling, check out the MSDN documentation.
Handling Events from Pages

The following code shows how to hook up the DateChanged event in a client ASP.NET page:

<expo:datebox runat="server" id="theDateBox"
    separator="-"
    selecteddate="9/14/2001"
    ondatechanged="Date_Changed" />

The event handler below writes a line describing how the date changed.

public void Date_Changed(Object sender, ASP.DateBox.DateChangedEventArgs e){
    String msg;
    msg = String.Format("<br>Date has been changed from <b>{0:d}</b>" +
                "to <b>{1:d}</b> through <b>{2}</b>",
                e.OldDate,
                e.NewDate,
                 (e.FromCalendar ?"the calendar." :"manual editing."));
    info.Text += msg;
}

You could omit the ASP namespace by importing it at the page level:

<%@ Import Namespace="ASP" %>

The actual signature of the page event handler deserves a few comments. In this sample application, I defined the DateChangedEventArgs class in the body of the DateBox user control—the file DateBox.ascx—which makes DateChangedEventArgs available only as a member of the DateBox class. If you try to use the following, perhaps more natural, signature, you get a compiling error:

public void Date_Changed(Object sender, DateChangedEventArgs e)

An exception is thrown because DateChangedEventArgs is not defined in or linked to the ASP.NET page. The error message hints that you missed an assembly. At this point you can do one of two things. You can place the class definition in a distinct module, compile it to an assembly, and reference the assembly in both the page and the user control. Alternatively, you can leave the class definition in the user control’s body but find a way to let the page know about the class.

The ClassName attribute set in the @ Control directive assigns the name of DateBox to the user control. All user controls—that is, all the ASP.NET elements referred to by using ASCX files—belong to a namespace named ASP. The full name of the DateBox class, then, is ASP.DateBox. If the DateChangedEventArgs class is defined within ASP.DateBox, the only fully qualified name for DateChangedEventArgs is ASP.DateBox.DateChangedEventArgs. You cannot shorten this name by importing ASP.DateBox as a namespace. The reason is quite simple: ASP.DateBox is a class, not a namespace. Instead, you can import ASP as a namespace and resort to the slightly shorter class name of DateBox.DateChangedEventArgs. Figure 5-8 shows the final effect of the event handler in a sample page. The full source code for the DateBox.ascx and TestDateBox.aspx applications is available on the companion CD.

note | If you omit the ClassName attribute in the @ Control directive, the class name of the user control takes a standard form. If the filename is DateBox.ascx, the class name is DateBox_ascx. Consequently, the full name of the event class is ASP.DateBox_ascx.DateChangedEventArgs.
Figure 5-8
Intercepting the DateChanged event fired by the DateBox user control.


Writing Custom Controls

User controls are good at prototyping new controls that have a complex user interface. They are the perfect solution when you need to combine two or more controls. As long as the structure of the new controls is stable during the lifetime of the program, user controls offer you an excellent mix of programming ease and productivity. The architectural principle lying behind a user control is aggregation rather than inheritance. If you need only an enhanced or stripped-down version of an existing control, you are better off writing a new class that inherits from it.

A Web control has varying degrees of complexity. You can write a simple Web control that outputs HTML code, but you can also write a more complex composite control that dynamically creates and lays out a tree of child controls. You can also devise templated controls, which let you specify user-defined ASP.NET templates so that you can populate parts of the user interface.

All ASP.NET controls can be extended through inheritance. Depending on how complex you want the new control to be, this extension can be extremely simple.

A Labeled TextBox Control

Earlier in this chapter, I built a user control made up of a label and a text box that you could independently program. To make it fully programmable, I exposed both the label and the text box as public properties of the Label type and TextBox type, respectively. If you want your users to have total control of the child elements, this approach works just fine. When you want to implement finer control on the properties of the constituent label and text box controls, you must understand that user controls have a structural limitation: constituent elements are aggregated in the all-encompassing container control. Constituent controls can either be exposed as-is as a property on the parent control or be completely hidden by the parent control. In the latter case, a child control could expose one or more of its properties through proxy properties defined on the parent control, as shown in Figure 5-9.

Figure 5-9
Exposing constituent control properties within a user control.

If you expose constituent elements of the user control, you cannot filter the child control’s programmability any longer on a control-by-control basis. By contrast, if you don’t expose child controls, you have to explicitly define any element of the interface as a distinct property. Basically, you’re faced with rebuilding a brand new programming interface rather than extending or enhancing the existing one. Let’s rewrite the LabelBox control as a custom control, that is, a class that inherits from TextBox.

note | The LabelBox control is made up of a label and a text box control situated side by side. So, at least in principle, you could build your interface starting with Control as well as Label or TextBox. No syntax rule suggests that one of these alternatives is the correct one. It is mostly a matter of preference and vision. In my implementation, I consider the LabelBox control as a sort of extended text box and build it using only the TextBox class.
public class LabelBox : TextBox
{
    public LabelBox()					{}
    public LabelBox(String strLabel, String strText)
    {
        Label = strLabel;
        Text = strText;
    }
    public String Label = "";
    public String LabelCssClass = "";
    public Boolean LabelOnTop = false;

The preceding code snippet shows the two constructors of the new class, one of which takes no argument. The LabelBox control has three properties: the text, the CSS style of the label, and a Boolean flag denoting whether the label has to be rendered above the control or next to it. A private instance of the Label class represents the label, which is created and configured in the override of the CreateChildControls method.

private Label m_lbl;
protected override void CreateChildControls()
{
    m_lbl = new Label();
    if (LabelOnTop)
        m_lbl.Text = Label + "<br>";
    else
        m_lbl.Text = Label + "&nbsp;&nbsp;";
    m_lbl.Font.Bold = true;
    m_lbl.CssClass = LabelCssClass;

    // Set standard styles for the text box
    this.Font.Name = "verdana";
    this.Font.Size = FontUnit.XSmall;
    this.Style["border"] = "solid 1px black";
}

The control renders itself through the Render method. In the implementation of this method, the new control outputs any HTML code that forms its user interface. In our example, you should render the label control and call the Render method of the base class.

protected override void Render(HtmlTextWriter writer)
{
    m_lbl.RenderControl(writer);
    base.Render(writer);
}
Using Custom Controls

A custom control is a compiled element that you register with pages by using assemblies. You use the @ Register directive and are required to specify the assembly name (without an extension) and the namespace. You don’t have to indicate a tag name because this defaults to the class name. You indicate instead the namespace prefix:

<%@ Register TagPrefix="expo" Namespace="BWSLib"
    Assembly="BWSLib.Controls.LabelBox" %>

The control’s declaration in the body of an ASP.NET page looks nearly identical to the declaration of a standard control or user control:

<expo:LabelBox runat="server"
    Label="Inline Label"
    Text="esposito"
    LabelTop="true"
    LabelCssClass="StdText" />

Because the LabelBox control derives from TextBox, you can use any of the base properties in addition to your custom ones.

note | Although you need a compiled assembly to deploy a custom control and a simple text file to deploy a user control, there is no relevant difference in performance between the two. User controls are compiled on the fly and then cached, so after the first use they can offer the same performance as other controls. The big difference between them is their design: user controls exploit aggregation whereas custom controls are truly object-oriented and leverage inheritance.

Figure 5-10 shows the LabelBox control in action in a sample page. The full source code for the LabelBox.cs and TestLabelBox.aspx applications is available on the companion CD.

note

When writing a custom control, you can override not only methods of the parent control but also properties. For example, you can override a property to make it read-only or to enhance its standard behavior. The following code shows how to modify ReadOnly to make it change the text box’s background when set:

public override bool ReadOnly {
    get {return base.ReadOnly;}
    set {base.ReadOnly=value;
        BackColor=(value ?Color.White :Color.Beige;)} }
Figure 5-10
Two LabelBox controls in action.
An Off-the-Shelf DataGrid Control

In the previous chapters, I illustrated a variety of the DataGrid control’s features, including pagination, sorting, templates, and in-place editing. When you are writing code and the DataGrid control is involved, in most situations you need to handle the events raised by the control. You do this by writing C# or Visual Basic code to flesh the <script> section out (the so-called code noodle that I mentioned earlier in this chapter when discussing ASP.NET lasagne code).

As you know, the objective of the DataGrid control is to show and sort pages of records that are formatted in columns of data. In spite of this declared intention, you still have to write several lines of code. Why? Because we are talking for the most part about Web controls. A Web control is a software component that springs to life when the ASP.NET page is processed on the Web server by a special module, the HTTP runtime. A Web control generates output composed of HTML tags and elements. The user’s client interacts with the HTML output of the Web controls. When an event occurs on the client (for example, a new grid page is requested), the page is posted back to the server. Next, the HTTP runtime restores the previously saved state, gives Web controls a chance to make this state consistent with their further expectations, and resumes server-side processing. When the DataGrid control is the Web control in question, a number of issue arise:

  • For performance reasons, the grid’s data source is not persisted across multiple requests of the same page.

  • No information about the current sort expression is stored.

  • No information about the columns that actually form the grid is stored.

As a result, to have a grid that moves through pages and sorts by columns, you have to write code every time you do the following:

  • Retrieve and bind the data source.

  • Sort the data source.

  • Set the new page index.

In most cases, the code you need is boilerplate code, but you do still have to write it or, even worse, cut and paste it from existing pages. When you need to consider reusability, the natural question is, why not embed all or part of this code you’ve written into a new, wrapped-up, ad-hoc DataGrid control? The answer is, why not?

No architectural or design issues prevent you from implementing an enhanced grid, but you should be aware of some critical decisions you must make. One of these decisions is particularly important: how you retrieve, and possibly sort, the data. As discussed in Chapter 2, scalability is a serious issue for Web applications, so you’ll need to answer the retrieval and sorting question based on the specific features of your application. You might want to cache the data source in Cache or Session rather than using XML files. You might also decide that, all in all, you can afford reloading data page by page by using super-optimized stored procedures. If your requirements are to display the most current data, you can even employ ADO server cursors to provide an up-to-date view of data at any time. You can see that there is more than one right way to store and retrieve data. What works for desktop applications does not work as well over the Web. That’s why the DataGrid Web control has been designed with such a flexible programming interface.

caution | The DataGrid Web control is radically different from the DataGrid Windows control. They belong to distinct namespaces: System.Web.UI.WebControls for the Web control and System.Windows.Forms for the Windows control. They have similar programming interfaces, but they are not identical or even functionally equivalent. The Windows Forms control features more advanced characteristics that have not been implemented in the Web’s counterpart. To accomplish the same task with both controls, you often must use different code. Bear this in mind when digging into the documentation.

All the aforementioned considerations do not necessarily lead to the conclusion that code-behind is the only way to lighten the code burden for an ASP.NET page based on the DataGrid control. However, to write an enhanced, ready-to-use version of the DataGrid control, you must decide how to solve the three issues just mentioned: paging, sorting, and data retrieval.

To handle paging, in most cases, you don’t need to do much more than set the new page index in the PageIndexChanged event, and you can hard-code this behavior in the new control. As for sorting, a common practice is to sort data using a DataView class, which ultimately provides the grid’s contents. You can hard-code in the grid’s internal code the foundation for auto-reverse and safe multifield sorting. With respect to data retrieval in a new control, I believe that this is an application-specific choice. In light of this, the best you can do is fire an event that alerts the code to refresh the grid’s view. Paging and sorting will be provided automatically but you are still responsible for binding the code to data. Such a control (named PowerGrid in our example) can have a few extra properties and require only the following code for the output shown in Figure 5-11:

<expo:PowerGrid id="grid" runat="server"
    AutoGenerateColumns="false"
    PageSize="3" >
    <Columns>
        <asp:BoundColumn DataField="employeeid" HeaderText="ID"
            SortExpression="employeeid" />
        <asp:BoundColumn DataField="firstname" HeaderText="First Name" />
        <asp:BoundColumn DataField="lastname" HeaderText="Last Name"
            SortExpression="lastname" />
    </Columns>
</expo:PowerGrid>
Figure 5-11
The PowerGrid control in action.

Wrapping up the DataGrid control in a new, more sophisticated control allows you to embed your graphical preferences—colors, fonts, borders, styles, or whatever—in the control’s code. The PowerGrid class inherits from DataGrid and sets a lot of visual attributes in the class constructor. It also hooks up a few events, as the next bit of code shows:

ItemCreated += new DataGridItemEventHandler(OnItemCreated);
SortCommand += new DataGridSortCommandEventHandler(OnSortCommand);
PageIndexChanged += new DataGridPageChangedEventHandler(OnPageChanged);

The ItemCreated handler adjusts the header to reflect the current sort expression and updates the pager bar. SortCommand is responsible for preparing the sort expression, taking into account directions and fields. PageIndexChanged cares about the index of the new page. Both selecting a new page and sorting by a new field cause the grid to retrieve the data and refresh the view. The code for refreshing the view, however, is left to the calling page and is not implemented in the control’s code. Whenever the grid needs to rebind and refresh the view, it fires a custom event: UpdateView.

The UpdateView Event

The following code declares the UpdateView event. In our example, you can use the EventArgs class, because all the information the calling page needs is available through the DataGrid control.

public event EventHandler UpdateView;
protected virtual void OnUpdateView(EventArgs e)
{
    if (UpdateView != null)
        UpdateView(this, e);
}

The UpdateView event is fired to rebind and refresh the content of the page. The following code shows how this is done for the PageIndexChanged event:

void OnPageIndexChanged(Object sender, DataGridPageChangedEventArgs e)
{
    CurrentPageIndex = e.NewPageIndex;

    // Fire page-level event: UpdateView
    OnUpdateView(EventArgs.Empty);
}

The event is handled by the client, just as any other event would be.

<expo:PowerGrid id="grid" runat="server"

    OnUpdateView="UpdateView">

The signature of the handler is standard.

public void UpdateView(Object sender, EventArgs e)
{
    UpdateDataView();
}

Unless the data you retrieve from the source is already sorted, the UpdateView handler needs to know about which sort expression to use. Unfortunately, the DataGrid control does not provide this information.

The SortExpression Property

In Chapter 2, when I discussed the sorting capabilities of the DataGrid control, I remarked how tricky auto-reverse sorting can be. In our example, you need to compare the current sort expression with the new sort expression and just reverse the order if the expressions match. I used the ViewState collection of the DataGrid control to persistently store this information. The PowerGrid control accesses this ViewState slot by using a read-only property named SortExpression:

public String SortExpression {
    get { return (String) ViewState["CurrentSortExpression"]; }
}

The following code looks like some code you have already seen in previous chapters that refresh grid views. The key functionality here is the retrieval of the sort expression via a custom property of the PowerGrid control. The full source code for the PowerGrid.cs, TestPowerGrid.cs, and TestPowerGrid.aspx applications is available on the companion CD.

private void UpdateDataView()
{
    DataSet ds = (DataSet) Session["MyDataSet"];
    DataView dv = ds.Tables["MyTable"].DefaultView;
    dv.Sort = grid.SortExpression;
    grid.DataSource = dv;
    grid.DataBind();
}

The PowerGrid control takes advantage of the reusability features of ASP.NET to provide you with a powerful and specialized control that uses minimal code. By combining custom controls with code-behind techniques, you can have lightly coded ASP.NET pages that separate code and layout and offer a good level of reusability.


Conclusion

Reusability applies to ASP.NET code mostly in terms of server controls, which are the atomic elements of these Web pages. You can expect two main advantages from a custom control: new functionality that replaces, restricts, or extends existing controls; and encapsulation of some common settings and behaviors that limits the amount of code necessary to build a solution.

In this chapter, I started the discussion with code-behind files as a solution for separating code and layout. Next, I covered user controls as a way to assemble several distinct controls with the goal of building aggregates that behave as a whole control. Have you developed a company-wide logon dialog box? Did you spend hours building a project-specific grid? Have you finally gotten your long-awaited progress bar? User controls offer a way to make all this code really reusable and not just injectable via cut-and-paste methods in host pages. User controls are ideal for building software components in which UI elements work in conjunction with back-end code. Together, they provide a suite of behaviors that is publicly exposed and programmable through properties and methods.

You can always employ user controls to write non-UI components, but doing so would be a poor design choice. Think of user controls as sealed components, not inheritable components, that are good for aggregation. If you need more flexibility than this, try custom controls, which are new Web controls built to inherit in pure object-oriented style from existing Web controls. In this chapter, I described custom controls as the programming technique that weds the true power of inheritance with the encapsulation of boilerplate code and common settings. I closely examined custom controls in the context of the DataGrid control.

Chapter 6 focuses again on the extremely versatile DataGrid control and the more advanced solutions you can build and consolidate by employing user and custom controls.


Advanced Data Reporting

Chapter 6
Advanced Data Reporting

The DataGrid control not only is the most versatile reporting tool you have in ASP.NET, but also is likely the only tool you will want to use when building professional reports. In the previous chapters, I reviewed the basic set of features and functionalities supplied by the control. Although the DataGrid control is useful in many circumstances, you still need to write a lot of code (mostly smart and tricky code) to build complex solutions.

The DataGrid control deliberately mimics the interface and behavior of a Microsoft Excel worksheet, so programmers like you expect it to provide the same level of functionality. Microsoft was just whetting your appetite when it developed the control’s object model, however, because you can actually do so much more with it than is immediately apparent from scanning its supported methods and events. In this chapter, you will learn a bunch of practical solutions and tips that take advantage of the object model. So far, we’ve only just touched on the concepts that we will explore in the following pages: item selection and information drill-down.


Item Selection

If you need a grid of data, you probably also need a way to let your users select a particular row of that grid and see related information. I already touched on this topic in Chapter 1 when I discussed the selection mechanism for the DataList control. Let’s see how it works for the DataGrid control.

The internal mechanism for selection when using the DataGrid control is nearly identical to the one you saw in action for the DataList control. Typically, users trigger the selection by clicking a column button with the CommandName attribute set to the keyword select. The same event can also occur programmatically when the SelectedItemIndex property of the DataGrid control is set to a value greater than -1.

A selected row can have a custom style that you specify by using the SelectedItemStyle property; however, columns (including templated columns) do not support a particular template for a selected item, such as the DataList control’s SelectedItemTemplate template. Only one row in the DataGrid control can be selected at any time. Multiple selection is not supported. Later in this chapter I’ll show you how to work around this limitation.

note | A row is selected only if the user clicks the column button associated with the Select command. Full row selection is not supported. There is an architectural issue behind this limitation. To process the selection event on the server, you need an interactive element on the page that, when clicked, posts back to the server. Only link buttons allow for this, unless you write some ad-hoc JavaScript client-side script code.
Enabling Item Selection

To enable declarative (as opposed to programmatic) selection, you need to have in the DataGrid control a button column with the CommandName property set to select.

<asp:ButtonColumn CommandName="select" HeaderText="Employee"
    DataTextField="EmployeeName" />

The content of the column can be bound to one data source field as well as show static text or even a small picture. When you don’t want data bound text, use the Text property instead of DataTextField. (These properties are part of the programming interface of the ButtonColumn class and have nothing to do with the actual selection.) You can place the select column anywhere in the grid and bind it to any column in the data source, just as you can for the EditCommandColumn discussed in Chapter 4.

note | A grid can have as many select columns as needed. Each select column must be created using the ButtonColumn class with the CommandName property set to select. The user can select the row by clicking any of these columns. By making each column in the grid a select column, you can simulate full-row selection.

The DataGrid control treats select command columns in a special way. When the column is clicked, the SelectedIndexChanged event fires to enable you to handle the event. The DataGrid control also applies to the selected row the graphical settings you set by using the SelectedItemStyle property, as the following example shows:

<SelectedItemStyle BackColor="blue" />

The SelectedIndex property gets and sets the index of the currently selected item. The index is 0–based and refers to the current page, not to the whole data source. The SelectedItem property returns the object that represents the currently selected item. This object is an instance of the class DataGridItem.

tip | You can obtain the absolute index in the data set of the specified DataGrid control item by using the DataSetIndex property of the DataGridItem class. Each row in the DataGrid control is rendered using an instance of the DataGridItem class, and you access the instance by using the Items collection.
Styling the Selected Row

The selection style has a lower priority than any style settings you explicitly set for the column by using ItemStyle. For example, the following code draws the Position column with a reddish color, and the style is not overridden when the user selects the column:

<asp:BoundColumn runat="server" DataField="title" HeaderText="Position" >
    <itemstyle backcolor="#ffddff" />
</asp:BoundColumn>

Note that any style attribute set by using ItemStyle or AlternatingItemStyle at the grid level (as opposed to the column level, shown in the preceding code) is overridden during selection. Figure 6-1 shows what a selected column looks like.

Figure 6-1
A DataGrid control with a selected row.
caution | In addition to changing the color of a selected item, you can change the font style to reflect selection. However, when you change the font, the grid can become slightly larger and the text in some cells can wrap to the next line. Also pay attention to the colors you choose. The background color should contrast with both items and alternating items, but avoid too sharp a contrast. When you plan to use selection, don’t differentiate items from alternating items too much.
Using Formatted Text

You can use only button columns to enable selection. Button columns can contain only plain data bound text or static text. You cannot apply special templates that mix database fields with special graphical settings, and as I mentioned earlier in this chapter, templated columns do not support an ad-hoc template for selection. You can work around this limitation, however, as Figure 6-1 shows. The trick is using in-memory columns based on expressions. An expression-based column doesn’t take up too much memory because it stores only the metadata of the column plus the expression.

After you retrieve the bindable data, run the following code, which adds a new, customized column. This column embeds in the text any simple HTML formatting you want.

// ds is the DataSet that has just been filled up
DataTable dt = ds.Tables["MyTable"];
DataColumn dc = new DataColumn("EmployeeName", typeof(String),
    "titleofcourtesy + ' <b>' + lastname + '</b>, ' + firstname");
dt.Columns.Add(dc);

This dynamic column is then bound to the select command column.

<asp:ButtonColumn CommandName="select" HeaderText="Employee"
    DataTextField="EmployeeName" />
Using Images

You can use small images to identify which column the user can click to select a row. Just set the Text property of the ButtonColumn class with the HTML text that points to the URL of the image.

<asp:ButtonColumn CommandName="select"
    Text="<img alt='Select' src=unselected.gif>" />

For a better graphical result, you might want to explicitly set the border attribute of the <img> tag to 0 and the align attribute to absmiddle. To make the grid even more user friendly, you can make a final refinement and change the bitmap when the row is drawn in selected mode. See Figure 6-2.

Figure 6-2
You can change a bitmap when a row is redrawn to really draw attention to the selected item.
Using the SelectedIndexChanged Event

When a row is selected, the grid fires the SelectedIndexChanged event. You hook into this event in the usual ASP.NET fashion:

<asp:DataGrid id="grid" runat="server"

    OnSelectedIndexChanged="SelectionIndexChanged">

You write a handler for SelectedIndexChanged only when you need to accomplish tasks in response to the row selection—for example, changing the bitmap to reflect selection. You don’t need the handler to draw the row in selected mode.

public void SelectionIndexChanged(Object sender, EventArgs e)
{
    SelectRecord();
}

The SelectedIndex and SelectedItem properties let you know about the selected item. To retrieve the DataRow object that produced the current DataGrid item, you can leverage the combined effect of the DataKeyField and DataKeys properties. As discussed in previous chapters, you set DataKeyField with the name of a field that accepts unique values, and DataKeys gathers those key values. You select an entry within the collection by using the index returned by SelectedIndex. The following code shows how to retrieve the key value for the selected row:

public void SelectRecord()
{
    int nEmpID = (int) grid.DataKeys[grid.SelectedIndex];
    DataRow dr = RetrieveRowByID(nEmpID);
}

When the SelectedIndexChanged event fires, the SelectedIndex property is guaranteed to be up-to-date.

The select command column is a button column, so you would expect it to fire the ItemCommand event when clicked. This is exactly what happens. With tracing enabled, notice that, as shown in Figure 6-3, the ItemCommand event arrives first, before the SelectedIndex property is updated.

Figure 6-3
Enabling tracing reveals the order of event firing when the user clicks the select command button.

When ItemCommand fires, SelectedIndex contains the index of the previously selected row.

Selecting Rows Programmatically

In principle, to select a row, you don’t need a select command column. Although clicking a cell is the most intuitive way for a user to make a selection, you could select rows programmatically, regardless of whether a specific command column is present. To select a row programmatically, you set the SelectedIndex property to the 0–based index that corresponds to the position of the row in the current page. You cannot select a row that belongs to another page, but if you assign to SelectedIndex a value higher than the number of items in the page, no exception is thrown.

note | When you programmatically select or deselect a row, neither ItemCommand nor SelectedIndexChanged is fired. Since no event occurs, your code is responsible for maintaining the consistency of the application’s state. For example, you can simulate the events by calling directly the subroutines the event handler would execute.
Deselecting the Selected Row

The DataGrid control does not provide an interactive way to deselect the currently selected row. You can do that only programmatically by setting the SelectedIndex property to -1. This operation will not fire any event to the page.

Can you remove the selection automatically and interactively? The intuitive approach would be to enable the user to deselect the row by clicking the row a second time—one click selects, and two consecutive clicks restore the original state. But this functionality is impossible for a simple reason: when the DataGrid control is rendered with a selected row, the cell of the select column is not clickable and cannot post back for a selection change event. The workable approach is to provide the page with a link button.

<asp:linkbutton runat="server" id="btnUnselect" enabled="false"
    text="Unselect" onclick="OnUnselectRecord" />

The link button is disabled or invisible until a row is selected. The state of the link button can be easily managed by using the SelectedIndexChanged event or any other code that runs after a selection is made. The link’s onclick event will then deselect the row programmatically.

note | With a pageable DataGrid control, the selected index is maintained across pages. For example, if row 3 is selected on page 1 and the user moves to page 2, by default row 3 will also be selected on the new page. If the new page has fewer rows than the row number initially selected (in our example, fewer than 3 rows), no row is selected. This is default behavior. To work around it, cancel the selection in the PageIndexChanged event.
Selecting Rows by Using Field Values

Another interesting feature you might want to implement in your pages is the ability to select a row based on the value of a key field. In Figure 6-4, you see that no row is selected and the ID text box contains 3. (In Figure 6-4 there are also more controls that I’ll talk about later on.)

Figure 6-4
Enter the employee ID and click Go, and the row with a matching primary key will be selected.

When the user clicks the Go button, the row whose primary key value matches 3 is selected and displayed in the panel beside the row. (See Figure 6-5.) The SelectedIndex property interprets any number you pass on as a page index, so when you want to select a row based on the value of a given field, you have to figure out how to turn that field value into a page index. The easiest way to accomplish this is by using the DataKeys collection. Set the DataGrid control’s DataKeyField property to the name of the field, typically a key field. Next, walk your way through the DataKeys collection and compare any value found with the specified key. If a match is found, the current index of the loop is the value for SelectedIndex. The following code shows how the DataKeys approach works with a numeric employee ID field:

private int GetPageIndexFromID(int nEmpID)
{
    int nRetValue = -1;
    for (int i=0; i<grid.DataKeys.Count; i++)
        if (nEmpID == (int) grid.DataKeys[i])
        {
            nRetValue = i;
            break;
        }
    return nRetValue;
}

This approach is not perfect. It does not work when you need to evaluate a more complex expression that involves more fields, and the search is limited to the items currently displayed.

Figure 6-5
Enter the employee ID and click Go to select the row with a matching primary key.
Selection and Drill-Down

When you have a lot of information to display and decide to split the presentation into two (or more) steps, you will greatly appreciate the grid’s ability to select rows. For example, the user first reviews a quick list of records in which only summary information is shown. Then he selects one particular row and asks for more information about it.

To handle this information drill-down, use the SelectedIndexChanged event, which is ideal for catching the user’s request for additional information about a record. Then turn the clicked grid item into a more manageable DataRow object. To retrieve the ADO.NET object that lies behind a grid row, you can use one of two approaches. In the first approach, you obtain a DataView object from the data source and use the Find method on the primary key. If the data source is not already a DataView object, you can easily build one from a DataTable object. The Find method, however, does not return a DataRow object directly; it returns the index of the row in the DataView object that contains the primary key value specified. It returns a null value if the primary key value does not exist.

If this approach does not work well for you—for example, you find it too restrictive because of the exclusive use of primary keys—a good alternative is to use the Select method of the DataTable object. The Select method evaluates an expression and returns an array with the matching DataRow objects.

// ds is the DataSet just filled up
DataTable dt = ds.Tables["MyTable"];
DataRow[] a = dt.Select("EmployeeID=" + nEmpID.ToString());

The cardinality of the resulting array depends on the nature of the expression. If the expression has to match only against the values of the primary key, the array will be empty or contain at most one row. For this reason, using the following code makes some sense. The try block assumes that the array has exactly one row. The code falls into the catch section only if no row is found.

try {
    view.EmployeeID = a[0]["EmployeeID"].ToString();
    view.TitleOfCourtesy = a[0]["TitleOfCourtesy"].ToString();
    view.FirstName = a[0]["firstname"].ToString();
    view.LastName = a[0]["lastname"].ToString();
    view.Title = a[0]["title"].ToString();
    statusbar.Text = "Ready";
}
catch (Exception exc) {
    view.ClearAll();
    statusbar.Text = "<b>Error occurred: </b>" + exc.Message;
}

The preceding code snippet uses the contents of the selected DataRow object to populate the fields of a user control. All the text boxes you see grouped in Figure 6-5 are the constituent controls of a user control named ViewPanel.

<expo:ViewPanel runat="server" id="view" />

The ViewPanel user control exposes the Text property of each text box as an individual property named for the represented field. To clear the whole panel (for example, when an item is deselected), you don’t have to clear all the fields one at a time. All you have to do is call the ClearAll method exposed by the control. This example is a practical application of what you learned about pagelets and reusability in Chapter 5.

Filtered Views

Let me point out a tricky issue that you might face when working with grids that show filtered data views. Suppose that you use a DataView object as the data source of a DataGrid control and that you apply a filter on the view so that the set of records you see by using the grid does not match the actual number of rows in the original table. In the application shown in Figure 6-5, you can set a filter string to restrict the rows displayed in the grid. A filter string is an expression that may contain column names combined with operators and constants. For example, the filter string shown below denotes all the rows where the value of the employeeid field is greater than 3:

employeeid > 3

The filter string is assigned to the DataView object built on top of the table and used to display the content of the DataGrid control. You assign the filter string to the DataView object’s RowFilter property and then link the DataView object with the grid, as shown in the following code:

DataView dv = new DataView(ds.Tables["MyTable"]);
dv.RowFilter = txtFilterString.Text;
grid.DataSource = dv;
grid.DataBind();

The same results could also be achieved using the DataViewManager object, which is a sort of centralized handler of table settings. The DataViewManager object works on all the tables in a DataSet object and returns the default view for the given table. The code below shows how to set a filter using the DataViewManager object:

DataViewManager dvm = ds.DefaultViewManager;
dvm.DataViewSettings["MyTable"].RowFilter = txtFilterString.Text;

Let’s review the behavior of the DataTable object’s Select method when a filter is set. The rub lies in the fact that Select always works on the underlying table and is unaffected by any filter you might build atop the table. So the sample application could paradoxically locate and display information about records that aren’t in the grid’s view. This subtle problem could easily become a serious security issue if the filter has been applied to limit the activity of the current user.

To work around this problem (which is by design), you can concatenate the filter string and the Select object’s expression by using the Boolean AND operator. This is exactly what the “Restrict Search To The Current Filtered View” check box does in the sample application.

String strFilter = "";
if (bSearchOnFilter.Checked)
    strFilter = (txtFilterString.Text != ""
                ? txtFilterString.Text + " AND " : "");
DataRow[] a = dt.Select(strFilter + "EmployeeID=" + nEmpID.ToString());

This code is fully demonstrated in the EmployeeManager.aspx and EmployeeManagerWithImages.aspx sample applications which are available on the companion CD. The results are shown in Figure 6-5.


Allowing the Selection of Multiple Items

The DataGrid control does not support the selection of multiple items in the current page, much less the whole data source. Nevertheless, a lot of Web sites out there provide this functionality. For example, Web sites that let the user create a mailbox show messages in a table of rows. Each row contains a check box for selection, and a link at the bottom of the page allows the user to execute actions on the selected rows. In ASP.NET, the table can easily be obtained using the DataGrid control. After you add an extra column with a check box and figure out how to expose the information behind a row, you are really close to creating a multiselection grid.

A multiselection grid is relatively easy to build as a constituent part of the page. You insert a DataGrid control with a templated column (to provide the check box) and then write all the necessary event handlers. In this section, I’ll be doing something slightly different and more complex but a lot more reusable: I’ll build a new DataGrid control that automatically provides the check box column, a custom footer with predefined functions, and a collection that returns all the items currently selected in the current page. I have indeed chosen a very fancy name for this new control: the SuperGrid control.

note | I built the SuperGrid control by enhancing the source code of the PowerGrid control described in Chapter 5. The SuperGrid control has no dependencies on the PowerGrid control. I deliberately made the class inherit from DataGrid rather than PowerGrid by duplicating the source code. I agree with what you’re probably thinking—that this approach is not very object-oriented—but it does make the code more easily reusable for your own projects.
Properties of the SuperGrid Control

Just like the PowerGrid control of Chapter 5, the SuperGrid control is a grid that automatically provides advanced sorting and pagination. In addition, it places at your disposal the extra properties shown in Table 6-1.

Table 6-1 Multiselection Properties of the SuperGrid Control
Property | Description
AllowMultiSelect | A Boolean value that enables and disables the multiselection feature. False by default.
AllowMultiSelectFooter | A Boolean value that enables and disables a custom footer with grid-specific functions. It overrides the user-defined footer, if any. False by default.
SelectedItems | Returns a collection of DataGridItem objects, each of which corresponds to a selected item in the current page. Null by default.

The following directive is needed to enable the use of the SuperGrid control in ASP.NET pages. You can change the TagPrefix attribute when you want to.

<%@ Register TagPrefix="expo" Namespace="BWSLib" Assembly="SuperGrid" %>

The following code snippet, instead, demonstrates how to use the control. The output of the code is shown in Figure 6-6.

<expo:SuperGrid id="grid" runat="server"
    AutoGenerateColumns="false"
    AllowMultiSelect="true"
    AllowMultiSelectFooter="true"
    DataKeyField="employeeid"
    OnUpdateView="UpdateView">
    <Columns>
        <asp:TemplateColumn HeaderText="Name" SortExpression="lastname">
        <itemtemplate> <%#
            DataBinder.Eval(Container.DataItem, "titleofcourtesy") + " <b>"
            + DataBinder.Eval(Container.DataItem, "lastname") + "</b>, "
            + DataBinder.Eval(Container.DataItem, "firstname") %>
        </itemtemplate>
        </asp:TemplateColumn>
        <asp:BoundColumn DataField="title" HeaderText="Position"
            SortExpression="title, employeeid" />
        <asp:BoundColumn DataField="hiredate" HeaderText="Hired"
            SortExpression="hiredate, employeeid"
            DataFormatString="{0:d}" />
        <asp:BoundColumn DataField="country" HeaderText="Country"
            SortExpression="country" />
    </Columns>
</expo:SuperGrid>
Figure 6-6
The SuperGrid control in action. The first column enables selection. The footer bar lets you clear all selections.
Layout of the SuperGrid Control

As you see in Figure 6-6, the DataGrid control has an extra column not mentioned in the code we just examined. It is a templated column that displays a check box. The SuperGrid control adds this column dynamically when the AllowMultiSelect property is set to true. In Figure 6-6, note the customized footer with an Unselect link button. You enable this control-specific footer by using the AllowMultiSelectFooter property. It overrides any footer you might have specified in the grid’s declaration. The grid you create in the ASP.NET page is automatically and programmatically completed with a templated column and a footer when multiselection is enabled.

Adding the Select Column

To show a check box in the column’s cells, you have two options. You can write a new custom column class or, more simply, you can create a dynamic templated column. (I discussed dynamic columns in Chapter 3.) The .NET Framework provides the Page object’s LoadTemplate method, which takes a virtual (not physical) path to a user control (an ASCX file) and uses the control’s contents to populate the column. You can also create column templates from in-memory strings by saving them to a temporary file. Finally, you can even create a dynamic template using a new class that inherits from ITemplate. This is how the select column is created in the sample.

The creation of the extra column takes place during the grid’s initialization phase and is fired from within the Init event handler.

public SuperGrid() {

    Init += new EventHandler(OnInit);
}

When the Init event is raised, the control has already been associated with the page, making it possible for you to access properties and methods on the Page object.

note | A custom control such as SuperGrid cannot directly access the Server object or any other ASP intrinsic object. In this case, you can access the Server object only by using plain ASPX files because all the code runs within a run-time class derived from the Page class. In ASP.NET, Server and other ASP intrinsic objects are properties of the Page class, and all user controls inherit a Page member from the base class Control.
public void OnInit(Object sender, EventArgs e)
{
    if (AllowMultiSelect)
        AddSelectColumn();
}

AddSelectColumn is an internal member function that creates a column with two templates: ItemTemplate and FooterTemplate. The former provides the check box for selecting the row. The latter provides a custom button bar with selection commands such as Unselect. The following code listing demonstrates the creation of the select column:

private void AddSelectColumn()
{
    TemplateColumn tc = new TemplateColumn();
    tc.ItemStyle.BackColor = Color.SkyBlue;
    tc.ItemTemplate = new SuperGridColumnTemplate();
    Columns.AddAt(0, tc);
}
Adding the Footer Template

In our example, a footer template is needed to host the link buttons that will execute grid-specific actions—for example, deselecting all the selected items in the current page. The following code shows the steps for adding a custom footer to the select column. It creates an instance of an ITemplate-based class for the footer template and assigns the resulting object to the column’s FooterTemplate property.

tc.FooterTemplate = new SuperGridFooterTemplate();

At this point in our example, the Unselect link button would appear at the bottom of the select column and look pretty dull, as shown in Figure 6-7.

Figure 6-7
A straightforward but not very visually stimulating link button.

You might want to manipulate the footer quite a bit to turn it into a more specific status bar with links, labels, and—why not—drop-down lists. I designed the SuperGrid control to provide a made-to-measure footer with an Unselect link button. This footer clears all other footer cells you might have. You enable the grid-specific footer by using the Boolean property AllowMultiSelectFooter. The next code listing shows how the SuperGrid control sets up the grid’s footer to make it look like the footer in Figure 6-6.

if (itemType==ListItemType.Footer
        && AllowMultiSelect
        && AllowMultiSelectFooter)
{
    // Force ShowFooter to true
    ShowFooter = true;

    // Removes all the cells but the first
    TableCell cell = e.Item.Cells[0];
    for (int i=1; i<Columns.Count; i++)
        e.Item.Cells.RemoveAt(1);

    // Span the first cell to cover the whole grid's width
    cell.ColumnSpan = Columns.Count;

}

The Unselect button in the built-in footer invokes the SuperGrid control’s public method named ClearSelection. So to easily integrate your existing footer with the deselect feature of the SuperGrid control, add a new link button that explicitly invokes the ClearSelection method.

Binding the OnClick Event Handler

So far in our example, the link button defined in the footer template isn’t bound to code. The link button declaration is incomplete and lacks an event handler for the OnClick attribute:

<asp:linkbutton runat=server text=Unselect id=lnkUnselect />

Unfortunately, you cannot bind the OnClick attribute by using a method within the template code. If you add an attribute such as OnClick=OnUnselect to the previous declaration, the control compiles successfully but a run-time error occurs as soon as you open the ASP.NET page. The rub is that the ASP.NET run time expects to find the definition of the method within the template. Having the method defined as a public member of the SuperGrid class is not enough. You can try inserting into the template string a block such as the following:

<script runat=server>
public void OnUnselect(object sender, EventArgs e)
{
    // Do something
}
</script>

The problem, however, is just shifted. Now the ASP.NET run time locates the click handler but still fails with any object or method code you invoke that is not part of the template. What is really needed here is a binding between the link button in the footer template and code defined within the SuperGrid control. This code can’t take the form of a declaration.

Any template is seen as a container control separated from the hosting page, so you cannot see methods and objects within the context of the DataGrid control, and you are denied access to the link button via its ID. The ID lnkSelect means nothing to the ASP.NET run time if it is called from within the grid’s context. To get hold of the living instance of the link button, the SuperGrid code needs to search for it within the footer item container. This can be easily done in the ItemCreated handler when the item being created is the footer.

if (itemType==ListItemType.Footer &&
        AllowMultiSelectFooter && AllowMultiSelect)
{
    // Look for a link button called "lnkSelect" in the context
    // of the grid item that represents the footer
    LinkButton lb = (LinkButton) e.Item.FindControl("lnkSelect");

    // Now you hold the living instance of the link
    // button in the footer and can bind it to any code in the
    // context of the SuperGrid control
    lb.Click += new EventHandler(OnUnselect);

    // Other code here...
}

Calling FindControl on the DataGridItem object that represents the footer template returns a valid LinkButton object with the specified ID. Utilizing the DataGridItem object is the key to bringing a reference to the link button in the context of the SuperGrid control. Now its Click event can be easily bound with any method of the SuperGrid control. The OnUnselect handler ends up calling the public method ClearSelection.

public void OnUnselect(Object sender, EventArgs e) {
    ClearSelection();
}

public void ClearSelection()
{
    foreach(DataGridItem dgi in Items)
    {
        // The check box is the first control in the first cell
        CheckBox cb = (CheckBox) dgi.Cells[0].Controls[0];
        cb.Checked = false;
    }
}

Clearing the selection is as easy as locating the check box control in the controls hierarchy and unchecking it.

Retrieving the Selected Items

The SuperGrid control exposes the items selected in the current page by using a custom property named SelectedItems, which returns an array of DataGridItem objects. SelectedItems is implemented as a read-only data member. The get accessor walks through the grid’s Items collection and adds to the ArrayList object only those items whose CheckBox control is checked.

public ArrayList SelectedItems {
    get {
        if (!AllowMultiSelect) return null;

        ArrayList a = new ArrayList();
        foreach(DataGridItem dgi in Items)
        {
            CheckBox cb = (CheckBox) dgi.Cells[0].Controls[0];
            if (cb.Checked)
                a.Add(dgi);
        }
        return a;
    }
}

A client page uses the SelectedItems property as follows:

public void OnAddToCart(Object sender, EventArgs e)
{
    foreach(DataGridItem dgi in grid.SelectedItems)
    {
        String strItemIndex = grid.DataKeys[dgi.ItemIndex].ToString();
        ListItem li = new ListItem("ID=" + strItemIndex, strItemIndex);
        if (!listboxCart.Items.Contains(li))
            listboxCart.Items.Add(li);
    }

}

Figure 6-8 (on page 195 in the next section) shows a sample page that adds the selected items to a cart. The cart is represented by a list box control that has some extra code for removing items. When the user adds items to the cart, the code also checks to see whether the item is already present in the cart.

Using the DataGridItem Object

Each displayed item in a DataGrid control is programmatically exposed using the DataGridItem class. This exposure applies to client rows as well as to nonclient items such as the header, footer, and pager bar. The client items are stored in the Items collection. The SelectedItems property of the SuperGrid control returns a subset of the Items collection. A DataGridItem object, though, is not a DataRow object and does not owe its content to a data source. How can you retrieve the actual row of data behind a DataGrid control item?

The DataGridItem has a number of interesting properties. One of the first that is likely to get your attention is ItemIndex, but it merely returns a 0–based index indicating the position of the row in the page. Another cool property is DataSetIndex, which returns the absolute index of a row in the data source. You can then use this index on the DataTable object to selectively access the DataRow used to populate the grid item.

// ds is the DataSet that has just been filled up
DataTable dt = ds.Tables["MyTable"];
DataRow row = dt.Rows[dgi.DataSetIndex];

Alternatively, as you learned in previous chapters, you can use the ItemIndex property to access the key value for a row if you use both the DataKeyField and DataKeys properties.

You expect the DataGridItem property to represent only the row object you are searching. However, handle this property with extreme care because in a few event handlers, it is not yet initialized and points to null objects.

Maintaining Selections Across Pages

The SuperGrid control does not support selection across pages. The structure of the control and its way of using postback events makes effective coding difficult. Although you can implement selection across pages in a pageable control, I recommend that you code cross-page selection according to the design of your application.

The sample application shown in Figure 6-8 handles cross-page selection. It saves references to the items selected in a given page to an external control, for instance, a list box. This control is then used as an intermediate cart that serves as the real application repository for selections. By controlling the insert process (to prevent the addition of the same item twice) and adding the ability to remove items from the cart, you can obtain an effective selection solution without too much effort—and without spending too much time changing the state management policies of the DataGrid control. The full source for the supergrid.cs, multiselect.aspx, and multiselect.cs applications is available on the companion CD.

Figure 6-8
A multiple selection application that uses the SuperGrid control.


Selecting by Condition

In all the techniques discussed so far, the selection of a control is manual and interactive, occurring only when the user clicks it. What about enhancing the control a little bit to make it support selection by condition?

When the selection feature is enabled in a DataGrid control, your ultimate goal is to retrieve one or more DataRow objects. In the normal process of selection, you use the grid to provide a friendly user interface. When you want to pick up all the rows that match a certain condition, you don’t need the grid to help with the selection process, although it is helpful in providing visual feedback about the matching rows.

When you need to process all the rows that meet certain criteria, you can use the Select method on the DataTable object. Select returns an array of DataRow objects without the involvement of the DataGrid control. I’m going to show you how to force a DataGrid control to draw all the rows that match a condition with a unique background color to indicate selection. The grid will not expose these rows through a collection because you can retrieve them by using the DataTable object.

caution | In my example, I’m assuming that you use a DataTable or DataView object to populate the grid. Using these objects is the most common scenario but does not cover all the possibilities. The next code snippet might not work if you use DataReader objects or arrays to populate the DataGrid control.

The ability to highlight rows by condition is governed by two more custom properties on the SuperGrid control that we haven’t discussed yet. They are RowSelectFilter and SelectBackColor. I’ll add these properties to the programming interface of the SuperGrid control.

public String RowSelectFilter {
    get { return (String) ViewState["RowSelectFilter"]; }
    set { ViewState["RowSelectFilter"] = value; }
}
private Color m_SelectBackColor = Color.SkyBlue;
public Color SelectBackColor {
    get { return m_SelectBackColor; }
    set { m_SelectBackColor = value;}
}

I made the RowSelectFilter property persistent across page requests by using the control’s ViewState collection. RowSelectFilter holds the string that represents the condition. The role of SelectBackColor is quite self-explanatory—it represents the color to use for the background of the row.

tip | When you plan to select rows by condition, you might want to disable any custom alternating item style. Otherwise, the grid is at risk of being colorful and dazzling but not very readable.
Changing the Background Color

Implementing the select-by-condition feature requires two important steps. First, you evaluate the condition for the row being drawn. Second, you change the background color for the row. The second step is the easiest, so I’ll explain it first.

You can override the background color of a DataGrid control item by hooking into the ItemCreated event. You make sure that the item type is either Item or AlternatingItem, and then you set the BackColor property of the item. (This code snippet must then be expanded to include the code that actually evaluates the condition.)

if (itemType == ListItemType.Item ││
    itemType == ListItemType.AlternatingItem)
{
    DataRowView drv = (DataRowView) e.Item.DataItem;
    if (drv != null && RowSelectFilter != "")
    {
        if (bMeetSomeCriteria)
            e.Item.BackColor = SelectBackColor;
            // Can set other style properties here...
    }
}
Evaluating the Condition

If the condition is not subject to dynamic changes, evaluating it is straightforward. A generic and variable condition is a bit more complicated to evaluate. What will the expression look like? The easiest approach you can take is to support any expression that is acceptable for data columns and for the DataTable object’s Select method. This is a great approach for two reasons: you already have the tools you need, and a rather powerful language, in data-binding expressions. The following are typical expressions you can evaluate:

country = 'USA'
hiredate < #1/1/1994#

These expressions can be enriched with Boolean and arithmetic operators, some functions (Len, Substring, Convert, Iif), and ad-hoc operators such as LIKE and IN. (See the MSDN documentation for more information about data-binding expressions.) How do you evaluate these expressions?

A very promising tool seems to be the DataBinder.Eval method. Unfortunately, Eval understands only those expressions that evaluate to properties or column fields. Therefore, to use Eval, you must add a new expression-based column to the data source. After you add a new column, however, you have no further reason to stick to Eval. Also, Eval is not a particularly lightweight method.

An alternative approach that does not require the creation of a column is based on the DataTable object’s Select method. As the code following demonstrates, you select all the rows that meet the criteria, then compare each of them to the current data item until a match is found:

if (itemType == ListItemType.Item ││
    itemType == ListItemType.AlternatingItem)
{
    DataRowView drv = (DataRowView) e.Item.DataItem;
    if (drv != null && RowSelectFilter != "")
    {
        DataTable dt = drv.Row.Table;
        DataRow [] a = dt.Select(RowSelectFilter);
        foreach(DataRow dr in a)
            if (dr == drv.Row)
            {
                e.Item.BackColor = SelectBackColor;
                // Can set other style properties here...
                break;
            }
    }
}

This code works fine, but consider that Select can take a while to complete and might return a bunch of rows. In addition, this code calls Select for each item and alternating item. There has to be a better way.

Adding an Extra Column

As discussed in previous chapters, an expression-based column is not very expensive and does not result in the storage of new data. Its only cost is evaluating the expression when you attempt to read the value, so it is ideal for our purposes. The approach we’ll take is to pad the data source with a newly added, sneaky column, defined as follows:

DataColumn dc;
dc = new DataColumn("RowSelectFilter",
        typeof(bool),
        RowSelectFilter);

The column will be a Boolean column with a hard-coded name—say, RowSelectFilter. Its contents are dynamically determined by evaluating the expression set through the RowSelectFilter property. Whenever you access a row of the RowSelectFilter column, the expression is evaluated and results in a Boolean value. Because the column is added internally to the SuperGrid control code, it is invisible to the user. Assuming that you have such a column, the code that highlights a grid item changes as follows:

if (itemType == ListItemType.Item ││
    itemType == ListItemType.AlternatingItem)
{
    DataRowView drv = (DataRowView) e.Item.DataItem;
    if (drv != null && RowSelectFilter != "")
    {
        if ((bool) drv["RowSelectFilter"])
            e.Item.BackColor = SelectBackColor;    }
}

One problem left to solve, but it is the trickiest. How and when do you create the RowSelectFilter column? The RowSelectFilter column must be up and running when the DataGrid control is rendered and, of course, cannot be created prior to setting up the data source. To create the column, you need to access the contents of the DataSource property, extract the collection of columns, and add the new one. If you perform this task before the grid is rendered, you end up executing the task too many times. On the other hand, creating the column when the RowSelectFilter property is set is risky because nothing can guarantee that at that time the DataSource property points to a valid and non-null object. A better time to create the RowSelectFilter column is when the DataSource property is set. But this solution also isn’t free of a little drawback: you must always set the RowSelectFilter property before you bind the grid to the data source and order the refresh.

Detecting when a given property is assigned—DataSource in this case—is not that difficult from within a control. You simply override the property, as shown in the following code snippet:

public override object DataSource {
    get {return base.DataSource;}
    set {base.DataSource = value;}
}

This code defines an override for the DataSource property that is identical to the original property. Now customizing the set accessor is as easy as adding some lines of code:

set { base.DataSource = value;

    // Custom code
    DataTable dt = null;
    if (DataSource is DataTable)
        dt = (DataTable) DataSource;
    else if (DataSource is DataView)
        dt = ((DataView)DataSource).Table;

    DataColumn dc;
    try {
        if (dt.Columns.Contains("RowSelectFilter"))
            dt.Columns["RowSelectFilter"].Expression = RowSelectFilter;
        else {
            dc = new DataColumn("RowSelectFilter",
                    typeof(bool), RowSelectFilter);
            dt.Columns.Add(dc);
        }
    }
    catch {RowSelectFilter="";}
}

The code first ascertains the type of data source. (As mentioned earlier in this chapter, the sample code supports only data sources that are DataView or DataTable objects. You can easily enhance the code to make it support the DataViewManager class. Supporting DataReader and collection classes is much trickier.)

After you hold the living instance of the DataTable object that populates the DataGrid control, you add a new column named RowSelectFilter. If the column already exists—for example, because you already added it—you only change the value of its Expression property.

Figure 6-9 shows the SuperGrid control in the context of our sample application. You can declaratively set all the properties pertinent to the select-by-condition feature. The full source code for the SuperGrid.cs, ByCondition.aspx, and ByCondition.cs applications is available on the companion CD.

<expo:SuperGrid id="grid" runat="server"

    RowSelectFilter="hiredate > #1/1/1994#"
    SelectBackColor="yellow">
Figure 6-9
A version of the SuperGrid control that selects rows by condition.


Aggregates and Summary Rows

To round out our discussion of advanced DataGrid controls, let’s examine complex reports in which you have summary rows with partial totals. The sample application extracts from the Northwind database information about the customers and orders. The grid displays all the orders issued in a given year, sorted by customer. Each row contains the total price of the order, which is already an aggregate value because the order is stored as a collection of items. As shown in Figure 6-10, the summary row is inserted between customers and contains the total number of orders and the total price of all orders.

Figure 6-10
A more complex DataGrid control showing partial totals and summary rows.
The Right Way to Query

Don’t let the tremendous number of methods and events in a DataGrid control fool you. A DataGrid control is really best suited for displaying tabular data. Although it provides many tools for combining the code you use to build the grid, these tools rarely affect the structure and content of the data. Despite this limitation, the DataGrid control does let you significantly change the layout of the cells displayed. What implications does this functionality have for your code?

The DataGrid control cannot add summary rows on the page because it has not been designed to—period. But if you have extra rows in the data source, you can modify the cell layout so that the whole row looks like a summary row. An important guideline to follow when you work with the DataGrid control is to provide the control with pre-processed data that closely resembles the final expected output.

Creating Relations Between Tables

Let’s see how to query for all the orders in the Northwind database, grouped by year and customer. Given a year, the following SQL command selects all the orders issued by customers. Only the sum of all item prices for each order is displayed. Figure 6-11 shows the typical result set when the query generates.

SELECT o.customerid, od.orderid, SUM(od.quantity*od.unitprice) AS price
    FROM Orders o, [Order Details] od
    WHERE Year(o.orderdate) = @nYear AND od.orderid=o.orderid
    GROUP BY o.customerid, od.orderid
    ORDER BY o.customerid
Figure 6-11
The results of a query displaying the total price of each order.

Notice in Figure 6-11 that the price column contains the total amount for each order. The summary row would add all the orders for a customers and also display the name of the customer. There are two basic ways of doing this, one of which uses the SQL language.

The GROUP BY clause of the SELECT statement in the T-SQL language provides the WITH ROLLUP extension that adds predefined summary rows to the result set. Of course, such a summary row has the layout of all other columns, but the content of each column can be customized to some extent. The following statement illustrates how to modify the previous command to allow for summary rows. Figure 6-12 shows the result set.

SELECT
    CASE GROUPING(o.customerid) WHEN 0 THEN o.customerid ELSE '(Total)' END
        AS AllCustomersSummary,
    CASE GROUPING(od.orderid) WHEN 0 THEN od.orderid ELSE -1 END
        AS IndividualCustomerSummary,
    SUM(od.quantity*od.unitprice) AS price
    FROM Orders o, [Order Details] od
    WHERE Year(o.orderdate) = 1998 AND od.orderid=o.orderid
    GROUP BY o.customerid, od.orderid WITH ROLLUP
    ORDER BY AllCustomersSummary

GROUPING is the T-SQL aggregate function that works in conjunction with ROLLUP in the body of a GROUP BY clause. The use of GROUPING causes a new column to be added to the result set. This column contains a value of 1 if the row has been added by the ROLLUP operator—therefore, it is a summary row. Otherwise, the column has a value of 0. By using a CASE..WHEN..END statement, you can merge this new column with the grouping column. For example, the T-SQL statement below creates a new column called AllCustomersSummary which normally contains the value of the CustomerID column and the string “(Total)” in all rows created by grouping on that column.

CASE GROUPING(o.customerid) WHEN 0
    THEN o.customerid
    ELSE '(Total)'
END AS AllCustomersSummary
Figure 6-12
The results of a query displaying the total price of each order.

By using the WITH ROLLUP feature, you get data from the source already in a format suitable for display. In the rest of the chapter, though, I’m going to illustrate an alternate, more flexible, but less lightweight, approach. It exploits some new features of ADO.NET (such as data relations) and lets you use summary rows with any number of columns. Key to this approach is the fact that summary rows are stored in a separate result set.

Based on the SQL Server 2000 Northwind database, the query in the preceding code, run for the year 1998, results in 270 records and 81 customers. The next query shows how to get this second result set, with the item totals for each order listed in the summary row. The results are shown in Figure 6-13.

SELECT c.customerid,
        c.CompanyName,
        Total=SUM(od.quantity*od.unitprice),
        TotalOrders=COUNT(DISTINCT od.OrderID)
    FROM Customers c, [Order Details] od, Orders o
    WHERE Year(orderdate) = @nYear
        AND c.CustomerID = o.CustomerID
        AND o.OrderID = od.OrderID
    GROUP BY c.CompanyName, c.CustomerID
    ORDER BY c.customerid
Figure 6-13
The results of the query that provides the data for the summary row.

With the ADO.NET classes, you don’t have to merge these two result sets because they can be more effectively processed in memory during the grid rendering. I grouped the queries in a new stored procedure that takes one input argument—the year.

CREATE PROCEDURE Orders_Summary_By_Customers_And_Year
    (@nYear int)

The data adapter runs the stored procedure and returns two result sets. The former result set is used to populate the data grid; the latter serves to fill up summary rows.

note

ADO.NET commands that return multiple result sets let you control only the name of the first one. If you call the first table MyTable, the others are appended a trailing index: MyTable1, MyTable2, and so on. You rename these tables as follows:

da.Fill(ds, "Orders");
ds.Tables["Orders1"].TableName = "OrdersSummary";

As an alternative, you can use table mappings to predefine the names for multiple result sets. For example:

da.TableMappings.Add("Orders1", "OrdersSummary");
da.Fill(ds, "Orders");
Adding Summary Rows

The DataGrid control does not allow you to add table rows at run time. Even if you could figure out a way to do so, often you would be adding rows to pages that already have the maximum number of rows. These new rows wouldn’t affect the way in which the grid extracts the items for a given page. To prevent any problem with rows and pages, you add extra rows directly to the data source. Ideally, you add an extra blank row between the blocks of records with different customer IDs, but making an insertion requires you to scroll the whole result set. You could more effectively append rows, set the customerid field as appropriate, and then sort. After sorting, each semi-blank row—the summary row—is magically in place.

foreach(DataRow row in dtOrdersSummary.Rows)
{
    DataRow blank = dtOrders.NewRow();
    blank["CustomerID"] = row["CustomerID"];

    // Application-specific trick. Figure out a reliable
    // way to identify the summary row later while drawing items
    blank["OrderID"] = -1;
    dtOrders.Rows.Add(blank);
}

The key strategy in the preceding code is inserting information in the summary row that simplifies your detection of it later when you need to display the summary row. During the grid rendering, you hook into the ItemCreated event and check the contents of the row being drawn. If the row is the summary row, the graphical layout changes to display information from the OrdersSummary table. In our example, I decided to mark the summary row with a -1 value in the OrderID field. (This decision is application-specific.) Figure 6-14 shows how the grid looks at this point.

Figure 6-14
The grid with summary totals. An OrderID field of -1 identifies a summary row.
Populating the Summary Row

The layout (font, background color, and number of cells) and contents of the summary row must be modified. Any graphical update can be done in the ItemCreated event. For example, you can group the first two cells and render the whole row with a white background and bold fonts.

DataRowView drv = (DataRowView) e.Item.DataItem;
if ((int) drv["OrderID"] == -1)
{
    e.Item.BackColor = Color.White;
    e.Item.Cells[2].Font.Bold = true;
    e.Item.Cells[2].HorizontalAlign = HorizontalAlign.Right;
    e.Item.Cells.RemoveAt(1);    // the order# column
    e.Item.Cells[0].ColumnSpan = 2;
}

The way you bind the cells with summary text depends on the type of the grid columns involved. If they are templated columns, you simply set the Text property of the desired cell with the desired text. For other column types, including bound columns, the binding approach is a little more complicated. In the case of a bound column, the association between the cell text and the data is handled in the ItemDataBound event. The ItemDataBound event fires after ItemCommand. As a result, any text you set in ItemCreated is soon overwritten by ItemDataBound. To work around this, hook into the ItemDataBound event.

Retrieving Summary Data

When the ItemDataBound event is processed, the grid is working on a row taken from the Orders in-memory table. When the row appears as a summary row, you need to replace this information with data coming from the OrdersSummary table. How do you get the corresponding summary row? Create a data relation between the two tables, which makes this retrieval quite straightforward.

DataColumn dc1 = dtOrders.Columns["CustomerID"];
DataColumn dc2 = dtOrdersSummary.Columns["CustomerID"];
DataRelation dr = new DataRelation("OrdersAndSummary", dc1, dc2);
ds.EnforceConstraints = false;
ds.Relations.Add(dr);

A DataRelation object creates a relationship between two tables that have a common column. Both tables must be part of the same DataSet object. After the relation is set, you get from each row of the parent table the array of child rows by calling the GetChildRows method.

note

Normally the parent/child relationship manifests in unique values in the parent column and duplicate values on the child column. In our example, however, the parent table (Orders) can have many rows with the same customer ID, whereas the child table (OrdersSummary) considers the customer ID as a unique field. To legitimize this situation, you must explicitly disable any constraint on DataSet. You could also opt for a simpler approach and disable the constraints only on the particular data relation. This is done by adding an extra Boolean argument in the DataRelation constructor:

DataRelation dr = new DataRelation("OrdersAndSummary",
dc1, dc2, false);

You set the relation between the two tables upon loading. During the ItemDataBound event, you obtain the corresponding summary row with the following, surprisingly simple, code:

DataRowView drv = (DataRowView) e.Item.DataItem;
DataRow[] a = drv.Row.GetChildRows("OrdersAndSummary");
DataRow drSummaryRow = a[0];    // Only one row selected by design
// Build the string to display
e.Item.Cells[1].Text = strTextToDisplay;

The final result of this code is shown in Figure 6-15. The full source code for the SuperGrid.cs, Summary.aspx, and Summary.cs applications is available on the companion CD. Note that the sample application also combines the summary rows with the select-by-condition feature I examined earlier in the chapter.

Figure 6-15
The SuperGrid control used to display summary rows and partial totals.


Conclusion

In this chapter, I examined techniques and tools for building sophisticated, professional reports using the DataGrid control. I also suggested the importance of having effective SQL code. The effectiveness of the SQL code has to be measured not only in terms of performance and query execution plans but also in terms of the capabilities supplied by the objects you will use on the Web server. ADO.NET classes offer a lot, and balancing the workload between SQL Server and the .NET Web server has never been so attainable.

Reporting does not exhaust the range of functionality provided by Web applications. In Chapter 7, I’ll focus on disconnected applications, caching, and batch update.


Disconnected Web Applications

Chapter 7
Disconnected Web Applications

If you’ve programmed data-driven applications, disconnected programming is nothing new for you. A few years ago, it was one of the most touted features of Microsoft ADO 2.x. The key difference between ADO and ADO.NET is the way in which the disconnected feature is embedded into the surrounding framework.

ADO and ADO.NET are designed differently. ADO was invented in the age of connected, 2-tier applications, and the object model design reflects that. The disconnected features of ADO were added in a nonintrusive way and exposed via the Recordset object as if they were a new, special type of cursor. ADO.NET, on the other hand, was designed at the same time XML was emerging as a hot new technology, and developers were realizing that life had a lot more to offer than the MoveNext and MovePrevious methods. The number of developers writing applications that interacted with a local, disconnected cache of data was growing. So ADO.NET is tailor-made for the .NET Framework and also designed for the new breed of application: n-tier, capable of disconnected functionality, and XML-aware.

Typically, the purpose of disconnection is to enable the application to cache data locally and then access the local cache instead of the native storage medium. However, not all applications can tolerate the latency associated with disconnection. Applications that are inherently database driven, function in a highly concurrent environment, and must work in a connected manner to detect incoming data changes just can’t be adapted to work in disconnected mode. They also can’t be forced to use inherently disconnected data objects. And what’s the typical ADO.NET disconnected data object? You guessed it—the DataSet object.


What Is the DataSet Object Really For?

The DataSet object represents an in-memory cache of data and is the critical object that gets passed between the middle tier and the client application (for example, a Microsoft Windows Forms application) or between the middle-tier and the Web Services layer. DataSet objects easily and efficiently serialize themselves into and out of XML. This means that data, as well as the related schema information, can be moved between tiers in a loosely coupled manner.

A Windows Forms application that receives a DataSet object can take full advantage of it. Data is cached on the client and, unless you need a current copy of the data, you can sort, page, and filter the data without accessing the database server. Add to this functionality the object’s complex programming interface, and you go straight to the conclusion that DataSet is the right object in the right place at the right time. How does all this affect Web applications?

Implications for Web Applications

The DataSet object is still great for storing the state that survives across page requests—for example, cached catalog information, the contents of a shopping cart, or a pageable sales report. However, this storage ability does not guarantee enhanced scalability and better performance. When accessing data over the Web, you have an extra layer to consider.

Which client processes disconnected data through the DataSet object? The HTML page displayed by the browser is one possible client. The middle-tier component (in the simplest case, an ASP.NET page) that connects to the database is another possible client. If the presentation layer is based on a browser, you can’t really send a DataSet object embedded with HTML code. I’m sure you could come up with some workarounds, but they won’t be exciting prospects until .NET languages are natively supported by Microsoft Internet Explorer.

Disconnection for Web applications means that you cache data on the Web server and keep your middle tier working off line and disconnected from the database server. The advantages of this technique are more relevant if your system architecture provides two distinct server machines for the database and the Web server.

Caching on the Web server is critical, and you have several options to choose from: using the Session object (in-process, out-of-process, or SQL Server–based); the Application object; the powerful Cache object (new to ASP.NET); or the same DataSet object, by performing brute-force serialization of it into the page view state. You can also cache to XML files to avoid taxing the Web server’s memory on a per-user basis. No solution works in every case, and each is more of a compromise than the perfect workaround. The characteristics and requirements of your application are the only way to determine which approach you should take.

note | The Cache object represents the ASP.NET cache engine that can be used by pages to store and retrieve objects across HTTP requests. This cache is unique for each application and stores objects in memory. The lifetime of the cache coincides with the lifetime of the application. Although the Cache object might seem like the thread-safe version of Application, it provides additional features to ensure that seldom-used data never wastes server resources. If you want to control how scavenging occurs, you can provide the following for each item: expiration dates, decay factors, priority level, key, and file dependencies.

Let me sum up your options. If you plan to use server-side caching, start by choosing a caching mechanism and pay careful attention to the advanced services of the built-in ASP.NET caching service. With regard to the data container object, you probably need to look at only the DataSet object. Whatever caching mechanism you implement, use DataSet to collect data. It is unique in its ability to group related information.

DataSet and the DataGrid Control

As mentioned in previous chapters, the use of DataSet can be critical when you use it with certain controls such as the DataGrid control. Using it with the Data- Grid control can be dangerous to the health of your application because the DataGrid control does not cache the DataSet object it is bound to. When you return to the Web server for a postback event, you have to reload DataSet and rebind with the DataGrid control. At that point, you have two mutually exclusive but equally safe options:

  • If you need fresh data, use the more lightweight DataReader object and use custom pagination to minimize the number of records loaded.

  • If you don’t care about fresh data (because your database is not volatile or your users are not interested in last-minute changes), cache the DataSet object. For the majority of applications, the Cache object is the best place to park data. To make it user-specific, name the cache slots you use after the session ID.

Like the Application object, the Cache object does not share its state across a Web farm, so the Web farm scenario is the hardest one for application-wide caching. Relying on the built-in database caching mechanism is probably the most efficient approach. Consider that databases, and SQL Server 2000 in particular, feature very advanced and super-optimized query engines. Queries that execute frequently, such as those used to page back and forth, are recognized and cached internally, so getting fresh results for a small number of rows takes little time. No matter how smart you are, I have a strong feeling that outperforming the cache system of SQL Server 2000 wouldn’t be that easy for you!

Towards Disconnected Applications

Applications called to manage data that is not particularly volatile or that has a low degree of decay can happily cache the DataSet object on the Web server. By doing so, these applications can use a DataGrid control to sort and page through the data.

In this chapter, I’ll go beyond the basics of data disconnection and focus on a couple of additional aspects that address more general techniques and cover a broad range of applications. One of these techniques is transparently loading data either from files or by using a data adapter object. The other is modifying the cached data in memory and submitting all the changes to the database in a single batch. This is known as batch update and is an old acquaintance of ADO developers.

The application we’ll examine uses a double level of caching. One level is in-memory caching, used to fill out the DataGrid control that is showing the data. This kind of caching is implemented by using the Cache object and is tied to the application’s lifetime. A second level of caching keeps the application disconnected from the data source across multiple executions of the application. Basically, you hibernate the state of the application and resume it on the next execution. Thanks to the DiffGram structure of the DataSet object, you can persist and reload all the changes you make to DataSet since the last connection to the database or the last time you committed your work.

caution | This hibernation technique is not appropriate for every application. To be really effective, it must rely on a database with a low level of concurrency and a low contention of data. If you cannot safely implement optimistic locking for the records, the batch update is not the right approach. The number of row conflicts is potentially high, canceling out the advantage of working disconnected.


Transparent Data Sources

The Employees Manager sample application we’ll be looking at is a relatively simple viewer of records that allows for pagination, sorting, and more important, insertions, deletions, and updates. Figure 7-1 shows the main application interface. (You can find the EmpMan.aspx application on the companion CD.)

Figure 7-1
The main interface for record viewer application.

When the application first loads, it performs two important tasks: it attempts to locate a particular XML file on the Web server that is known to contain the data to resume execution and, if the file is found, it reads the file’s content into the Cache object and displays the content in the grid. The grid represents the working copy of the data that is affected by any insertion, deletion, and update the user performs. If the file is not available, the data is retrieved from the original data source and then cached.

Working with the Cache Object

Unlike the Session object, the Cache object does not work on a per-user basis. Any object you store is visible to all users throughout all currently running sessions. Why would you choose Cache over Session? My opinion is that if you can easily figure out how to store and retrieve user-specific information from Cache, you should seriously consider using it. The Cache object implements a least recently used (LRU) mechanism that automatically frees up rarely used or unimportant data when the system memory becomes too scarce. In other words, the Cache object gives you more power when controlling the amount of information stored in memory. If you plan to store large blocks of data and are using Session, consider implementing a home-made scavenging mechanism to ensure that valuable server resources are preserved and the server is always in shape. You can accomplish the same results without any cost by using Cache, and I think Cache gets the job done better.

To recognize the current session, you can always rely on the session ID, which has an ASCII-compatible representation in ASP.NET. However, an application that supports offline activity might require multiple sessions for the same user before the data is actually committed. In this case, a session ID is unfit to recognize users because it would change from access to access. So you recognize users through their user IDs, and you use this piece of information to store user-specific data in the cache and to create made-to-measure files.

In the sample application, the login page passes the name of the connected user to the page that follows it by using a URL parameter. The user name is then used to look up an XML file containing the DataSet object previously saved for that user. If no file is found, the data is loaded from the database. No matter how data is retrieved, when it is loaded, it is soon copied into the ASP.NET cache by using a user-specific slot. Again, the user name is the key to keeping user information distinct.

Loading Data from a Generic Source

In our application, the function that loads data checks whether the dump file exists and then either reads the file content or fills a data adapter. You can use the File.Exists method to discover whether a certain file exists. File.Exists is certainly more lightweight than setting up a try/catch block, as shown in the next snippet of code, but a try/catch block protects you from a greater number of erroneous and inconsistent situations. If the file exists with corrupted content, a simple check for existence does not shield you from a run-time error.

private DataSet LoadData(String strFile, String strConn, String strCommand)
{
    DataSet ds;
    try {
        XmlTextReader xtr = new XmlTextReader(Server.MapPath(strFile));
        ds = new DataSet();
        ds.ReadXml(xtr);
        xtr.Close();
    }
    catch() {
        SqlConnection conn = new SqlConnection(strConn);
        SqlDataAdapter da = new SqlDataAdapter(strCommand, conn);
        ds = new DataSet();
        da.Fill(ds, "Employees");
    }
    return ds;
}

In spite of the radically different storage medium we’re using (a relational database versus an XML file), the structure of the resulting data is the same. The DataSet object can be serialized to XML efficiently and with excellent results. Basically, it can store and hence serialize all the information that pertains to a given set of tables, including primary keys, constraints, and relations.

The companion CD contains an application named XmlCreator.aspx that allows you to execute queries to fill DataTable objects and add them to a given DataSet object. The DataSet object is then serialized to an XML disk file that can be further used as the data set of a disconnected application.

tip | An application that reads all the data from a separate XML file can work without SQL Server 2000 installed. This lets you build effective demos that can be deployed and run on any client’s machine without setting up the database first. The spooky presence of the XML file can be hidden in custom classes that encapsulate the overall data access.
Creating Subtables

When you design applications that must work with the database both on line and off line, you might have to code the creation of a subtable—that is, a table that contains only a subset of the original rows. This code would not be particularly hard to write, but it has the potential to unveil some rather underhanded moves by ADO.NET objects.

All ADO.NET container objects such as DataSet and DataTable hold references to the child objects, not their whole contents. An object that belongs to a certain container—for example, a DataRow object that belongs to DataTable—cannot be linked to another object, even when no structural or syntactical contraindications exist. For example, you can’t add a DataRow object to a table that has a schema different from DataRow, and you can’t even add it to a table with the same schema. The only way to work around this constraint is duplicating the objects. In .NET, this linking issue is addressed in terms of the shallow and deep copy of objects. A shallow copy of an object limits duplication to the top-level interface of the class but adds references to child objects. In .NET, classes normally allow shallow copying through a method called Clone. If a Clone method is not available for a given class, then shallow copying is not publicly allowed. In their own implementation code, though, classes can always create shallow copies of themselves by using the MemberwiseClone method.

The DataTable class allows external callers to create shallow copies through the standard method Clone.

caution

A variable-to-variable assignment does not accomplish shallow or deep copying either. Instead, it simply creates another reference to the same memory location and hence to the same object.

DataTable dt1 = dt;

Once this statement executes both dt1 and dt point to the same object.
Cloning the Table Schema

The Clone method of the DataTable class lets you create a new empty DataTable object with the same structure (columns, keys, constraints, mappings, and relations) as the parent:

DataTable dt1 = dt.Clone();

A full copy of the table can be created by using the Copy method, which combines the cloning and the duplication of the rows.

Importing Rows

You can add rows to a DataTable object in two ways: by using the Rows property and the DataRowCollection collection class, or by using the ImportRow method. The two strategies work quite differently. When you use the Rows collection, you can add only unreferenced DataRow objects (that is, newly created DataRow objects or DataRow objects that are detached from any table.) The second strategy, which uses the ImportRow method, first creates a deep copy of the DataRow object and then adds it to the Rows collection. A deep copy contains all child objects; that is, the copied table has its own unique DataRow and DataColumn objects. The following code snippet demonstrates how to select some rows from a table and use them to populate a new table.

DataRow[] rgRows = dt.Select(strCriteria);
DataTable dtCopy = dt.Clone();
foreach(DataRow row in rgRows)
    dtCopy.ImportRow(row);

To manually and quickly duplicate a DataRow object, you use the Item- Array property of the DataRow class. ItemArray returns all the values in the row as an array. Interestingly, if you assign ItemArray an array of values, the values will be copied automatically in the row fields in the order they appeared in the original table. Armed with this functionality, you can easily set up a deep copy of a DataRow object as follows:

DataTable dt = drOrig.Table;
DataRow drCopy = dt.NewRow();
drCopy.ItemArray = drOrig.ItemArray;

You need to create a new DataRow object with the same schema as the original. The DataRow object, though, does not have a public constructor and can be created only by the NewRow method called on a DataTable object that has the needed schema. The row’s Table property returns the DataTable object with its corresponding schema.


Batch Updates

The DataSet object is designed with the assumption that optimistic concurrency is the default. As a container of disconnected data, the DataSet object accepts any sort of update to the rows it contains, so you can add new rows to any child table and update or delete existing rows. All these changes are persisted in memory and never get passed on to the database until the batch update is conducted. The batch update process starts when you call the Update method on a data adapter object and pass the DataSet as the argument. Prior to that, no change is applied to the database. When the batch update is completed, the DataSet changes are automatically committed to ensure consistency between the in-memory cache and the underlying database.

A batch update is the submission of an entire set of changes to the database. More exactly, the batch update repeats the user actions (typically, SQL commands) that produced the changes but targets the database rather than the DataSet object. In a multiuser environment, the batch update poses a potential conflict and design issue for you to resolve: when users concurrently access data, they can change the original database values while the rows are processed in memory. The batch update in such an environment is problematic if in the time elapsed between your fetch and the batch update, another user changes one of the rows you plan to modify. If your update is somehow dependent upon the current row value, then you might want to reconsider, and even reject, your most recent updates. If your update can simply overwrite any existing value (this depends on the application’s logic) then batch update is not problematic at all.

At update time, conflicts detected between the original values and the current values of certain database rows might introduce significant overhead that can invalidate the batch update. On the other hand, when you mix together data disconnection, batch updates, and a distributed, multiuser environment, you serve up exciting recipes for an effective Web architecture. Unfortunately, since we live in an imperfect world, you pay a price for them: row conflicts show up, but they are a natural and almost inevitable drawback.

Locking Rows

To update a database row in a multiuser environment, you must first lock it to prevent other users from accessing it. You can follow one of two lock models, each representing a different design philosophy: optimistic or pessimistic concurrency. Each lock model is not necessarily appropriate for every application. In some cases, system requirements force you to opt for one particular policy.

The Pessimistic Lock Model

Pessimistic concurrency is a defensive approach that tends to eliminate at the root any occurrence of conflicts. You set the lock on the row in a way that prevents other users from performing any action that can affect the current user. Other users will be given full access to the row only when the lock is released. The pessimistic model is mostly used in environments where the contention for data is quite heavy and the consistency of the application is mined with conflicts. In such systems, rolling back transactions—often distributed transactions—is typically more complicated than preventing conflicts by applying more durable locks. In a pessimistic model, a user planning to modify a row keeps the row locked until the change is committed, and the row is unavailable to other users. The pessimistic model promotes a transactional vision of the read and write actions.

The Optimistic Lock Model

Optimistic concurrency considers the reading and writing of information as two distinct, non-transactional actions. Typically, when a user reads a row, no lock is applied. This allows more users, at any given time, to concurrently read the contents of the row. The lock is set only when one user needs to write information, so you have no guarantee that the content of the row is exactly the same as it was when read earlier. This is just where the notion of optimism fits in. The optimistic lock policy assumes that the row has not been changed in the meantime. This assumption is reasonable for systems with a relatively low contention for data. If you can determine with some certainty that no user changed the row under your nose, the batch update is an effective technique. It reduces the hits on the database server, doesn’t lock records for very long, and contributes to establishing a much more responsive and forgiving application.

The application using the optimistic lock strategy is more responsive because all changes take place in memory prior to committing the update. Modifying a row is just a matter of overwriting a memory location. The application is more forgiving towards the user because you can design it to show a summary of the changes, giving the user a chance to accept or reject them in memory. In any case, optimistic concurrency leads to rather disconnected applications that use the database only as a downstream storage medium.

Depending on the ultimate goal of the application, conflicts might or might not be critical. When they are, you can apply a few techniques to detect and prevent them. I’ll discuss these later in the chapter.

Applying In-Memory Changes

Figure 7-2 shows the grid of data that users of this chapter’s sample application will see. The grid is based on the one I discussed in Chapter 4. It allows for new rows to be added and deleted, and supports in-place row editing. The user adds a new row by moving to the last page and clicking the link button at the bottom of the page. The user can edit and delete each row by clicking the two rightmost buttons.

Figure 7-2
The editable grid whose content can be loaded from either the database or an XML resume file.

In Chapter 4, while discussing the features of the DataGrid control’s in-place editing feature, I repeatedly mentioned the batch update as an alternative technique. It can be used in lieu of direct SQL commands that run on a per-row basis as soon as each modification takes place. In this chapter, you’ll see in action a fully editable DataGrid control that uses a double level of cache to park changes temporarily, waiting for the final submission to the database.

note | Although the table displayed in Figure 7-2 looks identical to the Employees table of the Northwind database, the records actually come from a custom SQL Server database named BWSLib. The custom database allows you to play with the rows without affecting the original Northwind database. BWSLib is available on the companion CD and can be easily attached to your own installation of SQL Server 2000.
Inserting New Rows

The three basic operations you can execute on the data are INSERT, UPDATE, and DELETE. Each operation is performed on the DataSet object that is currently in the ASP.NET cache. In the sample application, the DataSet object is retrieved using the GetDataFromCache method. This method is based on the following code:

String keyUserName = Session["USERNAME"].ToString();
DataSet ds = (DataSet) Cache[keyUserName];

I discussed the details of insertion in Chapter 4, but I’ll review them here. You first add a new row to the data source that the DataGrid control is bound to, and then you update its content by using typical in-place editing. You can also give default values to some of the row’s columns.

DataSet ds = GetDataFromCache();
DataTable dt = ds.Tables["Employees"];
DataRow dr = dt.NewRow();
dt.Rows.Add(dr);
tip | You can configure the data adapter object to automatically inherit schema information from the native database tables. Either by setting the MissingSchemaAction property to AddWithKey or by invoking the FillSchema method, the adapter loads and applies some settings from the source tables to the DataTable objects. Not all column settings can be imported, however. You can automatically set the properties AllowDBNull, ReadOnly, Unique, and AutoIncrement, but not the auto-increment seed, step, and default values. You’ll learn about auto-increment columns later in this chapter.

When adding new rows to in-memory tables, you should carefully consider the case of auto-numbered columns. I’ll say more on this point in a moment.

Updating Existing Rows

Updating a row is as easy as retrieving the DataRow object involved and changing the value of its columns. You can accomplish this by using the DataTable object’s Select method on a primary key value. The following is the typical code you use:

DataRow[] rgRows;
rgRows = dt.Select("employeeid=" + grid.DataKeys[e.Item.ItemIndex]);
DataRow dr = rgRows[0];

The Select method retrieves an array of rows that match the specified criteria. If the condition involves a primary key, the array contains at most one row. If the value for the primary key is obtained through the DataGrid control’s DataKeys collection, exactly one DataRow object will be selected.

Deleting Existing Rows

From the standpoint of the application, deleting an existing row is not much different from updating it. The application must retrieve the correct DataRow object and call the Delete method on it. However, beware of record deletion in in-memory tables. Deletion can be logical or physical. Logical deletion takes place using the Delete method of the DataRow object. Physical removal means that the row is removed from the DataTable object. It is conducted using the DataRowCollection object’s Remove method. A logically deleted row can be restored using the RejectChanges method. You can call RejectChanges for the specific DataRow object as well as for all the rows in a DataTable object and for all tables in a DataSet object. A physically removed row no longer exists in the table.

note | A row removed from the Rows collection of the DataTable object is not destroyed at all. It is only detached from the table and is in the same condition as a newly created row not yet added to any table. This means that hypothetically, you can add the DataRow object to the same table again as long as the object does not go out of scope. Adding the DataRow object again to the table’s Rows collection is sufficient. So what does it mean to delete both the logical and the physical? Only logically deleted rows participate in the batch update process and have the corresponding database row deleted. Removed rows are simply ignored and the corresponding database row remains intact.
Auto-Increment Fields

Auto-increment fields are database columns whose values are automatically determined by the database at insertion time. These values are progressive and increased according to the specified step attribute each time a new row is added. The initial value is known as the seed value of the field. Because of the nature of the auto-increment feature, you can’t know the value in advance when you insert a new row into an in-memory table, but leaving the field blank or filling it with potentially duplicated values can cause subtle troubles for the application.

The value you assign to an auto-increment field should be ignored when you submit that row to the database. More exactly, you should avoid specifying an explicit value for that field when preparing an INSERT command; otherwise, you are at risk of getting a runtime error from the database. Databases, in fact, do not allow explicit values for auto-increment fields. (In order to disable the auto-increment feature for SQL Server, you must set the IDENTITY_INSERT attribute to OFF.)

The ADO.NET DataColumn object supports the AutoIncrement property, with seed and step attributes, so why not enable this property when adding rows to an in-memory DataTable object? When you do, your code doesn’t have to strive to maintain the uniqueness of the identity column. To enable the auto-increment feature automatically for the column that represents the table’s identity, set the MissingSchemaAction property to AddWithKey prior to filling the DataSet object:

SqlDataAdapter da = new SqlDataAdapter(strCommand, conn);
da.MissingSchemaAction = MissingSchemaAction.AddWithKey;
da.Fill(ds, "Employees");

For example, the employeeid column of the Northwind Employees table is set as the identity column, and this setting is automatically imported in the resulting DataTable object that contains rows from that table. Unfortunately, the data adapter is not able to automatically import the seed and the step values of the column. You need to set these properties separately:

ds.Tables["Employees"].Columns["employeeid"].AutoIncrementSeed = 1000;
ds.Tables["Employees"].Columns["employeeid"].AutoIncrementStep = 1;

The seed determines the first value that will be generated. As you can guess, this seed value is not persistent across sessions, nor is it compared against the values already in the DataTable object. So a typical problem you’ll face is choosing a seed value that does not collide with other rows. Since the seed is session-specific, you can safely choose either of these approaches: you can use a seed value that is clearly out of the current range (say, 1000), or you can calculate the maximum value in the current range of rows. The following code shows how to compute the maximum value of the employeeid column. Figure 7-3 shows the sample application at work.

DataTable dt = ds.Tables["Employees"];
int nSeed = (int) dt.Compute("Max(employeeid)", "");
dt.Columns["employeeid"].AutoIncrementSeed = nSeed + 1;
Figure 7-3
The newly added row features an automatic new value for the auto-increment column.

Bear in mind that the auto-increment value has a very short life. You need it only to unambiguously identify the row within the boundaries of the application. The value is discarded as soon as the change is submitted to the database. If you read the table back, you can easily find a completely different value in the identity column. Although they appear similar, the auto-increment feature of the database and the DataColumn object are completely separate and independent.

Delete and Remove

As mentioned earlier, you can delete a row from an in-memory table in two ways: by using the Remove method of the DataRowCollection object, which deletes a DataRow object from the collection of rows of a given table; or by using the Delete method of the DataRow object, which simply marks the row for deletion. When a row is marked for deletion, its RowState property is set to Deleted. You can view all the deleted rows at any time by setting a row state filter on the table. (More on this in a moment.)

note | To avoid confusion, for the remainder of the chapter, deleted rows refer to rows logically deleted using the Delete method. Removed rows refer to rows physically removed from the DataSet object using the Remove method.

Marked rows are actually removed from the table only when the application calls the AcceptChanges method for the row. If the application calls RejectChanges, the row reverts to its pre-deletion state. When you logically delete a row that has just been added to the table (that is, the row’s RowState property is set to Added), the row is removed from the table instead of being marked as deleted.

Logical and physical deletion affects the batch update process. When you submit changes to the database, the data adapter selects the rows involved in the update by looking at their RowState properties. The values of rows marked as added are transmitted to the database via INSERT commands. Likewise, rows marked as modified are mapped onto the source rows via UPDATE commands, and deleted rows are removed from the database using DELETE commands. This is a key point. Only rows marked as deleted are taken into account during the batch update, so only rows logically deleted in memory have the rows corresponding to the database physically removed from the database.

The DataSet object—the all-encompassing ADO.NET container object—maintains original and current values for each row of each table. When a row is removed from a table, its row state information is lost and, consequently, the row is no longer available for the batch update. Calling the AcceptChanges method commits the state of the involved rows, turning any Added or Modified flag to Unchanged and removing any row marked as deleted. In light of this behavior, AcceptChanges must be called only at the end of the batch update process. Calling AcceptChanges prior to calling the Update method of the data adapter clears all the changes before they are applied to the database. Not calling AcceptChanges after the update is completed maintains pending changes in the DataSet object, which is re-issued next time. When the batch update operation ends successfully, the Update method automatically calls AcceptChanges and returns.

// ds stores the application's data
dataAdapter.Update(ds);

Notice, though, that Update can only call AcceptChanges on the DataSet you provide for the batch update. Sometimes the DataSet used for batch update is only a small subset of the original object where all the application’s data are stored—for example, a subset containing only the changed rows. In this case, you should explicitly accept changes on the original DataSet, too. So the correct sequence for an effective batch update is the one outlined in the following pseudo-code:

// Get changes in the original DataSet
dsChanges = ds.GetChanges();
// Performs the batch update for the given table.
// Changes are committed on the copy but not on the original!
dataAdapter.Update(dsChanges, strTable);
// Clears any pending change in memory
ds.AcceptChanges();
States of a Row

So far the DataRow object’s RowState property is emerging as the key player in the batch update process. The RowState property influences the return value of the HasChanges method on the DataSet object. HasChanges returns a Boolean value that indicates whether any constituent table has pending changes. Related to HasChanges is the GetChanges method, which returns a DataSet object that includes only the rows (and the tables) with pending changes. By tracking the current state, the RowState property indicates whether a row has pending changes. The feasible values for RowState are summarized in Table 7-1.

Table 7-1 Values for the RowState Property
Value | Description
Added | The row has been added to the table, but AcceptChanges has not been called yet.
Deleted | The row is marked for deletion from the parent table.
Detached | Either the row has been created but not yet added to the table, or the row has been removed from the rows collection.
Modified | Some columns within the row have been changed.
Unchanged | No changes have been made since the last call to AcceptChanges. This is also the state of all rows when the table is first created.

The AcceptChanges method has the power to commit all the changes and accept the current values as the new original values of the table, clearing pending changes. RejectChanges rolls back all the pending changes.


An Offline Buffer for Data

The sample application uses two levels of cache. The topmost cache is represented by the ASP.NET Cache object, which contains the data needed for pagination and all the changes entered at a given time. The second level of cache is an XML file that is expected to persist across two consecutive invocations of the application. (Because the file contains a serialized DataSet object, it is an XML file.) Like the Cache slot, the file is named for the connected user.

When you must serialize a DataSet object to XML, you have three basic options: save the object with schema information, save it without schema information, or save it in DiffGram format. (By default, the DataSet object is saved without schema information.) The available options are grouped in the XmlWriteMode enumeration. The following code shows how to save a DataSet object to a given disk file as a DiffGram:

ds.WriteXml(strFile, XmlWriteMode.DiffGram);

You can save directly to a path name or to a stream. Reading back the XML text to rebuild the DataSet object is a bit trickier. In this case, you use the ReadXml method, which has a prototype very similar to WriteXml. The only difference between them is the range and the semantics of the values you use to control how the DataSet object is set up. ReadXml accepts only the flags defined in the XmlReadMode enumeration.

Loading a DataSet from XML

The ReadXml method reads the contents of the XML document and fills the DataSet object with data. In doing so, it also creates the relational schema of the DataSet object according to two conditions: whether the reading mode has been specified, and whether a relational schema is already described in the XML source. The XmlReadMode argument defaults to Auto, which means that the ReadXml method examines the XML data and determines the most appropriate option to follow.

When the XML data is a DiffGram, the DiffGram format (described in the next section) is used to extrapolate data. When the XML document does contain an inline schema, this schema is used to determine the structure of the DataSet object’s child elements. This behavior evaluates to the ReadSchema option. If no schema is found, the schema information is inferred using the InferXMLSchema method on the DataSet object. As you can guess, when you set an explicit XmlReadMode method while loading a DataSet object, your performance can only be improved.

The DiffGram Format

A DiffGram is an XML serialization format that includes both the original value and the current value of each row. In particular, it contains the list of rows with the original values plus a final section where all the changes are grouped. Each row is given a unique identifier that is used to track changes between the two sections of the DiffGram. This relationship looks a lot like a foreign key relationship. The next listing outlines the structure of a DiffGram, where row 1 has been deleted, row 3 modified, and a new row inserted.

<diffgr:diffgram>
  <DataSetName>
    <Employees diffgr:id="Employees1" msdata:rowOrder="0">...</Employees>
    <Employees diffgr:id="Employees3" msdata:rowOrder="2">...</Employees>
    <Employees diffgr:id="Employees4" msdata:rowOrder="3"
        diffgr:hasChanges="modified">...</Employees>
    <Employees diffgr:id="Employees5" msdata:rowOrder="4">...</Employees>
    <Employees diffgr:id="Employees6" msdata:rowOrder="5">...</Employees>
    <Employees diffgr:id="Employees7" msdata:rowOrder="6">...</Employees>
    <Employees diffgr:id="Employees8" msdata:rowOrder="7">...</Employees>
    <Employees diffgr:id="Employees9" msdata:rowOrder="8">...</Employees>
    <Employees diffgr:id="Employees10" msdata:rowOrder="9"
        diffgr:hasChanges="inserted">...</Employees>
  </DataSetName>
  <diffgr:before>
    <Employees diffgr:id="Employees2" msdata:rowOrder="1">...</Employees>
    <Employees diffgr:id="Employees4" msdata:rowOrder="3">...</Employees>
   </diffgr:before>
</diffgr:diffgram>

The <diffgr:diffgram> root node has two children. The first is the DataSet object with its current contents, including newly added rows and modified rows but not deleted rows. This data tree takes its name from the DataSet object. You get and set the DataSet object’s name by using the DataSetName property.

The second child is the tree rooted in the <diffgr:before> node. This tree contains enough information to restore the original state of the DataSet object. For example, it still contains any row that has been deleted as well as the original contents of any modified row. All columns affected by the change are tracked in the <diffgr:before> subtree. Although this approach is certainly quite verbose and redundant, it enables a slightly better performance when restoring original values in the in-memory DataSet object. To overwrite an entire row, the previously mentioned ItemArray property on the DataRow object is a faster approach than locating a column and then updating it.

Identifying Rows

The diffgr:id attribute is used establish a link between current and original rows. The diffgr:hasChanges attribute helps find out quickly which records were deleted, inserted, or only modified. The structure of each row node depends on a few factors. In most cases it will look like the next code listing. Each column is represented by a node element, with the contents of the column being the node text.

<Employees diffgr:id="Employees1" msdata:rowOrder="0">
  <EmployeeID>1</EmployeeID>
  <LastName>Davolio</LastName>
  <FirstName>Nancy</FirstName>
  <Title>Sales Representative</Title>
  <TitleOfCourtesy>Ms.</TitleOfCourtesy>
  <BirthDate>1948-12-08T00:00:00.0000000+01:00</BirthDate>
  <HireDate>1992-05-01T00:00:00.0000000+02:00</HireDate>
  <Address>507 - 20th Ave. E. Apt. 2A</Address>
  <City>Seattle</City>
  <Region>WA</Region>
  <PostalCode>98122</PostalCode>
  <Country>USA</Country>
  <HomePhone>(206) 555-9857</HomePhone>
  <Extension>5467</Extension>
  <Notes>...</Notes>
  <ReportsTo>2</ReportsTo>
</Employees>

You can control the way in which the contents of each column is written to XML by using the ColumnMapping property of the DataColumn object. The property takes values from the MappingType enumeration. The default setting is Element, which means that, as shown in the preceding code, each column has its own node. You can also require the column’s value to be persisted as an attribute on the main row node (the <Employees> node in the preceding example). The value to pick up from the MappingType enumerator is Attribute. Finally, you can hide the column by using the Hidden setting, as shown in the next bit of code:

DataTable dt = ds.Tables["Employees"];
dt.Columns["Notes"].ColumnMapping = MappingType.Hidden;
dt.Columns["ReportsTo"].ColumnMapping = MappingType.Attribute;

These settings have a slightly different effect according to whether you apply the DiffGram format or plain XML serialization. If plain XML serialization is applied, the hidden column is left out of the resulting XML document. If you write DiffGrams, the hidden text is included in the document even though it is marked as hidden. The column mappings in the previous code change the <Employees> node, as shown here:

<Employees diffgr:id="Employees1"
    msdata:rowOrder="0"
    msdata:hiddenNotes="Education includes a BA in psychology from Colorado
    State University in 1970. She also completed "The Art of the Cold
    Call." Nancy is a member of Toastmasters International."
    ReportsTo="2">

The Notes column is marked as hidden using a dynamically generated attribute named hiddenNotes. The ReportsTo column instead is stored as an attribute on the parent row node.

note | When you try to build a DataSet object from an XML DiffGram, make sure that the target DataSet object has the same schema as the previously saved DataSet object that you are reading back. If it does not have the same schema, the DiffGram merge operation fails, and an exception is thrown. You cannot load a DiffGram into an empty, newly created DataSet object. The schema information of an existing DataSet object can be duplicated onto another DataSet object by using the Clone method.
About Null Values

To optimize the code, the DataSet object drops column fields with null values during the serialization process. Null values in a column are not a problem per se, and dropping the null column fields doesn’t affect the usability of the DataSet object. You can successfully rebuild the DataSet object from XML, and data bound controls can easily manage null values. This behavior is hard coded in the DataSet class XML serializer and cannot be configured. If this behavior happens to be a problem for you, consider the following workaround. When making the query, use the T-SQL ISNULL function to automatically turn any null value into a blank but usable value. For example, you replace this query:

SELECT employeeid, lastname, firstname, region FROM Employees

And you use the following one that automatically converts any null values in the region column:

SELECT employeeid, lastname, firstname, ISNULL(region, '') AS region
    FROM Employees

The example code nullvalues.aspx demonstrates XML serialization with and without null values and is available on the companion CD.


Submitting Changes

The DataSet object can submit data to the database in batch mode by using the data adapter’s Update method. Data can be submitted only on a per-table basis. When you call Update without specifying a table name, the code assumes a default name of Table. If no table exists with that name, an exception is raised. The Update method takes as arguments an instance of a DataSet object and a DataTable object, or a DataTable name. The Update method first examines the RowState property of each table row. Then it prepares and calls a tailor-made INSERT, UPDATE, or DELETE statement for each inserted, updated, or deleted row in the specified DataTable object. Rows are scanned and processed according to their natural order (their position in the table’s Rows collection.)

note | The Update method has several overloads. It can take a pair given by the DataSet and DataTable objects, a DataTable object, or even an array of DataRow objects. The Update method returns an integer value representing the number of rows successfully updated.
Detecting Changes

The DataSet object’s GetChanges method returns a smaller DataSet object that includes only the rows changed in all the child tables. When you need to know about all rows changed in the DataSet object, just call GetChanges. When you need to know only about a given table, call the DataTable object’s GetChanges method. In this latter case, you get a DataTable object. One overload of the GetChanges method makes it particularly interesting in situations in which you need to control the sequence of statements run during the batch update process.

The GetChanges method, both for the DataSet and DataTable classes, can accept an optional argument indicating the state of the rows to be selected. For example, you might want to select only those rows that have been inserted to ensure that all the INSERT statements are issued before the UPDATE statements. In this case, you can split the batch update process into steps by getting and submitting the changes in small groups of rows as needed. The following code shows how to select only the new rows added to a table and ensure that they are submitted prior to the modified rows:

SqlDataAdapter da = new SqlDataAdapter();
DataTable dt = ds.Tables["MyWorkingTable"];
DataTable dtInserted = dt.GetChanges(DataViewRowState.Added);
DataTable dtModified = dt.GetChanges(DataViewRowState.Modified);
da.Update(ds, dtInserted);
da.Update(ds, dtModified);

GetChanges returns a potentially empty DataTable or DataSet object with all the changes that were entered since the last time changes to the table were committed using AcceptChanges. When you need to know only whether a DataSet object has pending changes, calling the HasChanges method will be more effective.

The HasChanges method returns a Boolean value that indicates whether the DataSet object has changes, including new, deleted, or modified rows. (The DataTable object has no equivalent method.) Like GetChanges, HasChanges can take an optional argument of type DataViewRowState and filter changes by action. The HasChanges method is incredibly useful for keeping your user interface up-to-date.

You can create a panel, such as the one shown in Figure 7-4, to contain the controls you need when the table has pending changes. You can make it automatically appear when needed by linking its visibility state to the value returned by HasChanges.

panelChanges.Visible = ds.HasChanges();
Figure 7-4
A control panel that automatically appears when a table has pending changes.

When you have pending changes, you should take care of a few tasks, such as providing a way for the user to view changes, reject all changes, reject individual changes, and submit all the changes to the server, reconciling the in-memory data representation with the results that you get from the user.

Rejecting Changes

In-memory changes made to the DataSet object can be accepted or rejected at three different levels: the DataSet object, the DataTable object, and the individual DataRow object. Calling the RejectChanges method rolls back all the changes made to the specified object since it was created, or since the last time AcceptChanges was called. Calling RejectChanges on a DataSet object invokes the RejectChanges method on all DataTable objects contained in the DataSet.

An application based on a DataGrid control can easily reject the changes in the DataSet object and refresh the user interface by using a pretty simple, short piece of code. This code is the event handler associated with the Reject button in Figure 7-4.

public void OnRejectChanges(Object sender, EventArgs e)
{
    // Retrieve the DataSet from the ASP.NET cache
    DataSet ds = GetDataFromCache();

    // Reject all the pending changes on the DataSet
    ds.RejectChanges();

    // Refresh the grid to reflect rejected changes
    BindData(ds);
}
Viewing Changes

An application that works in a disconnected, off-line manner must offer its users a way to review changes before committing or rejecting them. In ASP.NET, this complex function can be coded in a pretty straightforward way thanks to the power of the DataGrid control and the DataSet DiffGram format. The next code fragment demonstrates how to set up a filter for the DataGrid control so that it displays only the modified, inserted, or deleted rows of a table.

// Retrieve the changes from the ASP.NET cache
DataSet ds = GetDataFromCache();
DataSet dsChanges = ds.GetChanges();

// Set the filter on the grid to select only modified rows
DataView dv = dsChanges.Tables["Employees"].DefaultView;
dv.RowStateFilter = DataViewRowState.Added;
dv.RowStateFilter │= DataViewRowState.ModifiedOriginal;
dv.RowStateFilter │= DataViewRowState.Deleted;

// Refresh the grid with changes
gridChanges.DataSource = dv;
gridChanges.DataBind();

To filter a table based on the row state, you use the DataView object’s RowStateFilter property. Figure 7-5 shows the sample application when you try to review the current changes.

Figure 7-5
The current changes in the DataSet object displayed in a separate grid.

The grid that shows changes has an extra column for displaying the state of the row in text. The number-to-string conversion is accomplished by the ToString method called on the RowState property of each single row. The following ASP.NET code presents the template used to populate the rightmost column in Figure 7-5.

<asp:TemplateColumn runat="server">
    <itemtemplate>
        <b><%# ((DataRowView)Container.DataItem).Row.RowState.ToString() %>
        </b>
    </itemtemplate>
</asp:TemplateColumn>

The Container.DataItem object represents the view of data in the row that is being processed.

Restoring Modified Rows

As mentioned earlier, the .NET Framework allows you to reject changes for an individual row. So why not take the sample application one step further by adding a button to reject changes and restore the original state? You add a new button column with the following ASP.NET code. It creates a link button with a command name of reject. Beware of the command name—it is case-sensitive.

<asp:ButtonColumn runat="server" Text="Restore" CommandName="reject" />

Clicking a button column can be detected by the DataGrid control’s ItemCommand handler. It is a generic handler for all button clicks. You recognize the actual source of the event by using the command name.

public void ItemCommand(Object sender, DataGridCommandEventArgs e)
{
    if(e.CommandName == "reject")
    {...}
}

The first (and pretty obvious) issue to consider is that you cannot use the Select method to locate the row to process. You can’t use Select with deleted rows because they are no longer part of the table’s list of current rows and thus can’t be retrieved. In the grid, how can you get the DataRow object hiding behind each displayed row with pending changes? You have to collect the row by using the filtered view that takes care of displaying it. You obtain the current DataSet object from the cache and then build a DataView object like the one that we used earlier to show changes:

DataView dv = ds.Tables["Employees"].DefaultView;
dv.RowStateFilter = DataViewRowState.Added;
dv.RowStateFilter │= DataViewRowState.ModifiedOriginal;
dv.RowStateFilter │= DataViewRowState.Deleted;

From the DataView object, you easily access the needed DataRow object. The item index will point to the correct address in the DataView object’s collection element. DataView elements are objects of the DataRowView type. You obtain the underlying DataRow object by using the Row property and reject its changes:

DataRow row = dv[e.Item.ItemIndex].Row;
row.RejectChanges();
dv.RowStateFilter = DataViewRowState.CurrentRows;

At this point, changes must be reflected in the current DataSet object. You start this phase by resetting the row state filter for the view. This step is necessary to avoid unpleasant side effects with the controls that are bound to the DataTable object that the view is based on. Once the freshly updated DataSet object is successfully re-cached, all you have left to do is update the two grids: the grid with the current rows and the grid with the pending changes. Figure 7-6 shows the sample application with a pending change. As soon as the individual change is restored, the two grids are refreshed, as shown in Figure 7-7.

Figure 7-6
The sample application has some pending changes, and the user is about to restore one of them.
Figure 7-7
One of the pending changes has just been rejected and the original values restored.

One final word about this code: the DataSet object’s GetChanges method returns null when the DataSet object (or the DataTable object) has no pending changes. Handle this case carefully or an exception will be thrown.

note | Although both DataRowState and DataViewRowState describe row states, each results in different enumerations with different values and goals. The DataViewRowState values are used to retrieve a particular version of data from a DataRow object. The DataRowState values, instead, retrieve the current state of a row in regard to its relationship to the list of rows maintained by a table.
Generating Commands

The data adapter’s Update method loops through the rows of the specified DataTable object to detect which rows are changed. To slightly optimize the whole operation, you might want the adapter to work on a smaller table that includes only modified rows. You get this ad-hoc DataTable object by using the GetChanges method. As mentioned earlier, be cautious, because ADO.NET returns a null reference if no changes are detected.

note | When executing the Update method, the data adapter loops through the table rows. Based on the row state, the adapter determines which command has to execute. It copies row values into the command’s parameters, fires the RowUpdating event, and issues the command. Next the adapter fires the RowUpdated event to the application and calls AcceptChanges for the row. In light of this, using a temporary DataSet containing only changes rather than the whole DataSet results in a very minimal optimization. The adapter has fewer rows to loop through, but this does not change the number of computationally relevant operations—the database statements. Moreover, as we’ll see shortly, using a temporary DataSet can significantly complicate conflicts resolution.

Three operations can modify the state of a table: inserting a new row, deleting an existing row, or updating an existing row. For each of these key operations, the data adapter defines a tailor-made command object that is exposed as a property: InsertCommand, DeleteCommand, and UpdateCommand, respectively. You are responsible for assigning to these properties meaningful command objects—for example, the SqlCommand object.

note | The availability of the InsertCommand, DeleteCommand, and UpdateCommand properties is a quantum leap in functionality from ADO. They give you a chance to customize the way in which in-memory updates are submitted to the database. With ADO, you had no control over the SQL commands silently generated by the library. In ADO.NET, for example, you can control the update by using a stored procedure to perform batch updates and subsequently work with cross-referenced tables and non-SQL data providers.

The commands must run for each changed row in the table and be general enough to accommodate different values. Parameters are good for this kind of task, but you have to bind them to all the values of a database column. The data adapter object supports parameter objects that expose the SourceColumn and SourceVersion properties. SourceColumn gets or sets the name of the source column that is mapped to the DataSet object and is used for loading or returning the parameter value. SourceVersion indicates whether the original or current value has to be used as the parameter value. The following code shows how to bind the customerid field of the database to a given data adapter. The column metadata used to configure the parameter must match one of the columns in the table that will be part of the batch update.

SqlParameter p1 = new SqlParameter("@nCustomerID", SqlDbType.Int);
p1.SourceVersion = DataRowVersion.Current;
p1.SourceColumn = "customerid";
da.InsertCommand.Parameters.Add(p1);
The Insert Command

In the sample application, the INSERT command is defined as follows:

StringBuilder sb = new StringBuilder("");
sb.Append("INSERT Employees (firstname, lastname, titleofcourtesy, ");
sb.Append("sbtitle, country, address, postalcode) VALUES(");
sb.Append("@sFirstName, @sLastName, @sTitle, @sPosition, @sCountry, ");
sb.Append("@sAddress, @sPostal)");
da.InsertCommand = new SqlCommand();
da.InsertCommand.CommandText = sb.ToString();
da.InsertCommand.Connection = conn;

All the parameters are added to the data adapter’s Parameters collection and are bound to a DataTable column. Auto-increment columns must not be listed in the syntax of the INSERT command.

The Update Command

The UPDATE command needs to identify one particular row to apply its changes. You do this using a WHERE clause in which a parameter value is compared against a key field. Note in the following code that the parameter used in the WHERE clause must be bound by using its original value rather than the current value.

StringBuilder sb = new StringBuilder("");
sb.Append("UPDATE Employees SET ");
sb.Append("title=@sPosition, country=@sCountry, ");
sb.Append("titleofcourtesy=@sTitle, firstname=@sFirstName, ");
sb.Append("lastname=@sLastName, address=@sAddress, ");
sb.Append("postalcode=@sPostal ");
sb.Append("WHERE employeeid=@nEmpID");da.UpdateCommand = new SqlCommand();
da.UpdateCommand.CommandText = sb.ToString();
da.UpdateCommand.Connection = conn;

p1 = new SqlParameter("@nEmpID", SqlDbType.Int);
p1.SourceVersion = DataRowVersion.Original;p1.SourceColumn = "employeeid";
da.UpdateCommand.Parameters.Add(p1);
The Delete Command

Like UPDATE, the DELETE command requires a WHERE clause to identify the row to remove. You also need to use the original version of the row to bind the parameter value.

StringBuilder sb = new StringBuilder("");
sb.Append("DELETE FROM Employees ");
sb.Append("WHERE employeeid=@nEmpID");
da.DeleteCommand = new SqlCommand();
da.DeleteCommand.CommandText = sb.ToString();
da.DeleteCommand.Connection = conn;

p1 = new SqlParameter("@nEmpID", SqlDbType.Int);
p1.SourceVersion = DataRowVersion.Original;p1.SourceColumn = "employeeid";
da.DeleteCommand.Parameters.Add(p1);
The SelectCommand Property

In the programming interface of the data adapter object, along with InsertCommand, UpdateCommand, and DeleteCommand, you’ll find a rather unusual property named SelectCommand. This property serves two main purposes. First, it gives you a chance to use a stored procedure with or without parameters to populate a DataSet object. When you set the SelectCommand property with a valid command object and then call the Fill method, the DataSet object is filled with data. The second use for SelectCommand is to automatically create the commands used in the batch update.

Command Builders

When invoked, the data adapter’s Update method assumes that the necessary INSERT, UPDATE, and DELETE statements are specified. If the pending changes require the use of a command that isn’t specified, the Update method generates an exception.

Automatically Generated Commands

The update commands can also be generated automatically and made available to the data adapter update engine. When you create a command builder object (for example, SqlCommandBuilder), the command builder automatically generates the SQL statements needed for the batch update.

note | Command builders are an optional feature of .NET data providers. Command builders work in conjunction with data adapters to generate custom SQL commands that are used during the batch update.

Command builders cannot be used in all cases. The automatic generation of commands can take place only under certain circumstances. In particular, command builders do not generate anything when the table is obtained by joining columns from more than one table; they also don’t produce statements when calculated, or aggregate, columns are detected. Command builders are extremely helpful and save code only when they deal with single table updates. How can command builder objects know about the structure of the table so that they can generate update statements? They rely on SelectCommand.

note | Command builders also fail when column names or table names contain special characters, such as spaces, periods, and quotation marks, even when these characters are delimited by brackets.

A fundamental prerequisite for a command builder object is that the SelectCommand property of the data adapter points to a valid query statement. If this query contains INNER JOIN statements or calculated columns, and if the query does not return key column information, an exception is thrown and no further command is generated. A command builder is a smart kind of object because it recognizes whether the object it is called to create already exists. If it does, only those command properties you did not set are generated.

How the Command Builder Works

The command builder object works closely with the data adapter. When you use a command builder, you don’t need to set any of the adapter’s command properties with the objects generated by the builder. The Update method will ask the associated command builder to provide commands when needed, so beware of accessing the InsertCommand, DeleteCommand, and UpdateCommand properties after creating the command builder. These properties contain non-null objects only when you explicitly instantiate them.

A command builder object creates commands on demand and then caches them internally to serve further requests. It purposely exposes three methods: GetInsertCommand, GetUpdateCommand, and GetDeleteCommand. The association between the data adapter and the command builder is established through the builder’s constructor, as seen in the following code example:

SqlCommand cmd = new SqlCommand();
cmd.CommandText = "SELECT employeeid, postalcode FROM Employees";
cmd.Connection = conn;
dataAdapter.SelectCommand = cmd;
SqlCommandBuilder cb = new SqlCommandBuilder(dataAdapter);

To generate INSERT, UPDATE, and DELETE statements, the command builder uses the SelectCommand property, which retrieves metadata. If you change SelectCommand after the metadata has been retrieved (for example, after the first of a series of updates), call the builder’s RefreshSchema method to update the metadata. SqlCommandBuilder uses some of the properties defined in the SelectCommand object: Connection, CommandTimeout, and Transaction. When any of these properties are modified, call RefreshSchema to update the structure of the generated commands. You also need to call RefreshSchema when the SelectCommand itself is replaced. In all other cases, the InsertCommand, UpdateCommand, and DeleteCommand properties retain their previous values no matter how many updates you perform on the same adapter.

Only the fields listed in the SelectCommand object are actually part of the update. For example, in the code we just examined, only employeeid and postalcode are inserted or updated using the code generated by the command builder. For the command builder to work, one of these columns must be the primary key.

note | The command builder is able to detect whether a column is also an auto-increment column. If the column is an auto-increment column, the command builder correctly avoids adding that column in the list of fields populated by an automatically generated INSERT statement.
A Quick Look at Auto-Generated Commands

If you want to find out which command was automatically generated, you can obtain a reference to it by using the GetInsertCommand, GetUpdateCommand, and GetDeleteCommand methods. Once you hold the objects returned by these methods, you use the CommandText property to learn the details of the command being run.

Trace.Warn("InsertCommand", cb.GetInsertCommand().CommandText);

To see what kind of commands the SQL Server command builder generates, use the following command text for the SelectCommand object:

SELECT employeeid, firstname, lastname, postalcode FROM Employees

The employeeid field is the primary key and also an identity column. This dual purpose is a special condition and is correctly detected as the text for the INSERT statements, which look like the following:

INSERT INTO Employees( firstname , lastname , postalcode )
    VALUES ( @p1 , @p2 , @p3 )

A bit more interesting is the command text generated for the DELETE and UPDATE statements, which follows:

DELETE FROM Employees
    WHERE employeeid=@p1 AND firstname=@p2 AND lastname=@p3
        AND postalcode=@p4

UPDATE Employees
    SET firstname=@p1, lastname=@p2, postalcode=@p3
    WHERE employeeid=@p4 AND firstname=@p5 AND lastname=@p6
        AND postalcode=@p7

The part of the statements you should notice appears with the WHERE clause. When you want to ensure the correct row is identified, you usually don’t have to use such complex conditions involving all the columns. So why is the command builder implemented like this? The answer is pretty simple: to comply with the requirements of optimistic lock. A WHERE condition that involves a check on all the modified fields ensures that if the update command runs successfully, no conflict is detected and the underlying row is exactly as it was before it was read.

note

Regardless of the SQL command contained in the SelectCommand property, no rows are ever returned. How is this possible? The answer to the mystery is easily revealed by taking a look at what happens under the hood of the SQL Profiler. The command builder executes the SelectCommand query in a SQL batch in which the actual text of the command is preceded by the following statements:

SET FMTONLY OFF
SET NO_BROWSETABLE ON
SET FMTONLY ON

As a result, the query does not return rows but only column metadata information.

Once the batch update successfully terminates, you have to refresh the in-memory data and reflect any change made at the database level. For example, triggers or default values might have changed or just completed the inserted or modified rows. To make fresh data available on the client so that you can reconcile in-memory and source tables, you repeat the query command that populated the DataSet object. Unfortunately, the DataSet object does not track any information about the sequence of commands that originated it, but you can take responsibility for storing the SQL query command previously used to populate the DataSet object. The DataSet object’s ExtendedProperties cargo collection appears to be the ideal place to store it.

Data Conflicts

The logic behind automatic command generation is based on an optimistic vision of concurrency, in which each record is not locked after being read so that it remains exposed for other users to access for reading and writing at any time. With an optimistic vision of concurrency, a number of potentially inconsistent situations can occur. For example, a row can be modified or even deleted after it is returned from the SELECT statement but before the UPDATE statement is actually issued.

To avoid overwriting new data, the automatically generated UPDATE statement contains a WHERE clause that allows the command to be successful only if the rows contain original values and are not deleted. When such an UPDATE command fails to update the row, we have a conflict of data. In this case, the command does not affect the involved row but throws an exception of type DBConcurrencyException. The following code snippet demonstrates a more accurate way to execute a batch update operation with ADO.NET:

try
{
    dataAdapter.Update(ds, "Employees");
}
catch (DBConcurrencyException dbdcex)
{
    Response.Write("Conflict detected.");
}

The command builder always generates commands according to the principle of optimistic concurrency. If you want the UPDATE to complete regardless of the conflict between original and current values, you will need to use your own UPDATE command and not rely on automatic command generation. A command that writes the row without comparing original and current values has a much simpler WHERE clause, as shown here:

DELETE FROM Employees WHERE employeeid=@p1

UPDATE Employees SET firstname=@p1, lastname=@p2, postalcode=@p3
    WHERE employeeid=@p4

Basically, the WHERE clause now needs only to unequivocally identify the row to work on. A condition based on the primary key column can easily pay the bill.

Resolving the Conflict

The data adapter throws an exception of type DBConcurrencyException for the first row where the update fails. All modifications occurring prior to throwing the exception are considered committed. When the first conflict is detected, ADO.NET passes the control back to the client application, stopping the execution of the batch update. You access the DataRow involved in the conflictual update by using the Row property of the DBConcurrencyException class.

The way in which the conflict is resolved is application-specific. You could decide that the last value always overwrites the current value, whatever it is. Alternatively, you could apply an algorithm to determine resolution on a case by case basis, or even leave the decision to the user. When you leave the decision to the user, however, the prompt message must include the value proposed by the application along with the value originally read from the database and the value read while trying the update. The DataRow property returned by the Row property of the DBConcurrencyException object contains both the proposed value and the original value of the row. It does not contain the value currently stored in the database for the column. This value can be retrieved only with another query command. The code that runs when a concurrency exception is raised now looks like the following.

try
{
    dataAdapter.Update(ds, "Employees");
}
catch (DBConcurrencyException dbcex)
{
    // One conflict has been detected...

    // Load the current row from the database
    SqlCommand c = new SqlCommand();
    c.CommandText = "SELECT * FROM Employees WHERE employeeid=" +
        dbcex.Row["employeeid"].ToString();
    c.Connection = conn;
    conn.Open();
    SqlDataReader dr = c.ExecuteReader();

    // Store all the values in the row in an array
    dr.Read();
    Object [] rg = new Object[NUM_OF_ROWS];
    dr.GetValues(rg);

    // Close connection and data reader
    dr.Close();
    conn.Close();

    // Use an ad-hoc user control to display data
    conflict.Display(dbcex.Row, rg);
}

The GetValues method of the DataReader object packages into an array of objects all the values read in the currently selected row in the data reader. The sample code passes this array, together with the DataRow object that caused the conflict, to a tailor-made user control. The user control is shown in Figure 7-8.

Figure 7-8
A user control displays all the information about the conflicting row.

The next listing shows how the user control retrieves and displays the needed information.

<script runat="server">
public void Display(DataRow row, Object [] rg)
{
    StringBuilder sb = new StringBuilder("");

    // Loops through the columns that should be updated
    sb.Append("<table>");
    for(int nIndex=0; nIndex<row.ItemArray.Length; nIndex++)
    {
        if (!row[nIndex].Equals(row[nIndex, DataRowVersion.Original]))
        {
        try {
            sb.Append("<tr>");
            sb.Append("<td>");
            sb.Append("<b>Original: </b>");
            sb.Append(row[nIndex, DataRowVersion.Original].ToString());
            sb.Append("</td>");
            sb.Append("<td>");
            sb.Append("<b>Proposed: </b>");
            sb.Append(row[nIndex].ToString());
            sb.Append("</td>");
            sb.Append("<td>");
            sb.Append("<b>Underlying: </b>");
            sb.Append(rg[nIndex].ToString());
            sb.Append("</td>");
            sb.Append("</tr>");
            } catch {}
        }
    }
    sb.Append("</table>");

    // Display the resulting text
    lblInfo.Text = sb.ToString();
}
</script>

The proposed value is the value stored in the DataRow object that is returned via the exception. To get the original value stored in the row when it was originally read from the data source, you use an overloaded version of the DataRow object’s Item property. When you access a particular column in a row, you can add a second, optional argument, which is the version of the row you want, as shown in this example:

row[nIndex, DataRowVersion.Original]

Resolving a conflict means that you decide what value should be written to the database. After you do this, you run a new command with a lazier WHERE condition and then resume the batch update for all the rows not yet committed.

Accepting In-Memory Changes

A critical aspect of conflict resolution in ADO.NET is the way you manage the acceptance of changes while in the middle of a concurrency exception. In ADO the process is more simplistic because the first conflict rolls back all the successful changes accomplished in the meantime.

In ADO.NET, rows successfully updated when the first conflict is detected are not rolled back. Changes to these rows have also been committed. The Update method, in fact, implicitly calls AcceptChanges on the row after the update is completed. If you use the original DataSet for the batch update, then you can safely resume the process once the conflict has been resolved. Previously updated rows are now marked as unchanged and won’t participate in the batch update again. But what happens if you used a temporary copy of the original DataSet—for example, the copy you get through GetChanges?

In this case, the changes to the rows updated prior to the first conflict are committed but only on the temporary DataSet. This makes writing the code to resume the batch update more challenging. Basically, you need to figure out a way to make the original DataSet accept the changes to the rows processed prior to the one that originated the conflict.

note | The data adapter’s RowUpdated event occurs during the execution of the Update method after a command is executed against the data source. By registering a handler for this, you can check the status of the command and the row after the update completed. By handling this event, you decide whether errors should be treated as exceptions or ignored and skip to the next row.


Conclusion

In this chapter, we definitely uncovered a lot of interesting aspects of disconnected Web applications. Since data is rarely read-only, we focused on the batch update. ADO.NET provides much more flexibility than ADO when conducting a batch update. You can write application-specific commands for the INSERT, DELETE, and UPDATE operations, and you can control more closely how conflicts are generated and resolved.

In ADO.NET, you can run a batch update with rules you bolt onto the code yourself and never raise a single conflict. This is probably the easiest route to take, requiring you only to write custom commands and preventing you from relying on system-provided command builders. If you choose to use command builders, you must be ready to handle conflicts and deal with the in-memory commit mechanism and process resumption.


Interoperable Web Applications

Part III
Interoperability
Chapter 8
Interoperable Web Applications

Interoperability is an application’s ability to communicate and work side-by-side with other applications that are not necessarily targeted to the same platform. Along with scalability, it is one the most precious philosopher’s stones in the world of open computing. It is precious because it yields practical results. Interoperability is an elusive and unquantifiable basic element of computing, but you and your users can detect and appreciate its underlying presence. Implementations will differ according to the context and timing of your applications. You create the proper conditions for interoperability, but it is always the offspring of a successful marriage between design issues and real-world coding solutions.

Interoperability is like a common API that is callable from different platforms and computing contexts. Interoperability enables Microsoft Win32 and Microsoft Windows 3.x applications to talk to each other. Interoperability allows you to retrieve data from different data sources by using the same data access technology such as OLE DB or ODBC. Interoperability orchestrates the exchange of documents between applications regardless of formats, storage media, and platforms such as Microsoft BizTalk. Interoperability relies on universally accepted protocols and languages, and in particular takes advantage of XML (eXtensible Markup Language), which is a standard that specifies a syntax that allows you to create your own markup language.

Interoperability touches a number of areas in the .NET Framework. For example, it involves the interaction between managed code and existing COM components. It deals with remote objects and serialization to enable applications to communicate beyond the boundaries of their own application domains. Finally, interoperability involves the exposure of the data to external platforms.

In this chapter, I’ll examine a couple of these aspects: the interaction of a .NET Web application with existing COM components in general and with ADO in particular; and data serialization to customized XML formats. While covering the former point, I’ll explore the broader topic of migration from COM to .NET systems. While covering data serialization, I’ll describe a number of features in the .NET Framework, such as XML support and object serialization. To put all this into more practical terms, in this chapter, you’ll find concrete information about two main topics: interaction with existing business objects and formatting .NET data for use on other platforms.


The COM Interop Services

In .NET, the COM Interop Services (CIS) layer provides access to existing COM components in a codeless and seamless way without requiring modification of the original component. COM components and .NET classes are conceptually similar although they are based on different models. They don’t know how to interact with each other, but their inherent similarity makes it easy for you to build a wrapper that exposes COM components as .NET classes and vice versa. The system module that provides this kind of service is the CIS, which is made of two distinct parts: one part makes COM components usable from within .NET applications; and one part does the opposite, namely making .NET classes callable from within a COM component.

When you want to incorporate COM code into a managed .NET application, you import the relevant information found in the type library by using a utility named tlbimp.exe. The imported information is summarized in a new .NET class that retains the same name found in the type library and also mirrors its methods and properties. The involved types are adapted to the .NET type system. After the importation is complete, the original COM object is ready for use the .NET way.

To call .NET managed code from within a COM application, you need to map the .NET assembly metadata to COM type library information and registry entries. After the .NET class is made up to look like a COM object, any COM client can use it as though the class were a true COM class. In order to retrieve this .NET class, you need to mark it with some special attributes and use another utility (tlbexp.exe) to generate and register type library information. The MSDN documentation contains detailed tutorials that cover both aspects of COM Interop Services.


Using ADO in .NET Applications

Setting up a .NET application to use ADO objects is not really different from setting up the same application to take advantage of any other COM object. You can use COM objects from within any type of .NET application as long as you obtain an assembly that exposes the COM type library information as .NET metadata.

The .NET Framework offers a tool named tlbimp.exe that reads in a COM type library and generates an assembly that exposes to all .NET clients the functionality of the type library as a .NET class. Behind the scenes, the tool creates and compiles a class that exposes the same methods and properties as the original COM object. During this process, the names are preserved and the types involved are adapted to make them compatible with the .NET type system. The new class is also given some basic methods, such as ToString and GetType, that are not in the original definition but are standard parts of the arsenal of any .NET class.

The tlbimp.exe utility is located in the bin folder in the .NET Framework installation path. By default this folder is as follows:

C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin

The tool has the following command line. It takes the name of the file containing the type library information plus some options that rule its overall behavior.

tlbimp.exe typelibname [options]

The tlbimp.exe tool works fine either when the type library is embedded as a resource in the component code or when the type library is stored in a separate file. What matters is that the file name you provide contains the information to process. By default, tlbimp.exe generates an assembly with the same name as the type library. You can configure the assembly’s file name by using the /out command line switch. The command line needed to create an assembly for the ADO library is as follows:

tlbimp msado15.dll

You specify the name of the DLL that contains the ADO code and type library, and the tool generates an assembly named adodb.dll.

tip | Microsoft Visual Studio .NET automatically creates an assembly when you right-click on the Add References node in the Solution box of the project and then choose one or more COM libraries to import.
caution | Assemblies generated from COM libraries might not survive across different builds of the .NET Framework. Make sure you re-create them each time you install a different version of the system.
The ADODB Assembly

To use ADO objects in .NET applications, you don’t have to create the assembly because it is already part of the framework. It is located in the following folder along with a few other assemblies. All assemblies represent typical COM libraries you also might want to use in .NET applications.

C:\Program Files\Microsoft.NET\Primary Interop Assemblies

Let’s see how to configure an ASP.NET page so that it can use an ADO object such as Recordset. A Web page that uses ADO always ends up having a heading similar to this:

<%@ Page Language="C#" %>
<%@ Assembly Name="ADODB" %>
<%@ Import Namespace="ADODB" %>
<%@ Import Namespace="System.Data" %>

The @ Assembly directive links an assembly to the current page. The association takes place at compile time. A page that declares an assembly imports all its classes and interfaces for use within the page. You indicate a compiled assembly by name and do not use an extension. To use the classes in the assembly, you don’t need the @ Import directive. However, the following directive lets you call any class in the ADODB assembly (the Recordset class) without prefixing it with namespace information:

<%@ Import Namespace="ADODB" %>

You can create a new recordset with this code:

Recordset rs = new Recordset();

And you don’t have to use this code:

ADODB.Recordset rs = new ADODB.Recordset();

Using only the @ Import directive is never sufficient to successfully access ADODB or any other assembly.

Getting a Recordset

After the original COM type library has been turned into a .NET assembly and properly linked to a page, you are ready to use the original COM objects as regular .NET classes. The following code shows how to access the Northwind database to fill a Recordset object with some rows:

Recordset adoRS = new Recordset();
adoRS.Open(strCommandText,
    strConn,
    CursorTypeEnum.adOpenForwardOnly,
    LockTypeEnum.adLockReadOnly,
    1);

The original signature of the ADO Recordset object’s Open method is modified as follows:

void Open(object Source,
    object ActiveConnection,
    ADODB.CursorTypeEnum,
    ADODB.LockTypeEnum,
    int)

If you work with ADO in Visual Studio .NET, you’ll also have full Microsoft IntelliSense support. (See Figure 8-1.)

Figure 8-1
The IntelliSense feature of Visual Studio .NET at work with imported ADO Recordset objects.

When you hold a Recordset object, you can extract the information it contains and populate the page. Look at this example:

StringBuilder sb = new StringBuilder("");
while (!adoRS.EOF)
{
    sb.Append("<span>");
    sb.Append("<input type=checkbox name=theEmployee value=");
    sb.Append(adoRS.Fields["employeeid"].Value.ToString());
    sb.Append(">&nbsp;&nbsp;");
    sb.Append(adoRS.Fields["lastname"].Value.ToString());
    sb.Append(", ");
    sb.Append(adoRS.Fields["firstname"].Value.ToString());
    sb.Append("</span><br>");
    adoRS.MoveNext();
}
adoRS.Close();
Response.Write(sb.ToString());

The preceding code creates a list of check boxes based on the fields in the Recordset object. The output is shown in Figure 8-2. The full source code for the ImportAdo.aspx application is on the companion CD.

note | When you use ADO to access data, you must access the database by using the data source’s OLE DB provider.
Figure 8-2
A list of check boxes created from rows in the Recordset object.
ADO Server Cursors

What are the reasons you might use ADO to perform data access tasks from within .NET applications? I can list a few, some of them more compelling than others. One reason is that you can attempt to save at least a portion of your investments in your ADO code. Another is that using ADO instead of ADO.NET appears to minimize the learning curve for .NET. But really, the best reason to use ADO is that ADO.NET does not cover 100 percent of ADO functionality.

ADO.NET does not support server cursor types such as adOpenKeyset and adOpenDynamic, which are available in ADO. In the ADO.NET object model, no explicit notion of a cursor even exists, but you can see the cursor-like functionality in the data reader and the DataSet objects. The data reader object maps to an ADO Recordset object that uses the read-only, forward-only cursor. The DataSet object maps to an ADO Recordset object that uses the static disconnected cursor. When your application uses server cursors and you plan to move them to .NET, either you change the application’s architecture to avoid server cursors or you stick to ADO and forget about ADO.NET classes. Avoiding a change in the architecture would definitely be a good reason to import and use ADO classes from within a .NET application.

note

In a future release of the .NET Framework, the architecture of the .NET data provider will be enhanced to allow for server cursors. You should not expect it to be a general implementation of server cursors, but rather a database-specific implementation. Server cursors will be exposed to applications through a new method similar to Execute in the command class (ExecuteResultSet), and return an instance of a new data-oriented class (SqlResultSet). The following pseudo-code just gives you an idea of the future implementation:

SqlCommand cmd = new SqlCommand(...);
SqlResultSet rs;
rs = cmd.ExecuteResultSet(ResultSetOptions.Scrollable │
                          ResultSetOptions.Updatable);
while(rs.Read())
{
    if (rs["Column"] == valueToMatch)
    {
        rs["Column"] = newValue;
        rs.Update();    // persist changes back to the database
    }
}
rs.Close();
Migration Issues

If you think that importing ADO code in .NET applications will save you from rewriting large blocks of code, I’m afraid you need to think again. In most cases, your ADO code is written in Visual Basic, but Visual Basic .NET isn’t compatible with Visual Basic 6 code. You probably won’t have to edit every line of your code, but you will need to carefully review every character. Even if your ADO code is written in C++, your situation is not significantly improved. One of the issues you have to face, at least in the most common migration scenario, is the unavailability in .NET languages of optional parameters—a frequent shortcut for Visual Basic 6 programmers. The changes required to make your ADO code work in the .NET Framework are significant enough to make porting to ADO.NET worthwhile.

Another issue to consider is the lack of integration between ADO Recordset objects and .NET data bound controls. ADO Recordset objects are not native .NET objects, and this significantly affects (and sometimes prevents) the effective use of list bound controls such as the DataGrid and CheckBoxList controls. In ADO.NET, you will be able to reuse your ADO and OLE DB skills more easily than your existing ADO and OLE DB code. Importing ADO code in ADO.NET applications can save you valuable coding investments, but it does not use ADO.NET features optimally.

You can reach a good compromise between the reuse of existing code and the overall functionality of the resulting .NET applications when you obtain the ADO Recordset object directly from a COM object and import it in a DataSet object.


Adapting Recordset Objects to DataSet Objects

Just as any other data adapter class, OleDbDataAdapter provides a Fill method that callers can use to add tables to a DataSet object. The OleDbDataAdapter class is defined in the System.Data.OleDb namespace, and you typically use it as follows:

OleDbDataAdapter da = new OleDbDataAdapter();

DataSet ds = new DataSet();
da.Fill(ds, "MyTable");

The Fill method of the OleDbDataAdapter class has an interesting peculiarity. It can load into the target DataSet object not only the result sets created by the given command but also the contents of an existing ADO Recordset object. One of the overloads of the Fill method has the structure shown here:

int Fill(DataSet ds, Object adodb, String srcTable);

The data adapter copies each result set found in the ADO Recordset object into a table in the DataSet. As usual, the first table is named after the specified srcTable argument, and all the other result sets add a progressive index to the table name: MyTable1, MyTable2, and so on. The link between ADO and ADO.NET is a one-way binding that you can use only to copy data from ADO to the DataSet object. Any updates are performed by ADO.NET.

note | The OleDbDataAdapter class’s Fill method features several overloads that work with ADO data objects. For example, you can import a single Record object into a DataSet object’s DataTable object as well as into a stand-alone DataTable object.
Loading a Recordset Object into a DataSet Object

The Fill method loads the Recordset object into a given table in the specified DataSet object, regardless of how you actually obtained the Recordset object. The following code shows how to retrieve the Recordset object by using plain ADO code, load it into a DataSet object, and then bind it to a CheckBoxList control. The output is shown in Figure 8-3.

Recordset adoRS = new Recordset();
adoRS.Open(strCommand, strConn,
    CursorTypeEnum.adOpenForwardOnly, LockTypeEnum.adLockReadOnly, 1);

// Transforms a Recordset into a DataSet using the OleDbDataAdapter
OleDbDataAdapter oda = new OleDbDataAdapter();
DataSet ds = new DataSet();
oda.Fill(ds, adoRS, "MyTable");

// Bind to the CheckBoxList control
chkOutput.DataSource = ds.Tables["MyTable"];
chkOutput.DataTextField = "lastname";
chkOutput.DataValueField = "employeeid";
chkOutput.DataBind();
Figure 8-3
The contents of an ADO Recordset object bound to a ASP.NET CheckBoxList control.

The signature of the Fill method mandates that you specify the name of the table. When a table with the name you specified already exists in the target DataSet object, the contents of both tables are merged. If primary key information is present, duplicate rows are refreshed and appear only once. When creating a table, the Fill operation creates primary keys and constraints if the MissingSchemaAction property for the data adapter is set to AddWithKey. Filling a DataTable object with the contents of a Recordset object is just a special case of a more general filling mechanism.

Status of the Recordset Object

Typically, you call Fill on an open Recordset object. If the Recordset object is closed prior to the beginning of the operation, no exception is thrown. The data adapter assumes that the current result set in the Recordset object is closed and attempts to move to the next one. If another result set is found, the execution continues; otherwise the method terminates.

When the Fill method terminates, all the result sets have been successfully loaded in the target DataSet object, and the original Recordset object is closed. The Fill method returns the number of rows successfully added to or refreshed in the DataSet object.

Loading a Recordset Object into a DataTable Object

The Fill method can load the contents of the Recordset object directly into a DataTable object. The DataTable object might or might not be a member of a DataSet object. The signature of this overload is slightly different.

int Fill(DataTable dt, Object adodb);

When this implementation of the Fill method terminates, the input Recordset object is left open. In addition, when handling batch SQL statements that return multiple results, this implementation of the Fill method retrieves schema information only for the first result.

Loading the contents of a Recordset object into a DataTable object provides you with a bit more flexibility because you can handle the various result sets separately. You can decide whether to load them all or discard a few. You are responsible for closing the Recordset object when finished.

A Common Migration Scenario

Being able to pump data out of an ADO Recordset object into an ADO.NET object is a big step forward on the road to integration between ADO and ADO.NET systems. Let’s review a common migration scenario and see how you can elegantly connect new ASP.NET pages to the existing middle tier and data back-end. You begin the renovation of the Web application with the presentation layer, then handle the more problematic changes to the middle tier. Let’s assume that, as shown in Figure 8-4, you have a typical Windows DNA 2000 system made of ASP pages populated by a middle tier that uses ADO for all data access needs.

Figure 8-4
Only two moves are required to port a Windows DNA system to .NET.

Well written ASP pages do not include plain ADO code but rather call into COM objects, usually Visual Basic COM objects. Such middle tier and data tier components can have methods that return only ADO Recordset objects, which the page uses to fill out the HTML user interface.

Linking to a Visual Basic COM Object

From the .NET perspective, calling into the ADO library is not really different from calling into any other COM object. CIS is involved and you jump out of the CLR. You can make a Visual Basic COM object callable from within an ASP.NET page by following the instructions discussed earlier in this chapter.

Let’s suppose that you have a COM component whose progID is MyComp.NWData, which you use for data access. This object has one method with the following signature and code:

Public Function GetEmployees() As ADODB.Recordset
    Dim strCnn As String
    Dim strCmd As String

    strCnn = "PROVIDER=sqloledb;UID=sa; DATABASE=northwind;"
    strCmd = "SELECT firstname, lastname, title FROM Employees"

    Dim rs As New ADODB.Recordset
    rs.Open strCmd, strCnn, adOpenForwardOnly, adLockReadOnly, 1
    Set GetEmployees = rs
End Function

How do you use this code to retrieve a Recordset object from within an ASP.NET page? You get a .NET wrapper for the library and link the resulting assembly to the page. Visual Studio .NET can do this for you; otherwise, you use the tlbimp.exe utility.

note | Unless you adopted a special naming convention, chances are good that the Visual Basic DLL filename conflicts with the default name chosen by the tlbimp.exe utility. Visual Studio employs a naming schema that prevents this risk, but if you run the utility, you are responsible for renaming the assembly file.

Let’s assume that you successfully created a .NET assembly called MyComp.dll from the original Visual Basic COM DLL. You link this assembly to the ASP.NET page by using the following directives.

<%@ Assembly Name="MyComp" %>
<%@ Assembly Name="ADODB" %>

Both the assemblies must be visible to the ASP.NET run time and placed, for example, in the bin directory of the application. Since the method of the MyComp component returns an ADO Recordset object, you must also link with ADODB if you want your code to remain strongly typed. Figure 8-5 shows how the Intermediate Language Disassembler (ILDASM) reads the .NET version of the MyComp component.

Figure 8-5
The MyComp component analyzed by the ILDASM utility.
Invoking COM Methods

What happens from now on in the migration scenario is not really different from what we discussed earlier for plain ADO code. You create a new instance of the .NET class that represents the VB COM object. Next you invoke the needed method and get an ADO Recordset object.

MyComp.NWData mc = new MyComp.NWData();
ADODB._Recordset rs = mc.GetEmployees();

The OleDbDataAdapter class lets you convert this recordset into a DataTable object, making it bindable to any data bound ASP.NET control—for example, the DataGrid control.

OleDbDataAdapter da = new OleDbDataAdapter();
DataSet ds = new DataSet();
da.Fill(ds, rs, "MyTable");
grid.DataSource = ds.Tables["MyTable"];
grid.DataBind();

Figure 8-6 shows this technique in action in the AdoAdapterFromCOM.aspx sample application.

Figure 8-6
The contents of the DataGrid controls come through the .NET wrapper for a VB COM component.
Caution with Visual Basic 6 Components

Most COM components work if invoked from within ASP.NET pages, even when you call them by using the late-bound Server.CreateObject method—the typical way of creating object instances in ASP. By creating a .NET wrapper, you also improve performance because the component is early bound to the managed code of the page.

Most of the COM components developed using Visual Basic 6 work in the single-threaded apartment (STA). If you plan to use these components from within an ASP.NET page, you should mark the ASP.NET page with the aspcompat attribute:

<%@ Page aspcompat="true" %>

The aspcompat attribute forces the page to be executed on an STA thread. In this way, the page can call STA components, such as those developed with Visual Basic 6. Calling into STA components is not as fast as calling into native .NET components or imported free-threaded components.

If you omit setting the aspcompat attribute and call STA objects, the run time is expected to detect the situation and throw an exception. However, an exception will not be thrown when you use the STA component through the services of a .NET assembly. The ASP.NET run time has no way to detect that it is going to make STA calls because the calls are not direct and are processed by an intermediate proxy. As a result, your application is walking a tightrope suspended between poor average performance and lethal deadlocks.

COM+ Components

The range of the COM components you can call from within ASP.NET pages includes Visual Basic COM components registered with COM+ applications. Setting the aspcompat attribute to true also enables you to access the ObjectContext object so that you can interact with the COM+ run time.


From DataSet Objects to ADO Recordset Objects

Binding an ADO Recordset object to a .NET application is a one-way process that works seamlessly only for loading ADO data into .NET data structures. A number of design reasons make this binding quite acceptable from the .NET viewpoint, but when your main requirement is working with the existing middle and data tiers, to update data, you might need to convert .NET data objects back into ADO Recordset objects. The Recordset object is then passed as an input argument to another method on a COM or COM+ middle-tier component.

An ADO Recordset object is a publicly created object that can work disconnected from a data source. After you have created a new instance of the object, you add some fields and shape it up to reflect the desired structure. The code that does this is simple to write in Visual Basic 6 but turns out to be a bit more quirky to arrange in .NET because of method arguments that you cannot leave blank or unspecified.

Serializing DataSet Objects to XML Recordset Objects

Any interaction with the ADO library involves COM Interop Services. Any time you send a Recordset object over the network to or from a .NET environment, type conversion and marshaling are performed. In light of this, let’s consider an alternative approach to converting to ADO Recordset objects. We’ll serialize a DataSet object into a non-standard XML format, the XML schema used by ADO Recordset objects.

The DataSet object already provides for XML serialization, as we discussed in Chapter 7. The default schema used to store XML, however, is in no way similar to or compatible with ADO Recordset objects. The most significant difference between the XML serialization format of the DataSet object and the Recordset object is that whereas ADO.NET fully leverages XML Schema Definition (XSD), ADO relies on an early version of XSD named XML Data Reduced (XDR). The .NET Framework comes with a tool named xsd.exe that can convert an XDR schema into the newest XSD format. However, no tool is provided to perform the reverse operation of serializing a DataSet object into an ADO Recordset object. We need a tool capable of automatically converting an XSD schema (belonging to the DataSet object) into an XDR schema. You can handle this conversion programmatically.

XML Schemas for DataSet Objects

Before continuing this discussion any further and illustrating the code that persists the changes entered in a DataSet object to an XML ADO Recordset object, let me briefly explain again the main concepts behind XML schemas.

XML schemas are XML documents used to define the data model behind an XML document. The schema describes the structure of the XML data in much the same way column metadata defines the table schema in a database. An XML schema defines the structure of XML data items by using the XML Schema Definition language. XSD provides elements, attributes, types, and groups that you use to define structure, validate data, and navigate internal relationships between data. XML schemas are now a World Wide Web Consortium (W3C) recommendation.

When it comes to interoperability, XML schemas are compelling because they are a recognized way to describe data formats, and XML is broadly supported on all platforms. XML schemas can be used as a contract for data exchange between two interoperable applications. Even applications running on different platforms can send each other data formatted in XML. Each application understands the incoming message as long as it is written in XML and includes the proper schema.

note | Prior to XML schemas, Document Type Definition (DTD) was the preferred way to describe XML data. XML schemas have several advantages over DTDs. XML schemas use XML syntax and do not require you to learn a new language and employ another parser. XML schemas have in their type system reusable types, which are extensible and allow you to create new types through inheritance. XML schemas also can group elements and apply recurrence rules to elements and attributes.

As discussed in Chapter 7, ADO.NET DataSet objects can load from and save to XML formats. DataSet objects can read and write XML schemas by using the ReadXmlSchema and WriteXmlSchema methods. These methods are the object counterparts of the previously mentioned xsd.exe tool.

The ADO XML Schema

The next listing you’ll see shows a simple yet effective XML file that represents the records selected by the following query:

SELECT employeeid, firstname, lastname FROM Employees

The XML data is generated by the Save method of the ADO Recordset object:

<xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'
    xmlns:rs='urn:schemas-microsoft-com:rowset'
    xmlns:z='#RowsetSchema'>
<s:Schema id='RowsetSchema'>
    <s:ElementType name='row' content='eltOnly'>
        <s:AttributeType name='employeeid' rs:number='1' />
        <s:AttributeType name='firstname' rs:number='2' />
        <s:AttributeType name='lastname' rs:number='3' />
        <s:extends type='rs:rowbase'/>
    </s:ElementType>
</s:Schema>
<rs:data>
    <z:row employeeid='1' firstname='Nancy' lastname='Davolio'  />
    <z:row employeeid='3' firstname='Janet' lastname='Leverling'  />
    <z:row employeeid='5' firstname='Steve' lastname='Buchanan'  />
</rs:data>
</xml>

The DataSet object renders the same records as follows:

<MyDataSet>
<xsd:schema id="MyDataSet" targetNamespace=""
    xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   <xsd:element name="MyDataSet" msdata:IsDataSet="true">
     <xsd:complexType>
       <xsd:choice maxOccurs="unbounded">
         <xsd:element name="MyTable">
         <xsd:complexType>
           <xsd:sequence>
           <xsd:element name="employeeid" type="xsd:int" minOccurs="0" />
           <xsd:element name="firstname" type="xsd:string" minOccurs="0" />
           <xsd:element name="lastname" type="xsd:string" minOccurs="0" />
           </xsd:sequence>
         </xsd:complexType>
         </xsd:element>
       </xsd:choice>
     </xsd:complexType>
   </xsd:element>
</xsd:schema>
<MyTable>
    <employeeid>1</employeeid>
    <firstname>Nancy</firstname>
    <lastname>Davolio</lastname>
  </MyTable>
  <MyTable>
    <employeeid>3</employeeid>
    <firstname>Janet</firstname>
    <lastname>Leverling</lastname>
  </MyTable>
  <MyTable>
    <employeeid>5</employeeid>
    <firstname>Steve</firstname>
    <lastname>Buchanan</lastname>
  </MyTable>
</MyDataSet>

The root node <MyDataSet> represents the DataSet object, and the name of the tag is indeed the name of the DataSet object. The contents of the <MyTable> node can be consistently compared to the XML code for the Recordset object. The schema information has a more detailed data structure and single pieces of information are rendered using a node.

Creating an ADO XML Schema

In spite of the significantly different syntax, at the highest level of abstraction, the two XML schemas have something in common: both describe the columns and provide data for each row. Let’s see how to persist the contents of a DataSet table to an ADO XML file. The XML file can be loaded as a Recordset object by any existing COM component in the middle tier.

In Figure 8-7, you can see the user interface of the sample application. After the user clicks to load records, the selected database rows are displayed in a list of check box controls.

Figure 8-7
The sample application where you can check some records to be exported as members of an XML ADO Recordset object.

The records are retrieved from the database by using COM components in your middle or data tier. These records enter the context of the application via an ADO Recordset object and are then loaded into a DataSet object. As long as you work in a .NET environment, a DataSet object is undoubtedly more manageable than an ADO data container.

When the user checks a few records in the check box list and clicks to export those records to XML, the XML format must be understood by the COM middle-tier components that are actually updating data on the database server. For this understanding to happen, the .NET code must serialize, all or in part, the contents of the DataSet object to a custom XML schema.

note | The DataSet class does not provide a direct way to change the template of the XML code being generated. However, you won’t find changing the template particularly hard to accomplish as long as you know how to leverage XSL Transformations (XSLT). The output of the DataSet object serialization process is XML data, and the main purpose of XSLT is transforming XML data from one schema to another.
Writing the Root Information

The root tag of the ADO XML schema is <xml> and includes a few namespaces that qualify some tags used in body of the document. This part of the output is considered invariant:

StringBuilder rootInfo = new StringBuilder("");
rootInfo.Append("<xml xmlns:s='");
rootInfo.Append("uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882' \r\n\t");
rootInfo.Append("xmlns:dt='");
rootInfo.Append("uuid:C2F41010-65B3-11d1-A29F-00AA00C14882' \r\n\t");
rootInfo.Append("xmlns:dt='");
rootInfo.Append("uuid:C2F41010-65B3-11d1-A29F-00AA00C14882' \r\n\t");
rootInfo.Append("xmlns:rs='urn:schemas-microsoft-com:rowset' \r\n\t");
rootInfo.Append("xmlns:z='#RowsetSchema'>\r\n");
sw.Write(rootInfo.ToString());
rootInfo = null;

The text accumulated in the StringBuilder object is flushed into an output stream that is opened on a server-side, temporary XML file. This is an aspect of the solution that you might want to rework according to the characteristics of the system.

Writing the Schema Information

As you can see, the XDR schema employed by ADO is much simpler than the XSD schema of ADO.NET. The XDR schema comprises only one element, named row. It is marked as eltOnly, which means that the element can contain only the specified elements and no free text:

StringBuilder schemaOpenInfo = new StringBuilder("");
schemaOpenInfo.Append("<s:Schema id='RowsetSchema'>\r\n\t");
schemaOpenInfo.Append("<s:ElementType name='row' content='eltOnly'>\r\n");
sw.Write(schemaOpenInfo.ToString());
schemaOpenInfo = null;

The critical information stored in the schema includes the column names and other information about the columns. The contents of each column are stored through attributes of the row element. The names of the allowable attributes are listed after each column name. Each column has a unique identifier:

int index=0;
foreach(DataColumn dc in ds.Tables[0].Columns)
{
    StringBuilder colInfo = new StringBuilder("");
    index ++;
    colInfo.Append("\t\t<s:AttributeType name='");
    colInfo.Append(dc.ColumnName);
    colInfo.Append("' rs:number='");
    colInfo.Append(index.ToString());
    colInfo.Append("' />\r\n");
    sw.Write(colInfo.ToString());
    colInfo = null;
}

After the column-specific information is written, you close the element tag and overall schema tag:

StringBuilder schemaCloseInfo = new StringBuilder("");
schemaCloseInfo.Append("\t\t<s:extends type='rs:rowbase'/>\r\n");
schemaCloseInfo.Append("\t</s:ElementType>\r\n");
schemaCloseInfo.Append("</s:Schema>\r\n");
sw.Write(schemaCloseInfo.ToString());
schemaCloseInfo = null;
Writing the Data

Your only remaining task is writing the actual data. The data of the Recordset object is bracketed by the <rs:data> tag. The recordset is expected to include only the records selected in the check box list. As shown in Chapter 1, you enumerate the contents of the Items collection of the CheckBoxList control and verify the Boolean state of the Selected property.

sw.WriteLine("<rs:data>");

// Collect only the rows with a selected item
foreach(ListItem item in chkOutput.Items)
{
    if (item.Selected)
    {
        StringBuilder data = new StringBuilder("\t<z:row ");
        DataRow[] rgRows;
        rgRows = ds.Tables[0].Select("employeeid=" +
            item.Value.ToString());
        DataRow row = rgRows[0];
        foreach(DataColumn dc in ds.Tables[0].Columns)
        {
            data.Append(dc.ColumnName);
            data.Append("='");
            data.Append(row[dc.ColumnName].ToString());
            data.Append("' ");
        }
        data.Append(" />");
        sw.WriteLine(data.ToString());
        data = null;
    }
}
sw.WriteLine("</rs:data>");

If the nth item is selected, you retrieve the underlying DataRow object by using the Select method of the DataTable object, and the primary key value stored in the CheckBoxList object’s Value property. (Storing the primary key in the Value property of CheckBoxList is not mandatory but does make good sense in almost all situations.)

Figure 8-8 shows the sample application using a <iframe> tag to show the content of the freshly generated XML file as rendered by the following code:

frXmlRecordset.Attributes["src"] = Server.MapPath("adorecordset.xml");
Figure 8-8
The sample application successfully exported the checked records to an XML document.
What About Data Types?

You probably noticed that in our XML document representing a subset of the data originally stored in a DataSet object, no type information exists. Earlier in the chapter, I defined the XML schema I was going to build as a “simple yet effective” schema. Well, the lack of explicit type information is the main reason I made that statement.

The XML document we have built so far is technically legal and correct. All Visual Basic applications will easily accept it and use it to build in-memory ADO Recordset objects. However, that XML document is simple and appropriate only for tasks that are not much more complicated than displaying text-based data. All the various pieces of information in the document are rendered in the same way, that is, using Unicode strings. Making those fields type-aware means adding some type information to the <AttributeType> node in the XML schema. You do this using a pair of attributes in the dt namespace—one of the namespaces defined in the root node:

<s:AttributeType name='lastname' rs:number='2'>
    <s:datatype dt:type='string' dt:maxLength='20' /></s:AttributeType>

The <s:datatype> element describes the type of corresponding character data used in the parent attribute value. The main attribute of <s:datatype> is the dt:type attribute. For variable-length data types, XDR also allows you to specify a maximum length via the dt:maxLength attribute.

The .NET type system and the ADO Recordset object recognize different types. And ADO types are, in turn, different from predefined XDR data types. You can’t obtain the XDR data type that corresponds to a .NET Type object—the only type information available for an ADO.NET column object—in a straightforward way. Whenever type information is critical for the health of your application, you should figure out how to map a DataTable object’s column .NET type to an XDR type. In fact, you should exhaustively consider each .NET type and map it to an element in another set of data types. The mapping process won’t be particularly hard, just boring.

From .NET Data to XML

So far we have mostly focused on how to make the contents of a DataSet object available to clients regardless of platform. Because of its inherently high level of interoperability, XML is the perfect data description language to use. Saving a table of data to XML allows .NET (and ADO) applications to interoperate with applications written for non-Windows platforms and written in a variety of programming languages. The flexibility of XML schemas dramatically increases the effectiveness of an XML-based approach. XML allows you to model the data the way you want and need. XML schemas allow you to provide an XML-based description of the data model you used. By publishing the schema along with the data, you enable your clients and your partner applications to easily import and understand the data.

Historically, the demand for data interoperability was associated with the need to retrieve rows of data from a data source, thus producing data in relational, tabular format. Tailor-made frameworks (such as Microsoft Foundation Classes and the Java set of classes) provided their own way to dump living instances of objects to streams of bytes. Needless to say, this dumping approach offered no form of cross-framework interoperability. Today the recognized success of XML and SOAP (Simple Object Access Protocol) make object serialization a much more compelling choice. Any framework-specific object can be exposed to other applications on other platforms. Thanks to XML and XML schemas, any living instance of a framework-specific object can morph into a living instance of another object that could comply with a radically different component model and memory format. In the .NET Framework, the challenge of XML object serialization is taken very seriously. As a result, you can see any .NET object through the lens of interoperability and plan to publicly expose all of your own classes (and not just DataSet objects) via XML. The ubiquity of XML and SOAP allows you to use .NET classes on Microsoft Windows XP and then use them on a Solaris platform, all dressed up as Java classes.

In general, serialization is the process that converts an object or a graph of objects into a linear sequence of bytes. You can then use the resultant block of memory for either storage or transmission over the network on top of an ad-hoc protocol. In .NET, object serialization can have three different output forms: binary, SOAP, or XML.

XML serialization is the process that converts the public interface of an object to a particular XML schema. Such a mechanism is widely used throughout the whole .NET Framework as a way to save the state of an object into a stream or a memory buffer. For example, ASP.NET Web Services (which I’ll cover in Chapter 9) use the XmlSerializer class to encode messages. Also, any object reference that ASP.NET applications store in the Session object is serialized to a binary format before being stored.


XML Object Serialization

In the .NET Framework, object serialization is offered through the classes in the System.Runtime.Serialization namespace. These classes provide type fidelity and support deserialization. Deserialization is the reverse process of serialization. It takes in stored information and re-creates objects from it.

Object serialization in .NET allows you to store public, protected, and private fields, and it automatically handles circular references. A circular reference occurs when a child object references a parent object, and the parent object also references the child object. Serialization classes in .NET can detect these references and work them out. Serialization can generate output data in multiple formats by using different made-to-measure formatter modules. The two system-provided formatters are represented by the BinaryFormatter and SoapFormatter classes, which write the object’s state in binary format and SOAP format, respectively.

Enabling Object Serialization

A .NET class that implements the ISerializable interface is serializable. A class that does not implement the interface must be marked with the SerializableAttribute attribute to be serializable. When you want to keep the serialization lean, prevent some fields from entering the serialization process by applying the NonSerializedAttribute attribute to them. You might not want to serialize fields that represent transient states or that are easily reproducible at run time. The following code snippet shows you how to mark .NET classes for serialization:

[Serializable]class Order {
    int price, numItems;
    [NonSerialized] int total;
    ...
}

The whole Order class is serializable except for the total member. In our example, you don’t have a compelling reason to save total because its value can be easily reproduced at any time by multiplying price and numItems.

To serialize an object to a storage medium, you first select a serialization formatter, typically the binary formatter or the SOAP formatter. Each formatter has its own class:

IFormatter binFormatter = new BinaryFormatter();
IFormatter soapFormatter = new SoapFormatter();

After you hold a living instance of the serializer, you call the Serialize method, passing the stream to write to and the object to save. You can also serialize an object to a file, a memory buffer, and even a string. When you need to serialize a graph of objects, pass only the root object to the Serialize method:

soapFormatter.Serialize(stream, rootObject);

If you used the SOAP formatter, the following is a plausible output:

<SOAP-ENV:Envelope
    xmlns:xsi=http://www.w3.org/1999/XMLSchema-instance
    xmlns:xsd="http://www.w3.org/1999/XMLSchema"

    xmlns:a1="...">
<SOAP-ENV:Body>
    <a1:Order id="ref-1">
        <price>1</price>
        <numItems>2</numItems>
    </a1:Order>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Rebuilding objects from a storage medium is also easy. You call the Deserialize method on the specified formatter:

Order rootObject = (Order) soapFormatter.Deserialize(stream);

It goes without saying that you cannot serialize to, say, SOAP, and then pretend to deserialize using the binary formatter.

The ISerializable Interface

A class that needs to control its serialization and deserialization processes can do this by implementing the ISerializable interface. When a class implements the ISerializable interface, the formatter calls the GetObjectData method of the interface at serialization time. GetObjectData then populates the supplied SerializationInfo structure with all of the data that represents the object. A class that implements ISerializable must also provide a private constructor with the following signature:

private MyClass(SerializationInfo info, StreamingContext context)

Such a constructor is called when the formatter is finished filling out the SerializationInfo structure. The streaming context information lets the class know about the destination of its bytes. Depending on whether the class will be serialized in a cross-process or cross-machine way, some fields might require special treatment. In general, this constructor should be declared private when the class is not explicitly marked as not inheritable.

Controlling the Deserialization Process

The .NET serialization mechanism also allows you to control the post-deserialization processing and explicitly handle data being serialized and deserialized. In this way, you are given a chance to restore transient state and data which, for one reason or another, you decide not to serialize.

By implementing the IDeserializationCallback interface, a class indicates that it wants to be notified when the deserialization of the entire object is complete. The class can easily complete the operation by re-creating parts of the state and adding any information not made serializable. The OnDeserialization method is called after the type has been deserialized. For example, you wouldn’t serialize member information for the current time; you can instead mark the field as non-serialized and implement the IDeserializationCallback interface to restore a consistent value at the end of the deserialization process:

[NonSerialized]
public DateTime d = DateTime.Now;
public void OnDeserialization(Object sender)
{
    d = DateTime.Now;
}
Type Binding

In .NET, you can bind one class type to another at the end of the deserialization process by first making a serializable class inherit the SerializationBinder class behavior. Then you implement the BindToType method, which controls the binding of a serialized object to a .NET class:

public abstract Type BindToType(
    String assemblyName,
    String typeName
);

The second step is setting the Binder property of the formatter of choice, typically SoapFormatter or BinaryFormatter. You set the formatter to operate on an instance of your binder class. After the operation completes, you serialize and deserialize as usual. The binder automatically maps involved types, and the assembly for the original class doesn’t even have to be loadable. This is an excellent solution for mapping between different versions of the same class.

Serializing to XML

A second, very special, type of .NET serialization is XML serialization. When compared with ordinary .NET object serialization, XML serialization is so different that you should not even consider it another type of formatter. It is similar to SOAP and binary formatters because it also persists and restores the object’s state, but when you examine the way each serializer works, you see many significant differences.

XML serialization is handled by using the XmlSerializer class, which enables you to control how objects are encoded into XML. You can use the xsd.exe tool to generate base classes that the serializer encodes to XML according to an XSD schema. The namespace for XML serialization is System.Xml. Serialization.

You can configure to some extent the process of XML serialization. For example, you can specify whether a property should be encoded as an attribute or an element and provide a name for that property when the default is inappropriate. You can also provide an XML namespace for elements.

Type Fidelity and Infidelity

The primary goal of .NET object serialization is fidelity between the original object and its serialized version. However, type infidelity is the goal of XML serialization. XML serialization is a feature whose expressed purpose is to facilitate interaction and interoperability between complex .NET objects and non-.NET platforms. This declared goal brings to light a number of limitations. For example, XML serialization supports only public members and cannot serialize graphs of objects (such as a doubly linked list). Circular references are not detected and lead to endless loops. Only the data contained in the class is serialized, and no type and assembly information data is ever included.

Exporting Data to XML

Let’s review the typical process that leads to creating XML documents that reflect the contents of living instances of .NET classes. You start out with an XSD document that describes the structure of the resultant data. If you don’t have and can’t write an XSD document, you use the xsd.exe tool to infer the document from an XML file.

Based on the XSD schema, you generate a C# or Visual Basic .NET class, which has the ability to serialize itself in an XML format that adheres to the specified schema. The class is generated using the xsd.exe tool, which is well hidden under the covers of the Visual Studio .NET user interface. This class is a template class that you need to flesh out a bit by adding functionality. Manipulating data with such a class has a double advantage. When you work under the aegis of .NET, you enjoy the effectiveness of strongly typed programming. When you move out of .NET and route your classes toward other platforms, you enjoy the interoperability and the flexibility of an XML schema.


Conclusion

Under the umbrella of interoperability falls a variety of programming issues. One of the primary issues concerns the protocols that allow for communication, interaction, and data exchange. Another critical issue is the schema of the data being exchanged. In recent years, we have observed the standardization process, which moves forward incrementally and is far from complete.

HTTP (Hypertext Transfer Protocol), which is broadly accepted and supported, was the first step to building a programmable Web. HTTP standardized the transportation layer of the anticipated infrastructure. To make a long story short, HTTP is always a valid protocol for communicating with remote applications.

Raw ASCII text was the natural type of information to exchange using HTTP, and although it worked well for a large number of applications, it wasn’t appropriate for all of them. In the programming world, as in everyday life, the more you have, the more you want, and the more precise and circumstantial your demand is. So although ASCII text worked pretty well, it wasn’t good enough because you couldn’t tag all its information, and a better option was needed. XML surfaced as the standard way to describe data across machines and platforms. Shortly after XML came SOAP and, for you, the ability to use standard techniques to issue remote method calls.

XML schemas and Web Services nearly complete the puzzle of interoperability. After these pieces are fully integrated in our work, all we need is a standard way to search and query for data. As I write this book, this standardization is under way.

In the next chapter I’ll delve into .NET Web services and its cross-platform applicability. In this chapter, you learned about the concept of interoperability. We started out making old (Windows DNA) and new (.NET) Windows systems work together, and then we tackled the more general challenge of making data available outside the native platform—in this case, the .NET platform. XML (including schemas and serialization) can make data widely available as can object serialization in the context of .NET.


Web Services

Chapter 9
Web Services

The Web service is the quintessence of the Web as we know and use it today. Even though the term Web service has been around only a short while, the concept is nothing new. You can think of a Web service as an abstract interface, first implemented in the HTML-based interaction between a user and a Web server. When a user invokes a URL, he is invoking a default method on a server-side module that provides some services. The return value is the plain old HTML page, where raw information is interspersed with graphics and layout information.

Take this idea one step further, and you get the broadly accepted definition of a Web service. A Web service is a software application that can be accessed over the Web by other software. A Web service is applicable to any type of Web environment: the Internet, an intranet, or an extranet. A URL is all the user needs to locate and access a Web service. In theory, a number of Internet-friendly protocols might be working under that URL. In practice, the protocol for Web services is always HTTP. A Web service has only one type of consumer: other software. Users can certainly access a Web service, but only by means of other tailor-made software.

The software community’s recent focus on the term Web service suggests that the technology—regardless of platform or philosophical approach—is finally sophisticated enough to generalize and parameterize the software mechanism used to access URLs. Web services have extended and made more powerful the programming interface for accessing Web applications. Instead of using only one default method to access a URL and return HTML, you can use a variety of more specialized methods with customized signatures. Most important of all, you have programmatic support from the development environments on a variety of software platforms (Microsoft .NET, Oracle, and Java). You can now manipulate a Web service by its software elements—an object, a class, a function, or anything else exposed by the interface of the environment.


Dynamic Link Web Libraries

Nobody invented Web services (at least not within the past two years), but all the main players in the Information Technology (IT) arena are rapidly adopting and transforming the idea of “software callable by other software” into solutions that fit all development platforms.

No matter how a Web service is created, by which vendor or platform, the way it is exposed to the public is standard. Any Web service can be imported and incorporated into a vendor-specific or platform-specific solution, as long as it adheres to accepted standards. Web services guarantee interoperability because they are based entirely on open standards. By rolling functionality into a Web service, you can expose it to any application on the Web that speaks HTTP and understands XML. Of course, some infrastructure that deals with Web communication and data transportation is required. No worries, though—the major IT players are building this infrastructure into their development platforms.

An emerging view of Web services regards them as Web libraries of functions. Just as Microsoft Windows Dynamic Link Libraries (DLLs) are shared pieces of code from which processes can import common functionalities, Web services are platform-independent, URL-accessible, dynamic link collections of functions.

Compared with DLL functions or COM objects, Web services are a very special breed of service-oriented systems. They do not require custom protocols for communication. For example, a COM client must use COM protocols to speak to a COM service. You can see that COM is not as pervasive on the Web as Web services, and it also suffers from firewalls that might routinely block communication on certain ports. Web services based on open standards such as HTTP and XML are the leading technology for system interoperation.

Web Service Specifications

A Web service has three important characteristics. First and foremost, a Web service is accessible over the Web by using a URL. If needed, its public set of functionalities can be protected in some way from unauthorized use through common programming techniques, including authentication and user profiling.

Second, Web services communicate with the outside world by using XML messages sent over standard Web protocols. HTTP is by far the primary transportation protocol, but others can be used as well—for example, asynchronous protocols such as SMTP and even MSMQ. As you learned in Chapter 8, SOAP is the XML-based protocol used to package the messages exchanged between the service and the user. The XML schemas technology also plays a key role because it provides the underlying type system for SOAP to build its own model of data.

Third, a Web service is registered on a public registry and, most important of all, has its descriptive information published in a standard way so that potential clients can discover it and instantly know how to access the methods.

Underlying Technologies

A variety of technologies cooperate to support the infrastructure for Web services. All are open technologies that either are already recommended by the W3C or are maturing quickly and, as such, are commonly perceived as de facto standards. A Web service communicates by exchanging XML packets over standard Internet protocols such as HTTP. The Web service back-end contains the code that produces and consumes such messages, which are mostly written according to the SOAP specification.

A Web service can be exposed through a global registry of applications. In this sort of Web yellow pages, companies provide both contact information and product and service information. Buyers are free to search the registry and locate companies that provide the services they need. The organization named Universal Description, Discovery, and Integration (UDDI) defines the specification and the rules that govern this process.

Web service discovery is the process of locating XML documents that describe the functionality of a particular Web service. Through the discovery process, a potential client learns that a Web service exists and where to find more information about it. The list of functions is supplied through the Web Services Description Language (WSDL).

The outermost envelope that wraps Web services is fully open and based on widely accepted standards, but the features of the underlying platform really make Web services run. Let’s learn more about writing and invoking Web services in .NET. For more about Web services, you might also want to check out Building XML Web Services for the Microsoft .NET Platform, by Scott Short (Microsoft Press, 2002).


.NET Web Services

A .NET Web service is a class that inherits from System.Web.Services.WebService. This class is placed in a source file and saved with an .ASMX extension. Files with an .ASMX extension are a particular flavor of ASP.NET pages. The .ASMX file contains a directive that informs the ASP.NET run time about the nature of the file, the language in use throughout, and the main class that implements the service:

<%@ WebService Language="C#" Class="NWService" %>

The main class must match the name declared in the Class attribute and must be public:

[WebService(Namespace="bwslib/0735615780")]
public class NWService : WebService
{

}

Indicating the base class for a .NET Web service is not mandatory. A Web service can also be architected starting from the ground up using a new class. Inheriting the behavior of the WebService class has some advantages, though. A Web service based on WebService has direct access to some common ASP.NET objects including Application, Request, Cache, Session, and Server. These objects are packed into an HttpContext object, which includes the time when the request was made. By using the WebService base class, a Web service also sees the ASP.NET server User object, which can be employed to verify the credentials of the user who is executing methods.

The .NET CLR supports keyword-like annotations to qualify the run-time behavior of programming elements such as classes and methods. These annotations are called attributes and are saved with the assembly metadata. The .NET Framework supplies many predefined attributes and also allows you to design your own. In the preceding code snippet, the WebService tag that prefixes the Web service class is just a system-provided attribute type that influences how the Web service is constructed and exposed.

The WebService Attribute

The WebService attribute is optional and does not affect the activity of the Web service class as to what is published and executed. The WebService attribute is represented by an instance of the WebServiceAttribute class. The WebService attribute allows you to change three default settings for the Web service: the namespace, the name, and the description.

The syntax for configuring a Web service attribute is declarative and rather intuitive. Within the body of the WebService attribute, you simply insert a comma-separated list of names and values. The keyword Description identifies the description of the Web service, whereas Name points to the official name of the Web service.

[WebService(
    Name="Northwind Info Service",
    Description="The Northwind Web Service")]
public class NWService : WebService {

}

Changing the name and description of the Web service is mostly a matter of consistency. The name of the Web service defaults to the name of the class that implements it, and no description is provided. The Name attribute is used to identify the service in the WSDL text that explains the behavior of the service to clients. The description is not used in the companion WSDL text but is only retrieved and displayed by the IIS default page for URLs with an .ASMX extension. The attributes just discussed originate the page shown in Figure 9-1.

Figure 9-1
The standard page that IIS shows to let you test .NET Web services.
Changing the Default Namespace

Each Web service should have a unique namespace that allows potential clients to distinguish it from other namespaces. By default, each .NET Web service is given the same namespace: http://tempuri.org. You should change it as soon as possible prior to publishing the service on the Web. Using a temporary name does not affect the overall functionality, but will affect consistency and violate Web service naming convention. Although most namespace names out there look like URLs, you don’t need to use URLs. A unique name suffices. You change the default namespace of a .NET Web service by using the Namespace property of the WebService attribute, as shown in the following example in which I’ve used the ISBN of this book.

[WebService(
    Namespace="bwslib/0735615780",
    Name="Northwind Info Service",
    Description="The Northwind Web Service")]

The namespace information is used extensively in the WSDL definition of the Web service.

note | When you read documentation about Web services, you are usually bombarded by a storm of similar-sounding acronyms: URI, URL, URN. Let me clarify them for you. A Uniform Resource Identifier (URI) is a string that unequivocally identifies a resource over the network. There are two types of URI: Uniform Resource Locator (URL) and Uniform Resource Name (URN). A URL is given by the protocol prefix, the host name or IP address, optionally the port, and the path. A URN is simply a unique descriptive string, for example, the human-readable form of a CLSID is a URN.
Defining Web Methods

A Web service class is a regular .NET class, so it can have public as well as protected or private members; however, a Web method must be public to be effectively exposed over the Web. This public method belongs to the class that is marked with the WebMethod attribute, adding another level of visibility to the members of a .NET class. The key advantage of belonging to a class marked with WebMethod is that you can use the same class to expose functionality to both internal components of the system architecture and public consumers accessing the service via a URL. A software component allowed to instantiate the Web service class sees all the public methods and does not necessarily recognize the service as a Web service. However, when the same component is invoked as part of a Web service, the IIS and ASP.NET infrastructure ensures that external callers can see only methods marked with the WebMethod attribute. Any attempt to invoke untagged methods via a URL results in a failure.

The WebMethod Attribute

The WebMethod attribute has several properties that you can use to tailor the behavior of a Web method. Table 9-1 lists the properties supported by the attribute.

Table 9-1 Properties of the WebMethod Attribute
Property | Description
BufferResponse | Set to true by default, this property indicates that ASP.NET should buffer the entire method’s response before sending it to the client. You typically set the property to false only if you know that the method returns large amounts of data. If set to false, the response is buffered but only in chunks of 16 KB.
CacheDuration | Specifies the length of time ASP.NET should cache the response of the method. This information is useful when you can foresee that the method will handle several calls in a short period of time. The duration is expressed in seconds and disabled by default. (The default is 0.) Caching recognizes distinct parameter values.
Description | Provides the description for the method. The value of the property is then written to the WSDL for the service.
EnableSession | Set to false by default, this property makes available to the method the Session object of the ASP.NET environment. Depending on how Session is configured, using this property might require cookie support on the client.
MessageName | Allows you to provide a publicly callable name for the method. Set the name of this property, and the resulting SOAP messages for the method target the name you set instead of the actual name. Use this property to give distinct names to overloaded methods in the event you use the same class as part of the middle tier and a Web service.
TransactionOption | Specifies the level of COM+ transactional support you desire for the method. A Web service method can have only two behaviors regardless of the value assigned to the standard TransactionOption enumeration you select: either it does not require a transaction or it must be the root of a new one.

The following code snippet shows how to set a few method attributes:

[WebMethod(MessageName="GetAllEmployees",
    CacheDuration=60,
    Description="Information about all the employees")]
public DataSet GetEmployees() {
}

[WebMethod(MessageName="GetSomeEmployees",
    CacheDuration=60,
    Description="Information about the specified range of employees")]
public DataSet GetEmployees(int nFirstEmpID, int nLastEmpID) {
}

[WebMethod(MessageName="GetEmployee",
    Description="Information about the specified employee")]
public DataSet GetEmployees(int nEmpID) {
}

Notice that the Web service class has one method with three overloads. Those methods are exposed over the Web by using distinct names. Notice also that attributes must be strongly typed in the declaration. In other words, the value you assign to CacheDuration must be a true number and not a quoted string containing a number, which would be the case in the layout of an ASP.NET page.

For the declarations in the preceding code, the standard IIS page for the Web service would look like that in Figure 9-2. The nwservice.asmx sample application that produces this output is available on the companion CD.

Figure 9-2
The sample Northwind Info Service shows off the overloaded methods.
Transactional Methods

The behavior of a Web service method in the COM+ environment deserves more attention. The stateless nature of HTTP prevents Web services from enlisting themselves in running transactions. In the case of a rollback, for example, tracking and canceling performed operations would be rather problematic. For this reason, a Web method can work only outside the scope of a transaction or start a transaction. The Disabled, NotSupported, and Supported values from the TransactionOption enumeration always cause the method to execute without a transaction. Both Required and RequiresNew create a new transaction.

When the method throws an exception, or an externally thrown exception is not handled, the transaction automatically aborts. Otherwise, when no exceptions occur, the transaction automatically commits. Of course, if the method calls SetAbort, the transaction always fails.

When your method needs to work within the scope of a transaction, make sure you import the System.EnterpriseServices namespace and reference the corresponding assembly. The namespace contains the definitions that enable you to access the programming interface of the COM+ services. You access the SetAbort and SetComplete methods by using the ContextUtil class.

Under the Hood of .NET Web Services

A .NET Web service is an ASP.NET application with an .ASMX extension that is accessed over HTTP. ASP.NET is a portion of the .NET Framework that works on top of IIS, taking care of files with special extensions such as .ASPX and .ASMX. One of the critical components of the ASP.NET infrastructure is the ISAPI filter that IIS involves when it gets a call for files with a certain extension. For example, Figure 9-3 shows the settings in the IIS configuration manager that handle .ASMX and .ASPX files with a system module named aspnet_isapi.dll.

Figure 9-3
The IIS mapping between .ASMX files and the appropriate ASP.NET ISAPI filter.

Calls for Web services always come through port 80. For .NET Web services, these calls are always made directly to URLs with an .ASMX extension. IIS intercepts these calls and passes all related packets to the registered ASP.NET ISAPI filter (aspnet_isapi.dll). The filter connects to a worker process named aspnet_wp.exe, which implements the HTTP pipeline that ASP.NET uses to process Web requests. Both executables are made of plain old Win32 code. The ASP.NET layer built atop IIS is shown in Figure 9-4.

Figure 9-4
The ASP.NET architecture that processes page and Web service requests.

The connection between the IIS process (the executable named inetinfo.exe) and the HTTP pipeline (the worker executable named aspnet_wp.exe) is established by using a named pipe.

note | A named pipe is a Win32 mechanism for transferring data over a network. As its name suggests, a named pipe works just like a pipe. You enter data in one end and the same data comes out the other end. Pipes can be established both locally to connect processes and between remote machines.

After the ASP.NET worker process receives a request, it routes the request through the .NET HTTP pipeline. The entry point of the pipeline is the HttpRuntime class. This class is responsible for packaging the HTTP context for the request, which is nothing more than familiar ASP objects such as Request, Response, and Server. These objects are packed into an instance of the class HttpContext. Then a .NET application is started.

The .NET application processes incoming requests for the files in the specified virtual directory. (See Figure 9-5.) While a given request is being processed, special classes that implement the IHttpModule interface are given a chance to pre-process and post-process the request. The HTTP packets coming through are examined to determine whether they are part of a SOAP request addressed to a Web service or contain the bytes needed for a page request. HTTP handlers, namely .NET classes that implement the IHttpHandler interface, are the endpoints for all HTTP requests that involve ASP.NET and Web services. A specific HTTP handler for ASP.NET pages and Web services is created on demand by made-to-measure factory modules. These modules are classes buried in the folds of the System.Web namespace and are named PageHandlerFactory and WebServiceHandlerFactory, respectively. HTTP handlers are ultimately responsible for parsing and compiling the bytes of .ASPX and .ASMX files, and for generating the response to the client.

Figure 9-5
The .NET HTTP pipeline that processes a Web request.
note | In .NET, the SOAP protocol is not an exclusive privilege of Web services, and .NET Remoting can optionally use SOAP packets to bind local and remote instances of .NET objects. Remoting can be considered as the .NET counterpart of DCOM. Compared to .NET Web services, .NET Remoting provides the same interoperability but is primarily optimized for .NET to .NET communication.
Invoking .NET Web Services

A Web service is always invoked by using an ordinary HTTP packet that contains information about the method to call and the arguments to use. This HTTP packet reaches the Web server by traveling as a GET or POST command. You can invoke a Web service method by using the following three approaches:

  • A POST command that embeds a SOAP request

  • A POST command that specifies the method name and parameters

  • A GET command whose URL contains the method name and parameters

The next listing shows a sample SOAP packet in the body of an HTTP POST command. It is the script for requesting the method GetEmployee to run and uses 2 as the argument.

POST /bwslib/Chap09/NWService/nwservice.asmx HTTP/1.1
Host: expo-one
Content-Type: text/xml; charset=utf-8
Content-Length: 335
Action: "bwslib/0735615780/GetEmployee"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetEmployee xmlns="bwslib/0735615780">
      <nEmpID>2</nEmpID>
    </GetEmployee>
  </soap:Body>
</soap:Envelope>

To invoke a method in a Web service, SOAP is not strictly necessary. You can employ GET or POST commands with a much simpler body. The following listing shows the HTTP packet for a method request made via the simple POST command. The request is to execute the method GetEmployee, passing a value of 2.

POST /bwslib/Chap09/NWService/nwservice.asmx/GetEmployee HTTP/1.1
Host: expo-one
Content-Type: application/x-www-form-urlencoded
Content-Length: 8

nEmpID=2

The next listing shows what this code would look like if you chose to employ a GET command:

GET /bwslib/Chap09/NWService/nwservice.asmx/GetEmployee?nEmpID=2 HTTP/1.1
Host: expo-one
Handling the Response

The Web service requestor receives the response in the form of an HTTP return packet. The following listing illustrates the response you get from a POST or a GET command’s traditional invocation of the GetEmployee method:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: actual length
<?xml version="1.0" encoding="utf-8"?>
<DataSet xmlns="bwslib/0735615780">
  <schema xmlns="http://www.w3.org/2001/XMLSchema">
    schema information
  </schema>
  XML representation of the DataSet </DataSet>

If you invoke the Web service method by using an HTTP POST command but use a SOAP payload to specify the desired action, you can expect this resultant string:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetEmployeeResponse xmlns="bwslib/0735615780">
      <GetEmployeeResult>
        <xsd:schema>
          schema information
        </xsd:schema>
        XML representation of the DataSet
      </GetEmployeeResult>
    </GetEmployeeResponse>
  </soap:Body>
</soap:Envelope>

The fact that Web services are based on HTTP commands and XML formatted text demonstrates that interoperability between Web services running on different platforms is a reality, though certainly not an automatic and seamless feature.

note | As you can easily guess from the packet contents you just reviewed, SOAP payloads are quite verbose when compared with simpler HTTP POST and GET commands. The benefits of using SOAP become clearer as the complexity of data increases. GET and POST commands support primitive types, including arrays and enumerations. SOAP, instead, relies on a portable and more complex type system based on XML schemas. In addition, in .NET, .NET Web services, thanks to XML serialization, also supports classes, structures, and DataSet objects and their arrays.
Invoking Web Services from Script

To give you a practical demonstration of how Web services are really just HTTP-accessible software agents, let’s write a Windows Script Host (WSH) script that allows plain Microsoft Visual Basic script code to download information from a remote server. To send HTTP commands from script code, I’ll use the Microsoft.XmlHttp object—a native component of Microsoft Internet Explorer 5 and Microsoft XML Parser (MSXML) 3 and later versions. The following script calls the method GetEmployee by using a GET command:

Const HOST = "http://..."
Set xmlhttp = CreateObject("Microsoft.XMLHTTP")
xmlhttp.open "GET", _
    HOST & "bwslib/chap09/nwservice/nwservice.asmx/GetEmployee?nEmpID=2", _
    false

' Actually send the request synchronously
xmlhttp.send ""

' Store the results into a RAW_EMPLOYEES.XML file
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.CreateTextFile("raw_employees.xml")
f.Write xmlhttp.responseText
f.Close

The resultant XML string—the body of the response—is stored in a local XML file. The next listing shows how to accomplish the same result by using the POST command:

Const HOST = "http://..."
Set xmlhttp = CreateObject("Microsoft.XMLHTTP")
xmlhttp.open "POST", _
    HOST & "bwslib/chap09/nwservice/nwservice.asmx/GetEmployee", _
    false

' Set the Content-Type header to the specified value
xmlhttp.setRequestHeader "Content-Type", _
    "application/x-www-form-urlencoded"
' Set the body and send the request synchronously
xmlhttp.send "nEmpID=2"
' Store the results into a RAW_EMPLOYEES.XML file
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.CreateTextFile("raw_employees.xml")
f.Write xmlhttp.responseText
f.Close

When using the POST command, you have to use a URL without parameters and store the parameter information in the body of the message. In addition, you also must indicate the content type of the message. The CallGetEmployeeGET.vbs and CallGetEmployeePOST.vbs sample scripts are available on the companion CD.

In addition to using WSH script, you can leverage Microsoft WebService DHTML behaviors (available at http://msdn.microsoft.com/downloads/samples/internet/asp_dot_net_servercontrols/webcontrolsVPDC/Default.asp) to call into a Web service from a client-side HTML page. All these techniques, however, are too low-level and not integrated with the development environment. The .NET Framework allows you to import any Web service in the existing project through proxy classes. A proxy class is just a .NET class that mirrors the programming interface of the Web service so that the application can interact with the remote service, even though it is totally unaware of the target location and platform.

Creating Proxy Classes

The availability of a proxy class makes calling a method on a Web service much easier. The class implementation takes care of all the work necessary to remote the call over the wire to the desired Web service method. The proxy class is automatically generated by using a system provided tool: the Web Services Description Language Tool (wsdl.exe). Microsoft Visual Studio .NET saves you from learning about the tool’s numerous command line switches by providing a context menu item named Add Web Reference, shown in Figure 9-6.

Figure 9-6
Adding a reference to a Web service in Visual Studio .NET.

The command line shown in the next code listing creates a C# source file named NWServiceProxy.cs that contains a class named after the Web service—in this case, NorthwindInfoService. The class is wrapped by a namespace named BWSLib.

wsdl.exe /out:NWServiceProxy.cs /namespace:BWSLib url

The name of the proxy class is the name of the Web service. If the name contains blanks, as in the name Northwind Info Service (the name of our sample Web service), the blanks are removed.

The /out switch allows you to specify a name for the class file. By default, the class is named after the Web service name. The default language is C#, but the /language option allows you to change it.

The /namespace option lets you indicate the namespace that will host the class. Visual Studio .NET defaults to a namespace that comes from the host name. For example, if you reference a Web service on http://expo-one, the default namespace for the proxy class is expo_one. Notice that a dash (-) in the host name is always replaced with the more Internet-friendly underscore character (_). Changing the default namespace is as easy as renaming the corresponding node in the Visual Studio Solution box. When you use the wsdl.exe utility from the command line, no namespace is used unless you explicitly specify it.

caution | Although the way you use proxy classes seems to closely match in-process communication, bear in mind that each method call on a Web service proxy class results in a round-trip to the Web server hosting the service.
Choosing the Transport Protocol

Another parameter you can set by using the command line is the protocol for carrying out an operation. The proxy class defaults to SOAP, which supports the most extensive set of data types when compared with the HTTP GET or POST command. The protocol you choose influences the declaration of the class because the class inherits from a particular protocol class. For example, when you choose SOAP, the proxy class for our Northwind Web service looks like this:

public class NorthwindInfoService :
    System.Web.Services.Protocols.SoapHttpClientProtocol
{

}

HttpPostClientProtocol and HttpGetClientProtocol are the base classes used when you choose the HTTP POST or GET command. As mentioned earlier, the actual URL invoked varies slightly according to the transport protocol. In the case of HTTP GET, the URL un-encodes the method name and the arguments. In the case of HTTP POST, the URL terminates with the method name. Of course, the source code of the proxy class reflects this difference.

Accessing the Underlying URL

When the wsdl.exe utility creates the proxy class, the name of the URL you specify is typically hard-coded. However, you can change this setting at any time and even programmatically. The URL is exposed using a read/write property on the proxy class. The name of the property is Url. The property is inherited from the base class WebClientProtocol—one of the proxy’s ancestors. The proxy class sets the Url property in the constructor:

public NorthwindInfoService()
{
    this.Url = "http://expo-one/bwslib/chap09/nwservice/nwservice.asmx";
}

In situations in which the URL cannot be determined unequivocally, or might change on a per-user basis or because of other run-time factors, you can ask the wsdl.exe utility not to hard-code the URL in the source. By using the /urlkey command line switch, you instruct the utility to dynamically read the Web service URL from the application’s configuration file. If you use a switch such as /urlkey:ActualUrl, the proxy class constructor changes as follows:

using System.Configuration;

public NorthwindInfoService()
{
    String urlSetting = ConfigurationSettings.AppSettings["ActualUrl"];
    if ((urlSetting != null))
        this.Url = urlSetting;
    else // Defaults to the URL used to build the proxy
       this.Url = "http://expo-one/bwslib/chap09/nwservice/nwservice.asmx";
}

ConfigurationSettings.AppSettings is a special property that provides access to the application settings defined in the <appSettings> section of the configuration file. Configuration files are XML files that allow you to change settings without recompiling the application. Configuration files also allow administrators to apply security and restriction policies that affect how applications run on various machines.

The name and location of the configuration file depends on the nature of the application. For ASP.NET pages and Web services, the file is named web.config and is located in the root directory of the application. You can also have other web.config files located in child directories. Child configuration files inherit the settings defined in configuration files located in parent directories. For Microsoft Windows Forms applications, the configuration file takes the name of the executable plus a .CONFIG extension. Such a file must reside in the same folder as the main executable.

Using Proxy Classes

No matter how you generated the proxy class, you use it as you would any other class in the .NET Framework. Visual Studio .NET automatically adds the class to the project. If you generate the class, you add it manually to the project. The source code of the proxy class can be freely edited in the same way that the code of any other class belonging to the project can be edited. Let’s see how to use a Web service class in an ASP.NET page.

To start, you add a reference to the proxy class in the ASP.NET page by using the @ Assembly directive. You can choose to pre-compile the class into an assembly and link by using the Name attribute:

<%@ Assembly Name="NWServiceProxy" %>

You can also link the proxy class via source code, resorting to the Src attribute:

<%@ Assembly Src="NWServiceProxy.cs" %>

Once the assembly is correctly linked to the page, you can use the proxy class in your code as you would do with local, native classes. The following code shows how to call one of the overloads of the NorthwindInfoService class’s GetEmployees method:

DataSet ds = new DataSet();
BWSLib.NorthwindInfoService nwis = new BWSLib.NorthwindInfoService();
ds = nwis.GetEmployees(1, 9);

Notice that the proxy class does not take into account the nicknames you might have set to distinguish, at the level of HTTP messages, the possible overloads of a given method. Previously, I marked each of the three overloads of the GetEmployees method with distinct nicknames by using the WebMethod class’s MessageName attribute. Although these names are the only way to invoke the method via a URL, they are invalid entries in the proxy class programming interface. However, the proxy class code uses those nicknames to arrange proper HTTP calls by using the desired protocol. Figure 9-7 shows the output of the ProxyClass.aspx sample application available on the companion CD, which uses the Northwind Info Service.

Figure 9-7
An ASP.NET page that connects to the Northwind Info Service.

In Figure 9-8, the same Web service serves a Windows Forms application by using a different proxy class.

Figure 9-8
A Windows Forms application serviced by the Northwind Info Service.


Web Service Implementation

So far we have only scratched the surface of real-world Web service programming. We still have to cover a number of significant aspects to complete our overview of the Web service development model. I’m not intending to offer you the ultimate book about Web service planning and programming. In spite of this, I want to discuss a few more arguments that become topical issues as soon as you move away from point-and-click Web services.

In the rest of this chapter, I will not delve into the nitty-gritty details of issues such as contract design, user authentication, and asynchronous calls. However, I’ll review the critical aspects of these issues so that you have a head start in finding out more information about them from sources such as MSDN online documentation. You’ll find these issues much easier to handle when you understand the big picture.

Contract Design

Defining the programming interface of a Web service might look like a deceptively easy task. Through an interface, a server and a client draw up a contract. The programming interface of a Web service is not much different from a contract between people. And as in contracts between people, a contract between a server and a client contains a lot of clauses and terms, intricacies and quirky language. By exposing the programming interface, a Web service asks prospective clients to look at the conditions set in the public WSDL script before deciding whether to adhere to the contract.

The excess of legalese in the contract that a Web service submits to its interested consumers can constitute a serious threat for the success of the service. A programming contract between a Web service and a client should be effective, which does not necessarily mean simple and succinct. Rather, it means that the programming interface of the Web service must be appropriate to the level and the complexity of the data being exchanged and the operations being requested and executed.

Minimizing Round-Trips

Each call addressing a Web service method requires a round-trip. Since all Web service activity takes place over the Internet, you cannot always expect a rapid response. Be prepared to measure the time needed for Web services calls in seconds rather than in milliseconds, meaning that your primary goal in designing a Web service is to minimize round-trips.

Since the round-trip is permanently tied to the request of an operation on the Web service, the best—and possibly the only—way to minimize round-trips is to merge more logically distinct functions. The open issue in this approach concerns using additional methods to the interface or additional parameters to the prototype of certain methods. Simple, succinct, and direct methods enhance overall design but certainly do not minimize round-trips, because to execute two functions, you need at least two round-trips.

On the other hand, incorporating more functionality in the body of a single and more complex method is effective in terms of performance but not necessarily in terms of the service usability. A client might receive more information than needed, paying the price of increased downloading time. Moreover, a client might be forced to use an overly complex signature, exposing itself to the risk of getting the requested information by trial and error. In such a case, you actually create more round-trips for the client.

Just as in life, in Web service programming, the truth lies somewhere in between. You should avoid method signatures that are too simple and too complex. This advice seems to contradict one of the golden rules for effective MTS/COM+ programming: write simple, stateless, and possibly parameterless methods. For Web services, use sophisticated (but not too complicated) methods that can incorporate functions with related parameters. Verify the complexity of the server-side code that validates the parameters and then execute the method, and make sure users don’t receive too much information that is not pertinent to their original requests. (Certainly ensure that this unrelated information is not the most relevant part of a response.) Finally, don’t forget that the more complex the programming interface is, the more you need effective and well-designed documentation.

Authentication and Authorization

At the highest level of abstraction, a Web service is a publicly accessible module that dispenses services to software consumers. However, in reality, not all software agents that can access the service have the rights to access it, nor can they invoke all the methods. A user must be authenticated to validate its identity to the system. After a user’s credentials are authenticated, the authorization process determines whether that user has access to a resource based on role information and the user’s profile.

The Web service infrastructure does not mandate that you implement authentication and authorization by using a made-to-measure API or approach. Instead the Web service leaves authentication and authorization up to the application, which should exploit the security features built into the application’s platform and protocol. The .NET-specific infrastructure for Web services does not provide ad-hoc facilities either. However, a Web service class that inherits from the WebService class can easily interact with the authentication mechanism of ASP.NET. ASP.NET, in turn, relies heavily on the IIS authentication mechanisms for HTTP, including basic, digest, and Windows-integrated authentication.

In ASP.NET, you retrieve a user identity by using a class that is specific to the authentication mode you choose. ASP.NET supports three authentication modes, which you set in the web.config file: Windows integrated (the default), form-based, and Microsoft Passport. These authentication modes rely on the WindowsIdentity, FormsIdentity, and PassportIdentity classes to represent authenticated users.

note | The Passport authentication mode (http://www.passport.com) is a third-party centralized service that provides a way to authenticate users by using a single logon. ASP.NET has built-in support for Passport, which saves you from having to interact with the Passport SDK to activate the authentication service. Passport requires you to download, install, and configure the Passport SDK prior to developing the Web service. ASP.NET does not provide any support to help client applications pass Passport credentials to the Web service. Each client application must implement its own code for that.

Using IIS-based authentication mechanisms with a Web service is fairly easy and codeless. All you have to handle is some configuration work to enable IIS to perform authentication before the Web service is called. You need to create accounts for each client that is allowed to access the Web service, and for each of them predispose the proper ACL on the .ASMX Web service file.

note | .NET applications in general, and Web services in particular, can authorize the execution of methods based on the identity or the role played by the identity in the application. Role membership evaluates to a set of privileges common to all participating identities. In .NET, role support is handled using objects that implement the IPrincipal interface, which represents the security context under which code is running. Applications check the client’s role through the IsInRole method on the IPrincipal interface.
Web Service–Specific Security

Authentication and authorization in .NET is not something optimized only for Web services. Both are designed to accommodate all applications running on the .NET platform. In most cases, a Web service can implement a customized security model that is also relatively simple to code and effective. The advantage of a handcrafted security model is that it does not require any sort of interaction with external components, including system components.

You might implement your own security layer if you already have, or find more manageable to use, a table of user account information. When you use your own security layer, you can use your own database instead of creating Windows accounts for each user. You might also use your own security layer when the results of the authentication and authorization depend on run-time conditions and data. Let’s review a consolidated approach for building a custom security layer.

Programmatic Login

Your Web service might require all its clients to log on prior to calling any methods. The signature of the method for logging on might look like the following:

long Login(String uid, String pswd, String role)

The method returns a value that works as a token to call other methods. The information stored in the token, as well as its data type, is completely up to you. The token should have some basic characteristics such as being encrypted, or at least not easily read by another person. In addition, you should be able to extract from it all the information needed to verify the authenticity of the token and the user prior to calling the requested method. The following pseudocode shows how it works:

long token = MyService.Login(uid, pswd, role);
MyService.GetEmployee(token, nEmpID);

The Login method must authenticate the user against a database or any persistent support you might want to consider. It then creates and returns a token that summarizes information about the user and the time the token was generated. Each method that requires the token first checks its validity before resuming execution. Of course, token-free methods are not subject to restriction rules.

Managing State

For Web services, relying on HTTP is advantageous because requests and responses pass through most firewalls, which normally accept packets coming through port 80. Other network protocols such as DCOM encounter serious interoperability problems because particularly restrictive firewalls won’t allow all the requested traffic. However, relying only on HTTP makes maintaining the state between successive calls difficult for Web services. HTTP, in fact, is a stateless protocol.

Web services in .NET handle state management in much the same way other ASP.NET applications do. Both Web services and ASP.NET applications have access to the same built-in options, that is, the Session, Application, and Cache objects; and both can employ other programming techniques such as storing data on disk files or in database tables. To access ASP.NET state objects, the Web service must be derived from the WebService class.

Web services built atop the WebService class automatically have access to the Application and Cache objects. All data stored in the Session object is available only when you set the EnableSession property of the WebMethod attribute to true.

Publishing and Deploying Web Services

Defining a standard way to publish and locate information about Web services is the ultimate goal of the UDDI specification (http://www.uddi.org). The UDDI Business Registry is a central repository—a sort of Web services yellow pages—where organizations can store discovery documents and service descriptions about their Web services. The same registry can be accessed programmatically by potential clients that want to discover whether a Web service with certain characteristics already exists.

A Web service can be discovered when it is associated with a discovery file (.DISCO). The discovery file is an XML document that contains links to other resources that describe the Web service. In addition to being located in the UDDI Registry, discovery files can be located in the root of the Web server that hosts the Web service. The following is the content of the .DISCO file for the Northwind Info Service:

<?xml version="1.0" encoding="utf-8"?>
<discovery xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.xmlsoap.org/disco/">
  <contractRef xmlns="http://schemas.xmlsoap.org/disco/scl/"
    ref="http://expo-one/bwslib/chap09/nwservice/nwservice.asmx?wsdl"
    docRef="http://expo-one/bwslib/chap09/nwservice/nwservice.asmx" />
  <soap address="http://expo-one/bwslib/chap09/nwservice/nwservice.asmx"
    xmlns:q1="bwslib/0735615780" binding="q1:Northwind Info ServiceSoap"
    xmlns="http://schemas.xmlsoap.org/disco/soap/" />
</discovery>

The <contractRef> node contains the link to the WSDL description of the functions available in the Web service. Learning what the Web service capabilities are and how to properly interact with the service is the first step for accessing a Web service.

The deployment of a Web service is rather simple and, compared to the deployment of Web applications before .NET was available, straightforward. You copy on the Web server the .ASMX file and any assembly it needs, with the obvious exceptions being the assemblies that are already part of the .NET Framework. Nonstandard assemblies have to be copied in the bin folder under the virtual directory for installation. The .DISCO file goes to the root directory of the application.


Web Service Optimization

As a software application that makes itself available only via Internet connections, a Web service is at risk of being, or becoming, a slow application. For this reason, optimization is more than ever a critical factor. The overall performance is affected by the network latency as well as the format of the protocol being used. The pair of HTTP and SOAP is based on text, and SOAP in particular is quite a verbose protocol. This results in packets significantly larger than those typical of binary protocols such as CORBA or even DCOM. However, relying on HTTP and SOAP is at the foundation of the Web service concept, because they are critical for interoperability and loosely coupled connections between clients and servers.

To improve the usability of a Web service (the characteristic you should focus optimization efforts on), you should look elsewhere and involve the client applications as well. In particular, asynchronous calls and custom SOAP extensions can help considerably.

Asynchronous Calls

A call to a Web service method can occur both synchronously and asynchronously, depending on how the client sets it up. When you use proxy classes, the method invocation typically takes place in a synchronous way. Fortunately, you do not have to change the Web service’s structure to make it support asynchronous calls.

Asynchronous calls make sense in Windows Forms applications where the user has a lot of controls to work with while waiting for the response to arrive. Web pages, which are created on the server without user intervention, have nothing to gain from asynchronous calls. The only exception to this is when you have HTML pages that use Dynamic HTML behaviors to call into Web services.

The proxy class for the Web service already contains all that you need to set up an asynchronous call. The Northwind Info Service we built in this chapter has three Web methods that correspond to the overloads of the same GetEmployees method. In addition to the GetEmployees method, the proxy class also features method triplets such as GetEmployees, BeginGetEmployees, and EndGetEmployees. In particular, you find the following:

  • GetEmployees, BeginGetEmployees, EndGetEmployees

  • GetEmployees, BeginGetEmployees1, EndGetEmployees1

  • GetEmployees, BeginGetEmployees2, EndGetEmployees2

The first method of the triplet is one of the overloads for the Web service GetEmployees method. That method calls its Web service counterpart in a synchronous way. The other two methods, instead, call into the same Web service method but in an asynchronous fashion. The following code snippet shows an excerpt from the proxy class code that illustrates how the synchronous call works:

public System.Data.DataSet GetEmployees() {
    object[] results = this.Invoke("GetEmployees", new object[0]);
    return ((System.Data.DataSet)(results[0]));
}

The overload considered in this code is GetEmployees(), so it takes no arguments and returns all the employees in the database.

Issuing Asynchronous Calls

An asynchronous call is made of two distinct function calls. The first function call opens the connection and begins the data download. The second function call informs the Web service requestor that the download is complete and gives the requestor a chance to grab the resultant object. The asynchronous methods for GetEmployees() have the following components:

IAsyncResult BeginGetEmployees(AsyncCallback callback, object asyncState)
{
    return this.BeginInvoke("GetEmployees",
        new object[0], callback, asyncState);
}

DataSet EndGetEmployees(System.IAsyncResult asyncResult)
{
    object[] results = this.EndInvoke(asyncResult);
    return ((System.Data.DataSet)(results[0]));
}

BeginGetEmployees takes all its regular arguments plus two more: an asynchronous callback function and a reference to a proxy class object. It returns an object of type IAsyncResult, which provides a way to monitor what’s going on over the network. By using an object of IAsyncResult, a software client can check at any time whether the download is complete. The end of the execution can be notified automatically via a callback function, that is, an object of type AsyncCallback.

Let’s suppose that you want to make the following call asynchronous:

NorthwindInfoService nwis = new NorthwindInfoService();
DataSet ds = nwis.GetEmployees(1, 9);

You first get the distinct name for the overloads, which has been set in the proxy class. You do this snooping in the source code of the proxy. If no overloaded method is involved, you can determine the names of asynchronous methods pretty easily: they are BeginMethodName and EndMethodName. As for the Northwind Info Service, the asynchronous pair for the GetEmployees(first, last) method is BeginGetEmployees1 and EndGetEmployees1. You begin the download with the following code:

AsyncCallback ac = new AsyncCallback(NWServiceCallback);
NorthwindInfoService nwis = new NorthwindInfoService();
nwis.BeginGetEmployees1(1, 9, ac, nwis);

NWServiceCallback is the name of a method defined in a class within the client application. This method is automatically called back by the proxy class when the download is complete. The typical implementation of the method is shown here:

void NWServiceCallback(IAsyncResult iar)
{
    NorthwindInfoService nwis = (NorthwindInfoService) iar.AsyncState;
    m_DataSet = nwis.EndGetEmployees1(iar);
}

The AsyncState property of the IAsyncResult argument evaluates to the instance of the proxy class in use. Once you hold a living instance of the proxy class, you call the method that terminates the connection with the Web service. This method receives the underlying IAsyncResult object and finally returns the expected data. Figure 9-9 shows two screens for a sample Windows Forms application that downloads a DataSet object from Northwind Info Service.

Figure 9-9
The sample application downloads data asynchronously from a Web service.
SOAP Extensions

A SOAP extension is like a hook that you register with the Web service to access the raw SOAP XML either as it is about to be transmitted or as it’s received. A SOAP extension works both on the client by using proxy classes and on the server. When you want to play tricks or customize the underlying XML, SOAP extensions provide the right connection point. A valid example of their use is to encrypt or compress method parameters for improved performance and security.

The ASP.NET SOAP extension architecture closely resembles the hook architecture in Win32. Basically, a registered SOAP extension can inspect or modify a SOAP message at specific stages in message processing on either the client or the server. A SOAP extension is made of two elements: the extension class and the attribute class.

You write an ASP.NET SOAP extension by creating two classes. The extension class inherits from the SoapExtension class, whereas the attribute class inherits from SoapExtensionAttribute. After you have created, say, a MySoapExtension class, you associate it with a Web service method as follows:

[WebMethod]
[MySoapExtension()]public object DoSomething()
{

}

All SOAP packets originated by calls to that Web service method are then filtered by the extension. The critical method of a SOAP extension is ProcessMessage:

public abstract void ProcessMessage(SoapMessage message);

The argument passed to this method is SoapClientMessage when the extension is invoked on the client, and SoapServerMessage when the extension works on the server. ProcessMessage implements a switch statement and does different tasks according to the stage of the message. There are four possible stages: BeforeSerialize, AfterSerialize, BeforeDeserialize, and AfterDeserialize.

A SOAP extension attribute is a worker class that provides custom properties specific to the SOAP extension. Derived attribute classes must necessarily override the ExtensionType property, which returns the type of extension that is associated with the attribute. Here is an example:

public class MySoapExtensionAttribute : SoapExtensionAttribute
{
    // Define custom attributes here...

    public override Type ExtensionType
    {
        get { return typeof(MySoapExtension); }
    }
}

The point of contact between the SOAP attribute class and the extension class is the GetInitializer method on the SOAP extension class:

public abstract object GetInitializer(
    LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute);

Any SOAP extension–specific data gets passed using the custom attribute class and can be cached in the SOAP extension class at this point. GetInitializer is called only once in the extension’s lifetime. The use of attribute classes decouples the logic of the SOAP extension from any custom attribute it might need.

Extreme Optimization

Round-trips are one of the most critical aspects of Web services. When your Web service is destined to play in a corporate environment with all the bandwidth it can muster, a few extra round-trips won’t really degrade the performance too much—or at least this is our hope. Web services operate on the Web, however, so you do have to minimize round-trips.

I would suggest you consider making the Web service capable of executing an array of methods in batch mode—an extreme form of optimization. If you could figure out a way for clients to pack multiple calls together, and if you could make your Web service unpack and process all of them, you would be close to minimizing the impact of round-trips.

The overall idea of batch execution might sound attractive, but you’ll need to sort out a number of subtle and not so subtle issues. For example, what happens when you need to use the results of one call as the input of the next call? What if you need to control the flow through conditional expressions or loops? I don’t have the all-encompassing solution in my hands, but I do want to point you to a feature that very few software applications can boast about having. I’m talking about the ability to use C# and any other .NET language as a script.

The .NET Framework provides classes in the System.CodeDom.Compiler namespace that let you compile C# code on the fly and in memory, which means you never write the resultant assembly to disk. The Web service can expose a method that accepts a C# string and then just compile, run, and return the results to the client.

caution | Using this form of mobile code might pose serious security concerns. How can you make sure that the code accepted by the Web service is safe for the Web server? You can work around this issue in several ways. For example, you can enable the compilation feature only for authorized users. Or, better yet, you can allow the resulting dynamic assembly to run in a sort of sandbox, where potentially dangerous calls are just forbidden.

What would this kind of scripted Web service actually look like? I think the C# code you send in would closely resemble a kind of Web stored procedure, with the Web service working like a database. In terms of performance, this approach makes sense because you maximize the information transfer both for data coming and data going. In terms of its feasibility, consider what will happen with the next version of Microsoft SQL Server. Although the product is an unmanaged application, it will certainly host the .NET CLR. This simple fact would enable it to execute C# and Visual Basic .NET code within the environment. So chances are good that in the near future, you will be able to write compiled stored procedures using C#. SQL Server will add its own object model (say, a variation of today’s SQL Distributed Management Objects (SQL-DMO)) to let you code against tables and columns. You would be able to do the same with Web services as well with the public methods of the Web service to create the programmable object model.


Conclusion

Web services are one of the key elements of the so-called programmable Web. They are extremely versatile software elements that really have the potential to open up a new era in sofware: the age of interoperability. Web services can be effectively used to participate in and set up business-to-business (B2B) transactions. They are great at exposing software functionality to customers and integrating heterogeneous platforms.

Web services are exclusively based on open and commonly accepted Internet protocols. This is their major strength but also a significant weakness. The strength, which enables true interoperability, comes at the price of decreased speed, which is seriously affected by the bandwidth. Web services are not good at everything but certainly represent a category of software agents that we are all looking for.


Exposing Data to .NET Applications

Chapter 10
Exposing Data to .NET Applications

The ADO.NET framework is made of two distinct groups of constituent classes: the generic container classes and the data source-aware classes. The data source-aware classes are still part of the System.Data namespace but are tightly integrated with a specific data source. Although data source-aware classes provide general-purpose functionality, their implementation is based on the features of a particular data source. So ADO.NET command classes, for example, might have a different set of methods depending on whether they work against an OLE DB provider or Microsoft SQL Server.

The container group of ADO.NET classes are abstracted from the physical data source. Although they represent data tables, rows, columns, and primary keys, nothing in their programming interface keeps track of the content source. Thus, a DataTable object or a DataSet object can be filled with data retrieved from a commercial database server as well as filled with packets of data read from a connected device.

The point of contact between data source-aware classes and container classes is represented by a new breed of component: the .NET data provider. Such managed data providers in .NET play the same role as OLE DB providers in the COM world. In the past few years, companies invested a lot of money in the OLE DB technology. OLE DB turned the vision of a universal data access strategy, theorized in the Universal Data Access (UDA) specification, into concrete programming calls. OLE DB is built on the idea of using a suite of COM interfaces to read and write the contents of data sources irrespective of their relational, hierarchical, or flat architecture.

Maybe the C++ oriented design that OLE DB came up with is a bit too complex for widespread use, but it definitely provides great flexibility when exposing proprietary data in a standardized way. OLE DB supplies a common COM-based API that enables consumer applications to talk to data provider modules without knowing about internal details. Each provider encapsulates a particular data store such as a commercial database, a system component such as Active Directory, or, more simply, the manager of proprietary data with a custom format. According to the UDA specification, OLE DB is the tool of choice to universalize data access.

The advent of .NET pushed all COM-based technologies to the side, and OLE DB was no exception. All .NET applications that want to access data by using OLE DB providers must bust out of the common language runtime environment and rely on COM interoperability and enterprise services. Such a move creates overhead that of course never benefits overall application performance. To ameliorate these issues, which are at the core of data access, .NET introduced managed data providers. Managed data providers are patterned after OLE DB providers but also access data within the context of the common language runtime in a simpler and more effective way.

In the pre-.NET era, OLE DB providers were your primary option for making your proprietary data publicly available—that is, available in a widely accepted format. For relatively simple data formats (such as comma-separated files), you also had the option of using the OLE DB Simple Provider Toolkit. The Simple Provider Toolkit provides tools for writing (even in Microsoft Visual Basic) shrink-wrapped OLE DB providers with limited functionality. In .NET, exposing proprietary data requires a more sophisticated approach, and you have multiple and equally powerful options from which to choose.


Exposing Proprietary Data

To expose custom data in a format that can be easily consumed by a .NET application, you can take any of the following routes:

  • Develop a made-to-measure XML schema for the data and build a .NET class around it.

  • Write a COM–based OLE DB provider.

  • Write a simple .NET data provider.

  • Architect and develop a full-fledged .NET data provider.

Where should you concentrate your efforts? Your answer depends on the nature of the data you are providing and the audience you expect to reach. Each approach has pros and cons, and choosing the one that best fits your data is—guess what—up to you!

Using Tailor-Made Classes

XML lets you reach the same level of universality as OLE DB. In addition, XML is free from the problems that prevented OLE DB from becoming a recognized and widely accepted technology for data access and data sharing. XML is terrifically simple, capable of being read and authored by anyone and, last but not least, ubiquitous (that is, available on all platforms).

In .NET, you have at your disposal many classes and facilities for manipulating XML documents, from XmlReader to XmlDataDocument, and from XmlWriter to XPathNavigator. When you publish a basic XML schema, prospective clients can easily understand it, but bear in mind that using more advanced XML-related technologies could limit your potential audience. For example, the parser used to process your documents on a certain platform might not be updated to support XML schemas, namespaces, XPath, and so on.

In addition to using XML, your .NET applications have other ways to make data accessible and consumable. For example, you can use the classes in the System.Data namespace as a kind of super-array data structure. These classes include the DataTable class and the DataSet class. Serializing a DataSet object to XML output is particularly suitable in situations where interoperability is critical. It does not matter whether you persist data as XML to a file or remote it as a string in a SOAP packet. What does matter is that you serialize your data to XML, as discussed in Chapter 8. When the target of your data is another .NET application, what could be better than using a native .NET data structure such as DataTable?

To verify the practicality of this approach, let’s see it in action and write a class that grabs some directory information and then exposes it as XML. We’ll make available a DirectoryListing class that returns information about directories in a variety of formats including an XML string, an XML file, and a DataTable class.

The DirectoryListing Class

The DirectoryListing class works like an in-memory version of the old faithful MS-DOS dir command. It collects most of the typical directory information including name, size, type, attributes, date and time of the last access, and user comments. The class considers all the files and subdirectories of a given path. The output is obtained in tabular format by using a DataTable object as well as by using an XML string or an XML file.

Like many other classes in the .NET Framework, the DirectoryListing class has two constructors, one of which is the default.

// Default class constructor
public DirectoryListing()
{

}

// Ad-hoc class constructor
public DirectoryListing(String path)
{
    // Stores the path to work on
    m_path = path;

}

Aside from the constructors, the class has one property named Path and one method named Fill. The Fill method has several overloads so that it can deal with the various outputs. The Path property represents the file system path to the directory for which you are running the query. The class stores the path name into a private data member, which is read and written by using the property’s get and set accessors. The property defaults to the root of the C: drive.

private String m_path = "c:\\";
public String Path
{
    get {return m_path;}
    set {m_path = value;}
}

The Fill method triggers the process that retrieves directory and file information and serializes it into the specified output format. The method has four overloads, as shown here:

public String Fill()
public String Fill(String path)
public int Fill(String path, String filename)
public int Fill(String path, DataTable dt)

The first two overloads return an XML string that represents the directory information. If no path is explicitly indicated, the method works for the path currently stored in the Path property. In terms of practical implementation, the first overload works as a special case of the second:

public String Fill()
{
    return Fill(m_path);
}

After the class ascertains that the specified path exists, it creates and populates two distinct DataTable objects, one for directory information and one for file information. Next, the two tables are added to a temporary DataSet object, and the corresponding XML representation is returned. This final step is necessary to automatically save the DataTable objects into a single XML stream. You can decide whether to use a temporary DataSet object; you can write down your own XML schema and your own serializer without resorting to the ADO.NET schema for DataSet objects.

public String Fill(String path)
{
    // Make sure the path is correct
    if (!_VerifyPath(path))
        return "";

    // Create and fill the table FOR DIRECTORIES
    DataTable dt1 = _PrepareDataTable("Folder");
    _FillDataTableDirectoryInfo(dt1, new DirectoryInfo(path));

    // Create and fill the table FOR FILES
    DataTable dt2 = _PrepareDataTable("File");
    _FillDataTableFileInfo(dt2, new DirectoryInfo(path));

    // Serialize to XML via a temporary DataSet object
    DataSet ds = new DataSet("DirectoryListing");
    ds.Tables.Add(dt1);
    ds.Tables.Add(dt2);
    return ds.GetXml();
}

In the implementation of the DirectoryListing class, located in the DirectoryListing.cs sample file on the companion CD, the third overload of the Fill method writes the XML representation of the contents to a disk file:

public int Fill(String path, String filename)
{
    // Make sure the path is correct
    if (!_VerifyPath(path))
        return -1;

    // Create and fill the table FOR DIRECTORIES
    DataTable dt1 = _PrepareDataTable("Folder");
    _FillDataTableDirectoryInfo(dt1, new DirectoryInfo(path));

    // Create and fill the table FOR FILES
    DataTable dt2 = _PrepareDataTable("File");
    _FillDataTableFileInfo(dt2, new DirectoryInfo(path));

    // Add the table to a temporary DataSet object
    DataSet ds = new DataSet("DirectoryListing");
    ds.Tables.Add(dt1);
    ds.Tables.Add(dt2);

    // Serialize the DataSet to an XML stream
    StreamWriter sw = new StreamWriter(filename);
    ds.WriteXml(sw);
    sw.Close();

    // Return the table row count
    return dt1.Rows.Count + dt2.Rows.Count;
}

The Fill method returns the total number of rows added to the temporary DataSet object, including those representing a subdirectory and those representing a file. The layout of the final XML code looks like the following sample script:

<DirectoryListing>
  <Folder>
    <Name>My Apps</Name>
    <Size>0</Size>
    <Created>2001-09-05T11:28:18.8048560+02:00</Created>
    <Attributes>ReadOnly, Directory</Attributes>
    <Type>&lt;DIR&gt;</Type>
    <Comment>My favorite handcrafted applications</Comment>
  </Folder>
  <Folder>
    <Name>My Clients</Name>
    <Size>0</Size>
    <Created>2001-09-05T11:28:35.9795520+02:00</Created>
    <Attributes>ReadOnly, Directory</Attributes>
    <Type>&lt;DIR&gt;</Type>
    <Comment>Projects I'm taking over for some of my clients</Comment>
  </Folder>
  <Folder>
    <Name>My Lab</Name>
    <Size>0</Size>
    <Created>2001-09-05T11:28:44.8222672+02:00</Created>
    <Attributes>ReadOnly, Directory</Attributes>
    <Type>&lt;DIR&gt;</Type>
    <Comment>Results of genetic manipulation of source code</Comment>
  </Folder>
  <File>
    <Name>ToDo.txt</Name>
    <Size>449</Size>
    <Created>2001-11-07T17:07:09.6498992+01:00</Created>
    <Attributes>Archive</Attributes>
    <Type>Text Document</Type>
    <Comment />
  </File>
</DirectoryListing>

As you might have noticed, the XML schema uses a distinct tag name for information representing a directory and for information representing a file. The <Folder> tag is used for directories, and the <File> tag wraps file information. Using two tag names is based on the idea of using two separate tables, one named Folder and one named File. To keep the XML code as simple as possible, I deliberately did not return schema information. When you want to include schema information, modify the Fill method to make the DataSet object send out XML with the schema embedded:

StreamWriter sw = new StreamWriter(filename);
ds.WriteXml(sw, XmlWriteMode.WriteSchema);
sw.Close();

When the Fill method of the DirectoryListing class is used to get a unique DataTable object, the distinction between folders and files ceases to make sense. The following code shows how to implement the Fill method to retrieve a unique DataTable object:

public int Fill(String path, DataTable dt)
{
    if (!_VerifyPath(path))
        return -1;

    _PrepareDataTable("FolderItem", dt);

    DirectoryInfo dir = new DirectoryInfo(path);
    _FillDataTableDirectoryInfo(dt, dir);
    _FillDataTableFileInfo(dt, dir);

    return dt.Rows.Count;
}

File and folder information are written to a new or existing DataTable object whose name is set as FolderItem. In our example, the method returns the number of rows found in the table at the end of the operation.

Now you have an example of the programming interface for a data container class. Let’s see how to create and manipulate data in memory using ADO.NET container objects.

Creating In-Memory Tables

A DataTable object is a sort of heterogeneous, multidimensional array with extra methods that facilitate data access and retrieval. Although you will likely use it to store data originating from a database, the DataTable object (and the DataSet object) is a general-purpose data container that simply holds data, no matter where that data originates. Like a database, the DataTable object knows how to index, filter, select, and sort its child rows. These functionalities are built into the code of the DataTable class and in no way rely on a back-end data source. (It is no accident that objects such as DataSet, DataTable, DataColumn, DataRow, and DataRelation belong to a super namespace such as System.Data and are not part of a provider-specific namespace such as System.Data.SqlClient and System.Data.OleDb.) The DataTable object is just a .NET class good at storing in memory any data that can be described as a table by using a collection of rows and columns, so don’t be too scared to use it to expose proprietary data or volatile information.

You create a new DataTable object by using one of its two constructors:

DataTable dt = new DataTable();
DataTable dt = new DataTable("NameOfTheTable");

At this point you have only an empty object with no columns or rows. Prior to adding rows, you must define the columns. The next listing demonstrates how to add the columns needed to properly represent directory and file information:

private void _AddColumnsToTable(DataTable dt)
{
    DataColumn dc1 = new DataColumn();
    dc1.DataType = typeof(String);
    dc1.ColumnName = "Name";
    dt.Columns.Add(dc1);

    dc1 = new DataColumn();
    dc1.DataType = typeof(int);
    dc1.ColumnName = "Size";
    dt.Columns.Add(dc1);

    dc1 = new DataColumn();
    dc1.DataType = typeof(DateTime);
    dc1.ColumnName = "Created";
    dt.Columns.Add(dc1);

    dc1 = new DataColumn();
    dc1.DataType = typeof(String);
    dc1.ColumnName = "Attributes";
    dt.Columns.Add(dc1);

    dc1 = new DataColumn();
    dc1.DataType = typeof(String);
    dc1.ColumnName = "Type";
    dt.Columns.Add(dc1);

    dc1 = new DataColumn();
    dc1.DataType = typeof(String);
    dc1.ColumnName = "Comment";
    dt.Columns.Add(dc1);
}

To add a column, you need to create a new instance of a DataColumn object and give it at least a data type and a name. You must also add the column object to the Columns collection of the DataTable object. During this process, all data pertaining to the column gets stored internally in the DataTable object. After the object is added, you can blissfully null it out or reassign to it a new instance of the DataColumn class.

A defined set of columns enables a table to accept and store rows of data. Rows are stored using the Rows collection. A DataRow object does not have a public constructor that clients can directly call. You must create rows by using the NewRow method of the DataTable object. The NewRow method takes care of mirroring the columns’ metadata into the newly created row object.

Directory Information

The following listing shows how to populate a given row with directory-specific information. The work is not adding and filling out the row but getting file system information for the specified directory name.

private void _FillDataTableDirectoryInfo(DataTable dt, DirectoryInfo dir)
{
    // Loops through subdirectories
    foreach (DirectoryInfo d in dir.GetDirectories())
    {
        DataRow dr = dt.NewRow();
        dr["Name"] = d.Name;
        dr["Size"] = 0;
        dr["Comment"] = _ReadDirComment(d.FullName);
        dr["Created"] = d.CreationTime;
        dr["Type"] = "<DIR>";
        dr["Attributes"] = d.Attributes.ToString();
        dt.Rows.Add(dr);
    }
}

The .NET Framework provides two powerful classes for identifying files and directories: DirectoryInfo and FileInfo. You find them in the System.IO namespace. The DirectoryInfo structure for the directory being searched is passed on to the helper method shown in the preceding code. This helper method loops internally through the array of subdirectories returned by the GetDirectories method. Each element in this array is another DirectoryInfo object. Accessing the standard properties of a directory is straightforward. The directory attributes are rendered by using an object of type FileAttributes, which is exposed via the Attributes property. Note that when you call a member of the FileAttributes class, the ToString method nicely translates bitwise enumeration values into readable and comma-separated strings such as "ReadOnly, System, Archive".

In the schema we designed for the table, some columns represent information that, although pertinent to the directory, is not natively exposed by the DirectoryInfo class. This information includes the size of the directory (intended as the recursive sum of the size of child files and subdirectories), the user comment, and the item type. Size and item type are specific to each file, so let’s set them to static text for directories. The user comment is directory-specific. It is not part of the DirectoryInfo interface because it originates from a local file named desktop.ini, which is not supported on all platforms where the .NET Framework is available.

The Desktop.ini File

The desktop.ini file is a configuration file that is resident in the folder to which its contents are applied. The desktop.ini file is not supported on all versions of Microsoft Windows 95 and Microsoft Windows NT 4. Nothing prevents you from creating a desktop.ini file in any folder on a Windows NT 4 machine, but the operating system won’t be giving the file the special meaning it has on machines running Microsoft Windows 98, Microsoft Windows 2000, or Microsoft Windows XP.

A desktop.ini file is a short text file that can contain a comment about the contents of the folder. It can also indicate a nonstandard icon for Microsoft Windows Explorer to use when rendering the contents of the folder. The Web-like shell available on Windows 98 and later systems attributes a special meaning to this file when the containing folder is marked read-only. When the folder is read-only, Explorer draws the folder with a custom icon. In addition, it uses the comment to display a tooltip when the mouse hovers over the folder in the shell view. (See Figure 10-1.)

Figure 10-1
The tooltip resulting from a desktop.ini file.

The typical contents of a desktop.ini file are shown here:

[.ShellClassInfo]
Infotip=Results of genetic manipulation of source codeIconFileChemistry.ico
IconIndex=0

This file feature is broadly used in the Windows XP shell, which also provides a system dialog box for creating user comments without resorting to modifying desktop.ini files. Incidentally, in Windows XP, desktop.ini files are marked as system files. No method in the .NET Framework or in the Microsoft Win32 and Windows XP SDKs provides a function for extracting the description of a folder. Nevertheless, both in Windows 2000 and in Windows XP, you can display the Comment column in the shell view and know at a glance about the contents of a given folder. (See Figure 10-2.)

Figure 10-2
The content of the desktop.ini file used in the Windows shell.

Desktop.ini is a plain old .INI file, and the entry we’re mainly interested in is Infotip. Getting at that information is as easy as reading some bytes from a text file: you use the private method _ReadDirComment. (The complete source code for DirectoryListing class is in DirectoryListing.cs, available on the companion CD.)

File Information

The .NET class that returns system information about a particular file is FileInfo. All files contained in a folder can be accessed by using the GetFiles method on the DirectoryInfo object, which represents the parent directory. The following code shows how to populate a table row associated with a file in the folder:

private void _FillDataTableFileInfo(DataTable dt, DirectoryInfo dir)
{
    // Loops through files
    foreach (FileInfo f in dir.GetFiles())
    {
        DataRow dr = dt.NewRow();
        dr["Name"] = f.Name;
        dr["Size"] = f.Length;
        dr["Comment"] = "";
        dr["Created"] = f.CreationTime;
        dr["Type"] = _ReadItemType(f.Extension);
        dr["Attributes"] = f.Attributes.ToString();
        dt.Rows.Add(dr);
    }
}

User comments are not supported for files through the desktop.ini interface. However, as long as you use an NTFS file system, you can associate extra streams with each file in the system and store comments and other normally invisible information there.

Both files and directories can have an extension, but the Windows shell uses file extensions to catalog files into classes. For example, all files with a .BMP extension belong to the same logical group and share the same shell settings such as the icon, context menu, tooltip, and Properties dialog box. Each class of files has a name and a description. Windows Explorer displays the description in the Type column. Can you get this setting information programmatically? You can when you access the system registry.

Getting File Type Information

To get hold of the string displayed in the Type column, you first need to read the default value of the following key:

HKEY_CLASSES_ROOT\.bmp

Next you use that string to gain further access to the registry. Typically, the string you read is Paint.Picture. (This name is machine-specific.) The next access attempt reads the default value from the following:

HKEY_CLASSES_ROOT\Paint.Picture

The next code listing shows how to read the default value by using the .NET Registry class. You must import the namespace Microsoft.Win32.

private String _ReadItemType(String sFileExtension)
{
    String strType = "File";
    if (sFileExtension == "")
        return strType;

    // Open the .EXT key (i.e., .BMP)
    RegistryKey reg = Registry.ClassesRoot.OpenSubKey(sFileExtension);

    try
    {
        // Read the indexer key (i.e., Paint.Picture)
        String indexerKey = reg.GetValue(null).ToString();
        reg.Close();

        // Access the indexer key and read the value
        reg = Registry.ClassesRoot.OpenSubKey(indexerKey);
        strType = reg.GetValue(null).ToString();
        reg.Close();
    }
    catch {}

    return strType;
}

At this point in the code, all pieces of the DirectoryListing jigsaw puzzle are in the right place. Now let’s see how DirectoryListing actually works.

Using the DirectoryListing Class

You use the DirectoryListing class whenever you need to process or display groups of files. Let’s consider a Windows Forms application that selects and displays all files under a certain root folder. Figure 10-3 shows the final result.

Figure 10-3
The sample application using the DirectoryListing class.

When the Go button is clicked, the following code runs:

private void btnGo_Click(object sender, System.EventArgs e)
{
    DirectoryListing dir = new DirectoryListing();
    DataTable dt = new DataTable();

    // Fill the table
    status.Text = String.Format("Loading from {0} ...", textBox1.Text);
    int nItems = dir.Fill(textBox1.Text, dt);
    if (nItems <0)
    {
        status.Text = String.Format("An error occurred accessing the path: {0}.",
            textBox1.Text);
        return;
    }

    status.Text = String.Format("{0} item(s) found.", nItems);

    // Shows the XML text
    textBox2.Text = dir.Fill(textBox1.Text);

    // Take care of the UI
    Form1.ActiveForm.Text = String.Format("DIR - {0}", dir.Path);
    dataGrid1.DataSource = dt.DefaultView;
}

The Fill method takes the path name and populates a newly created DataTable object with the structure described in the preceding code. In addition to having a sophisticated programmable interface, a DataTable object is a valid source for data bound controls. The following line of code is all you need to populate the grid of Figure 10-3:

grid.DataSource = dt;

You can apply a mask to the rows and filter them based, say, on the file extension. To do that, you simply create a DataView object on top of the table. Any data bound application that builds views of working data is better off using DataView objects throughout, even when no row filter is actually needed. In light of this, the preceding line of code should be rewritten like this:

grid.DataSource = dt.DefaultView;

Creating a view based on the file extension is as easy as writing the following code. The strFilter variable contains any file spec string with wildcards (e.g., *.exe). Figure 10-4 shows how the sample application handles filtered views.

// Casting requires that you always use DataView objects to bind data
DataView dv = (DataView) grid.DataSource;
dv.RowFilter = "Name Like '" + strFilter + "'";
Figure 10-4
A filtered view of the executables in the Windows system folder.
Rendering a Data View to XML

In ADO.NET, the DataSet object is the only object that can save file contents to XML. To render to XML only the subset of rows included in a given view, you have to create a temporary DataSet object filled with only the rows in the view. The following code demonstrates how to do this:

private String _DataViewToXml(DataView dv)
{
    DataSet ds = new DataSet("DirectoryListing");
    DataTable dt = dv.Table.Clone();
    ds.Tables.Add(dt);
    foreach(DataRowView drv in dv)
        dt.ImportRow(drv.Row);

    return ds.GetXml();
}

You first clone the structure of the table behind the DataView object. You then add the new DataTable object to the DataSet object and fill it with all rows in the current view. The ImportRow method automatically creates a duplicate of the DataRow object.

A Web Forms Application

The DirectoryListing class works as an extremely simple data adapter object and is independent of any database. Since the class exposes its data through either XML or a DataTable object, nothing prevents the data from being used over the Web in the context of an ASP.NET application. Figure 10-5 shows the class at work in an ASP.NET page.

Figure 10-5
A Web Forms application using the DirectoryListing class.

You link the class to the page using the @ Assembly page directive and then use it as follows:

<%@ Assembly Src="DirectoryListing.cs" %>

public DataView CreateDataSource(String strDir)
{
    DirectoryListing dir = new DirectoryListing();
    DataTable dt = new DataTable();

    dir.Fill(strDir, dt);
    return dt.DefaultView;
}

The full source code for the Windows Forms and Web Forms applications using the DirectoryListing class is available on the companion CD in the WinDirList and WebDirList folders, respectively.

note | So far I have deliberately skipped over how to apply and persist changes. But don’t panic—I’ll be discussing this topic soon.
Using OLE DB Providers

Creating an ad-hoc .NET class and/or a customized XML schema is definitely the first and probably the simplest option to consider when you want to expose proprietary data to .NET applications. Depending on your target audience, a .NET-to-.NET solution might not be optimal, even when you base it on XML.

OLE DB providers are fully supported in .NET by the generic managed provider for OLE DB data sources. The managed provider works for .NET applications in much the same way ADO works for COM applications. The common model is shown in Figure 10-6.

Figure 10-6
OLE DB providers in the context of .NET applications.

The OLE DB .NET Data Provider is made of all data access classes having the OleDb prefix and defined in the System.Data.OleDb namespace. This managed provider takes advantage of COM Interop Services to connect to the specified OLE DB provider for a given data source. Any interaction between a .NET application and an OLE DB provider takes place outside the CLR because such an interaction is actually between a .NET module and a COM module.

The OLE DB .NET Data Provider provides a bridge that enables a .NET application to access data through the same channels as a Win32 COM application. It gives you immediate access to all the existing OLE DB providers and saves your current investments so that applications can continue to successfully call into existing OLE DB providers.

If you have to design a software mechanism to make proprietary data available to the widest possible audience, writing an OLE DB provider is a valuable approach. Although its overall design is fairly complex and it is strictly COM-based, the OLE DB provider still represents your only option for writing a single module to make the wrapped data accessible to all.

In terms of interoperability and overall simplicity, only XML can surpass OLE DB. OLE DB has in its favor, however, a more thorough representation of the capabilities of the data source than any you could design using XML data structures. XML is only about data description, whereas OLE DB is about a common API for accessing data. To write OLE DB providers, you can use the ATL classes available from Microsoft Visual Studio 6.

caution | Not all OLE DB providers supply the same set of features, so the .NET data provider for OLE DB is not guaranteed to work with any OLE DB provider. You can safely use it with the provider for SQL Server (versions 6.5 and 7), Oracle (MSDAORA), and the Microsoft Jet engine. You cannot use it with the OLE DB provider for ODBC (MSDASQL). Test it carefully with any other provider you plan to use.

Although supported by .NET, OLE DB providers are no longer the preferred way to access data in the .NET platform. A different breed of components natively integrated in the .NET universe has taken their place: .NET data providers.


Inside .NET Data Providers

When compared with OLE DB providers, .NET data providers have a lot to offer. Their simplified data access architecture often results in improved performance without the loss of functionality. Furthermore, .NET managed data providers directly expose provider-specific behavior to consumers by using methods and properties. They also involve a much smaller set of interfaces than OLE DB providers. Last but not least, .NET data providers work within the boundaries of the CLR and require no COM interaction. For example, the data provider for SQL Server 7 and SQL Server 2000 hooks up directly to the wire level, enabling it to gain a substantial performance advantage. This advantage is shown in Figure 10-7.

Figure 10-7
Comparing the SQL Server data provider and the OLE DB data provider.

The data provider for SQL Server does not have to pass through an intermediate module—that is, the OLE DB provider for SQL Server—and uses instead the RPC to issue commands to the database. When compared with its corresponding OLE DB provider, the same advantages apply to any native managed provider.

The Architecture of .NET Data Providers

The functionalities supplied by a .NET data provider fall into a couple of categories:

  • Support for the DataSet class through an implementation of the methods of the IDataAdapter interface

  • Support for connected data access, which includes classes representing connections, commands, and parameters

The simplest data provider interacts with callers only by using the DataSet object for reading and writing. The other category of support allows you to control connections and transactions, and to execute direct commands, regardless of whether these are based on the SQL language. Figure 10-8 shows the class hierarchy for the two standard managed data providers in .NET: the OLE DB provider and the SQL Server provider.

Figure 10-8
.NET data providers connect, execute commands, and retrieve data mostly by accessing a specific data source.

The objects that wrap connections, commands, and readers are provider-specific and can result in different sets of properties and methods for each data provider. Any internal implementation is database-aware. The only class not included in this schema is DataSet because it is common to all providers and works as a generic container for disconnected data.

Table 10-1 describes the interfaces of a .NET data provider.

Table 10-1 Interfaces of .NET Data Providers
Interface | Description
IDbConnection | Represents a unique session with a data source.
IDbTransaction | Represents a local, nondistributed transaction.
IDbCommand | Represents a command that executes when connected to a data source.
IDataParameter | Allows implementation of a parameter to a command.
IDataReader | Reads a forward-only, read-only stream of data created after the execution of a command.
IDataAdapter | Populates a DataSet object and resolves changes in the DataSet object back to the data source.
IDbDataAdapter | Supplies methods to execute typical operations on relational databases (insert, update, select, delete).

Of all the interfaces listed in Table 10-1, only IDataAdapter must be present in every .NET data provider. If you don’t plan to implement an interface listed in the table or a method of an interface, expose the interface anyway but throw a NotSupportedException exception for each of its methods. Wherever possible, avoid a no-op implementation of methods and interfaces because doing so might result in data corruption, particularly when committing and rolling back transactions. In the rest of this chapter, I’ll review the main architectural features of a managed data provider and then offer you sample code for a simple provider that implements the same functionality as the DirectoryListing class. In doing so, I’ll also implement some update functions.

Make sure you assign your own data provider a unique namespace. In addition, prefix classes with a nickname that identifies the provider throughout any internal and client code. For example, use class names such as SqlConnection, SqlCommand, and SqlDataReader. In this example, the nickname is Sql. Try to use distinct files to compile individual functionalities.

Implementing a Connection

The provider connection class inherits from IDbConnection and must expose at least the ConnectionString, State, Database, and ConnectionTimeout properties. The following methods are mandatory: Open, Close, BeginTransaction, ChangeDatabase, and CreateCommand. You are not required to implement transactions. Likewise your data provider is not requested to support nested transactions, although IDbTransaction allows for that. The following code snippet gives you an idea of how to implement a connection:

namespace SampleDataProvider
{
    public class MyConnection : IDbConnection
    {
        private ConnectionState m_state;
        private String m_sConnString;

        public MyConnection ()
        {
            m_state = ConnectionState.Closed;
            m_sConnString = "";
        }

        public MyConnection (String connString)
        {
            m_state = ConnectionState.Closed;
            m_sConnString = connString;
        }

        public IDbTransaction BeginTransaction()
        {
            throw new NotSupportedException();
        }

        public IDbTransaction BeginTransaction(IsolationLevel level)
        {
            throw new NotSupportedException();
        }
    }
}

You should provide at least two constructors, one being the default, which takes no argument. The other recommended constructor accepts only the connection string. When returning the connection string by using the ConnectionString property, always return exactly what the user set unless security-sensitive information dictates otherwise. For example, you might want to remove a password.

The items you recognize and support in the connection string are up to you, but standard names should be used whenever it makes sense. The Open method is responsible for opening the physical channel of communication with the data source. This channel should not be opened before the Open method is called. Consider using some sort of connection pooling when opening a connection turns out to be an expensive operation. When the provider is expected to provide automatic enlistment in distributed transactions, the enlistment should occur during Open.

An important difference between ADO.NET connections and other connections such as ADO is that with ADO.NET, you are requested to guarantee that a connection is created and opened before any command is executed. Clients have to explicitly open and close connections, and no method will open and close connections implicitly for the client. This approach leads to a centralization of security checks. Checks are performed only when the connection is obtained, but the benefits apply to all other classes in the provider that work with connection objects.

You close the connection by using the Close method. In general, Close should simply detach the connection and return any objects to the pool. You can also implement a Dispose method to customize the destruction of the object.

The state of a connection is identified via the ConnectionState enumeration. While the client performs operations over the connection, you ensure that the internal state of the connection matches the contents of the State property. So, for instance, when you are retrieving data, set the connection’s State property to ConnectionState.Fetching.

Implementing a Command

The command object formulates a request for some actions and passes this request to the data source. If results are returned, the command object is responsible for packaging and returning results as a tailored data reader object, a scalar value, or via output parameters. According to the special features of your data provider, you can arrange for results to appear in other formats. For example, the data provider for SQL Server lets you obtain results in XML format when the command text includes the FOR XML clause.

The command class must support at least the CommandText property and the text command type. The provider parses and executes the command. This is the key aspect that enables a provider to accept any text or information as a command. Supporting command behaviors by using the CommandBehavior enumeration is not mandatory and, if needed, you can support more custom behaviors.

Within a command, the connection can be associated with a transaction. If you reset the connection (users should be able to change the connection at any time), first null out the corresponding transaction object. If you support transactions, when setting the Transaction property of the command object, consider additional steps to ensure that the transaction you’re using is already associated with the connection used by the command.

A command object works in conjunction with two classes representing parameters: xxxParameterCollection, which is accessed through the Parameters property; and xxxParameter, which represents a single command parameter stored in the collection. Of course, the xxx stands for the provider-specific nickname. For SQL Server, the two classes are SqlParameterCollection and SqlParameter.

You create a provider-specific command parameter by using the new operator on the parameter class or by using the CreateParameter method of the command object. Newly created parameters are populated and added to the command’s collection by using the methods of the Parameters collection. The module that allows command execution is then responsible for collecting data that is set via the parameters. You have the choice of using either named parameters (as the SQL Server provider does) or the ? placeholder (similar to the OLE DB provider).

You must have a valid and open connection to execute a command. You execute a command using any of the standard command types: ExecuteNonQuery, ExecuteReader, or ExecuteScalar. Consider providing an implementation for the Cancel and Prepare methods.

Implementing a Data Reader

As you learned in previous chapters, a data reader is a kind of connected buffer that the provider creates to let clients read data in a forward-only manner. The actual implementation of the reader is up to you, but let me offer a few important guidelines.

When the DataReader object is returned to the user, it should always be open and positioned prior to the first record. Users should not be able to directly create a DataReader object. Only the command object must create and return a reader. For this reason, mark the constructors as internal. Use the keyword internal in C#:

internal MyDataReader(Object resultset)
{

}

Use the keyword friend in Visual Basic .NET:

Friend Sub New(ByRef resultset As Object)
    MyBase.New

End Sub

The DataReader class must have at least two constructors: one that takes the result set of the query and one that takes the connection object used to carry out the command. The connection is necessary only when the command must execute with the CommandBehavior.CloseConnection style. In this case, the connection must be closed automatically when the DataReader object closes. The result set can take any form that serves your needs, for example, an array or a dictionary.

A DataReader object should properly manage the RecordsAffected property. This property is applicable only to batch statements that include inserts, updates, or deletes. It normally does not apply to query commands. When the reader is closed, you might want to disallow certain operations and change the reader’s internal state, cleaning up internal resources such as the array used to store data.

The DataReader object’s Read method always moves forward to a new valid row, if any exist. More important, it only advances the internal data pointer to the row but does not actually read the row. The actual reading takes place with other reader-specific methods such as GetString and GetValues. Finally, NextResult moves to the next result set. NextResult copies a new internal structure into a common repository where methods such as GetValues read from.

Implementing a Data Adapter

A full-fledged .NET data provider supplies a data adapter class that inherits both DbDataAdapter and IDataAdapter. The DbDataAdapter class implements a data adapter designed for use with a relational database, but use IDataAdapter when all you really need is a class that implements the IDataAdapter interface and copies some disconnected data to an in-memory programmable buffer such as the DataSet object. Implementing the Fill method of the IDataAdapter interface is in most cases sufficient to return disconnected data by using a DataSet object. Typical constructors for the DataAdapter object are the following:

XxxDataAdapter(SqlCommand selectCommand)
XxxDataAdapter(String selectCommandText, String selectConnectionString)
XxxDataAdapter(String selectCommandText, SqlConnection selectConnection)

Classes that inherit from DbDataAdapter must implement all the members and define additional members for provider-specific functionality. This ends up requiring the implementation of the following methods:

Fill(DataSet ds)
FillSchema(DataSet ds, SchemaType st)
Update(DataSet ds)
GetFillParameters()

The following are required properties: TableMappings, which defaults to an empty collection; MissingSchemaAction, which defaults to Add; and MissingMappingAction, which defaults to Passthrough. You can provide as many implementations of the Fill method as you need.

Table mappings govern the way in which source tables (that is, database tables) are mapped to DataTable objects in the parent DataSet object. Mappings take into account table names as well as column names and properties. Schema mapping refers to the way in which columns and tables are treated when adding new data to existing DataSet objects. The default value for the missing mapping property tells the adapter to create in-memory tables that look like source tables. The default value for the missing schema property handles possible issues raised when the DataTable objects are actually populated. If any of the mapped elements (tables and columns) are missing in the target DataSet object, the value of MissingSchemaAction suggests what to do. Both MissingXXX properties are a kind of exception handler. The value Add forces the adapter to add any missing table or column. No critical information is added unless another AddWithKey value is assigned to the property.

When an application calls the Update method, the class examines the RowState property for each row in the DataSet object and executes the required INSERT, UPDATE, or DELETE statement. When the class does not provide UpdateCommand, InsertCommand, or DeleteCommand properties but does implement IDbDataAdapter, you can try to generate commands on the fly or raise an exception. You can also provide a made-to-measure command builder class to help with the command generation.

Simple vs. Complex Data Providers

Just as in OLE DB, in .NET you have simple and complex providers. The difference between the two is in the number of implemented interfaces. If you fully support all the interfaces listed in Table 10-1, your data provider is complex. A fully loaded provider is needed only in a small number of cases—for example, when you have a proprietary wire format or are a database vendor. In such a case, the performance advantages are substantial.

More frequently, your ultimate goal is making proprietary data publicly available, for both reading and writing functions, by using a common programming interface. In this case, you don’t need command objects and parameters, nor do you need to pool connections or manage transactions. All you do need is the ability to read from and write to your data, which is the .NET counterpart of simple OLE DB providers. You can create a class for this purpose, but your class will not necessarily fulfill the requirements of a common interface. That’s why simple .NET data providers are definitely worth a closer look.


Writing a Simple Data Provider

Functionally, a simple .NET managed data provider is nearly identical to the DirectoryListing class we examined earlier. In terms of the internal structure, though, a significant gap exists between the two. A generic class such as DirectoryListing has no particular requirements, which can make it the major strength or the weakest point of your solution. In contrast, the class that outfits a simple managed data provider must implement the IDataAdapter interface and, subsequently, must expose a precise number of methods and properties.

Let’s consider extending the DirectoryListing class so that it works as a data provider. The class will need at least two constructors. The default constructor takes no argument and initializes the internal state of the object in a default way. A second constructor takes one or more arguments and serves as the command text that runs against the data source.

public class FileSystemDataAdapter : IDataAdapter
{
    String m_strPath;
    DirectoryInfo m_di;

    // Constructors
    public FileSystemDataAdapter()
    {
        m_strPath = "c:\\";
    }
    public FileSystemDataAdapter(String strPath)
    {
        m_strPath = strPath;
    }

}

The directory path of the second constructor represents the command text. The private member of the DirectoryInfo type represents the connection with the file system through which information is retrieved.

The Table Mapping Mechanism

As mentioned earlier, the IDataAdapter interface comprises the three properties listed in Table 10-2.

Table 10-2 Properties of the IDataAdapter Interface
Property | Description
MissingMappingAction | Indicates how the class should behave when a column has no entry in the specified table of mappings.
MissingSchemaAction | Indicates how the class should behave when the in-memory DataSet object lacks a source table, column, or relationship.
TableMappings | Returns a collection object (which is empty by default) that contains mappings between the column names used by the data source and the column names used by the DataSet object.

As described in Table 10-2, the MissingMappingAction property determines the action that occurs when a column name being selected by the adapter does not have a corresponding entry in the TableMappings collection. After the command terminates, the resultant set of rows and tables must be passed back to the caller packed in a DataSet object. The TableMappings collection determines how each column and table in the freshly obtained result set should be mapped in the DataSet object that the caller will actually get. By default each table in the result set originates a DataTable object with a prefixed name in the DataSet object. The first table takes the name that the caller specified (Table by default), and all other tables are named by adding a progressive index to the name of the first table (by default, Table1, Table2, and so on.) Likewise, each column in each result set originates a column in the table. By specifying a table mapping, you can give each table a name and control which columns are added to the DataSet object. This is helpful if your application needs to manage different user profiles. In this case, you can always run the same query but use different mappings for different categories of users. The MissingMappingAction enumeration defines three feasible actions: an exception is thrown, the column is ignored, or, as happens by default, the column is processed using the original data source name.

The MissingSchemaAction specifies the action to take when a client tries to add data to the DataSet object when the required DataTable or DataColumn object is missing. In this case, by choosing one of the values in the MissingSchemaAction enumeration, you can decide to raise an error, ignore the missing tables and columns, add the necessary tables and columns, or even complete the schema with important information.

Filling the DataSet Object

The IDataAdapter interface comprises the methods listed in Table 10-3.

Table 10-3 Methods of the IDataAdapter Interface
Method | Description
Fill | Adds or refreshes rows in the DataSet object. In your implementation, define the name of the tables used and created.
FillSchema | Adds a DataTable object to a DataSet object and configures the matching schema in the data source.
GetFillParameters | Retrieves the parameters that the user embedded in the command text.
Update | Executes the operations necessary to update the data source.

For simple data formats, you can implement all the methods in Table 10-3, except for Fill, to simply raise the NotSupportedException exception. You also don’t need table mapping. The following code is a typical null implementation of a feature you don’t provide. Raising an exception is preferable to a no-op implementation.

public int Update(DataSet ds)
{
    throw new NotSupportedException();
}

The implementation of a simple .NET data provider results in a class that has a handful of empty functions and just one working method—Fill. The source code for Fill closely resembles the core code of the DirectoryListing class. The IDataAdapter interface requires Fill to have at least one version that returns an integer and takes a DataSet parameter. Of course, you are free to define as many overloads as you need.

public int Fill(DataSet ds, String strTableName)
{
    if (!Directory.Exists(m_strPath))
        return -1;

    m_di = new DirectoryInfo(m_strPath);
    ds.DataSetName = "DirectoryListing";
    DataTable dt = new DataTable(strTableName);
    ds.Tables.Add(dt);

    Internal_CreateTable(dt);
    int nRows = Internal_FillTable(dt);
    dt.AcceptChanges();

    return nRows;
}

public int Fill(DataSet ds)
{
    return Fill(ds, "FolderItem");
}

The two internal functions used in this listing, Internal_CreateTable and Internal_FillTable, respectively, prepare the structure of the table and loop through subdirectories and files to add rows. Note that you should call AcceptChanges on the DataTable object prior to returning the rows. Doing so does not affect the actual data being returned but it does mark all rows in the table as unchanged. If you do not call AcceptChanges, each row is given a state of Added, which has a serious impact on the subsequent process of detecting changes for the update.

Using the Simple Data Provider

Let’s see how to use this data provider. The following code is an excerpt from an ASP.NET page that reads a path from a text box and populates a DataGrid control with the results:

public void OnLoadData(Object sender, EventArgs e)
{
    FileSystemDataAdapter da = new FileSystemDataAdapter(txtCommand.Text);

    DataSet ds = new DataSet();
    int nItems = da.Fill(ds, "MyTable");
    if (nItems <0)
        status.Text = "Directory not found.";
    else
        status.Text = String.Format("{0} item(s) found.", nItems);

    // Display the data
    grid.DataSource = ds.Tables["MyTable"];
    grid.DataBind();
}

You use the FileSystemDataAdapter class in the same way that you take advantage of the SQL Server data adapter in any ADO.NET application. You create a new instance of the class, specifying the arguments required by the adapter’s constructor. For simple providers, you allow users to indicate the command text by using the constructor rather than by setting up nonstandard properties or methods. After the adapter is up and running, you call its Fill method and pass a DataSet object to be filled. The Fill method creates and adds a new table that you can then use as any other .NET collection class. Figure 10-9 shows the new data adapter in action.

Figure 10-9
A directory listing retrieved using a simple data provider.
Updating the Data Source

For the FileSystemDataAdapter class, the underlying data source is the file system, in particular the originating folder for any information retrieved. An “update” has a different meaning depending on the data source. When the data source is a database, the update is inserting or deleting rows from a table. When the data source is the file system, updating might mean changing some attributes of a certain file or directory and even creating new directories or files.

Let’s suppose that your data provider supports editing the user comment associated with a particular folder. To simplify our example, let’s treat all the other information as read-only. A data adapter object updates the underlying data source by using the Update method. Although you can use a custom method with an ad-hoc signature, for consistency you should use the Update method or, at least, overload it. You should also add extra properties to the adapter unless you can perform the update using a simpler overload.

Detecting the Updates

Although no strict guidelines dictate the behavior of the Update method, you can easily figure the role it plays. The method must loop the whole table and check the state of each row. When a modified row is found, the new information is read and persisted to disk. The Update method is defined in the IDataAdapter interface as follows:

public int Update(DataSet ds)

This prototype is not optimal for our purposes, but changing it causes a compile error. A DataSet object can contain multiple tables, and typically the Update method processes the default table, which has a name of Table. We can manage the method and make Update default to the first table in the DataSet object or to the table with a fixed name you determine. The following code demonstrates the structure of the Update method:

public int Update(DataSet ds)
{
    // Determine the table to work on
    DataTable dt = ds.Tables[0];

    // Initialize the update counter
    int nUpdatedRows=0;

    // Loop through the rows in the table
    foreach(DataRow dr in dt.Rows)
    {
        if (dr.RowState == DataRowState.Modified)
        {

        }
    }

    return nUpdatedRows;
}

Earlier in this chapter, I remarked about the importance of calling AcceptChanges at the end of the Fill method. If you omit that call, during the update process the check against a Modified row state can repeatedly fail even though the row has been effectively updated. This happens because the Added state—the state of the row when a row is first added to the table—has a higher priority than Modified.

Implementing the Update

The goal of the Update method is to modify the contents of the desktop.ini file in the folder pointed to by the row. The row, however, contains only the name of the folder, not the full path. You solve this problem at the root by changing the implementation of the Fill method so that it returns the full path in the Name column. If you do not change the implementation, the prototype of the Update method lacks an important piece of information—the full path to the parent folder, which contains the directories and files listed in the table.

The full path can be made available by using a custom property such as parentFolder, which you set prior to calling Update. To avoid adding new properties, which keeps the overall interface of the adapter more consistent, you can overload the Update method, adding a String argument to denote the name of the parent folder. The code looks like the following:

public int Update(DataSet ds)
{
    // Default to the parent folder set to fill the table
    if (m_strPath != "")
        return Update(ds, m_strPath);
    return 0;
}

public int Update(DataSet ds, String parentFolder){
    // Determine the table to work on
    DataTable dt = ds.Tables[0];

    // Initialize the update counter
    int nUpdatedRows=0;

    // Loop through the rows in the table
    foreach(DataRow dr in dt.Rows)
    {
        if (dr.RowState == DataRowState.Modified)
        {
            // Files and folders listed in the table
            // are contained in parentFolder

        }
    }

    return nUpdatedRows;
}

When no parent folder is specified, the adapter defaults to the path used to fill the DataSet object. If this information is unavailable, the Update method fails. If the modified row points to a file instead of a folder, the update doesn’t happen. The name of the folder in the table is combined with the parent folder name to get the fully qualified path to desktop.ini. At this point, the update can take place.

String fullDirName = Path.Combine(parentFolder, dr["Name"].ToString());
_WriteDirComment(fullDirName, dr["comment"].ToString());

If the desktop.ini file does not exist, it is created with the following content:

[.ShellClassInfo]
InfoTip=...

The code that does this follows:

StreamWriter sw = new StreamWriter(strDesktopIni);
sw.WriteLine("[.ShellClassInfo]");
sw.WriteLine("InfoTip=" + newText);
sw.Close();

If the desktop.ini file already exists, the _WriteDirComment method first reads its contents and then modifies the string in memory. The file is then replaced with a new one that has the new content. Note that in Windows XP, the desktop.ini file must also be marked as a system file.

A User Interface to Update

To demonstrate the update feature of the file system data adapter object, I reworked one of the sample ASP.NET pages developed in Chapter 4. The new page provides an editable DataGrid control, as shown in Figure 10-10.

Figure 10-10
A user interface for editing the folder user comment.

In the sample page, only the Comment column is editable. After the text is entered, the DataTable object used to populate the grid is updated and the changes are made persistent by the adapter. The following code demonstrates the UpdateCommand event handler invoked by the DataGrid Web control when you save the changes on an editable row:

public void UpdateCommand(Object sender, DataGridCommandEventArgs e)
{
    // Get the new text in the comment textbox (column index is 3)
    int nColComment = 3;
    TextBox txtComment = (TextBox) e.Item.Cells[nColComment].Controls[0];

    // Get the underlying DataTable
    FileSystemDataAdapter da = new FileSystemDataAdapter(txtPath.Text);
    DataSet ds = new DataSet();
    da.Fill(ds, "MyTable");
    DataTable dt = ds.Tables["MyTable"];

    // Select the row being edited (only one element in the array)
    DataRow[] rg;
    rg = dt.Select("name='" + grid.DataKeys[grid.EditItemIndex] + "'");

    // Perform the update
    rg[0]["comment"] = txtComment.Text;
    da.Update(ds, txtPath.Text);
}

For the update to take place, you need to retrieve the DataRow object behind the DataGrid row being edited. You can easily do this by using the same techniques we analyzed in Chapter 4, which are based on primary key values. Although the contents of this table (a directory listing) does not come from a database table, you can still use the DataTable object’s Select method to locate a range of DataRow objects based on a condition. Likewise, you can use the DataKeyField and DataKeys properties on the DataGrid control to associate each grid row with key values for the underlying table.

In the displayed table of files and directories, the Name column represents a primary key. You cannot have two items (files or folders) with the same name in the same folder. After the correct DataRow object is retrieved and its Comment field updated, you call the data adapter’s Update method to persist changes. In Figure 10-11, you can see the grid after the update.

da.Update(ds, txtPath.Text);
Figure 10-11
A user interface displays the updated user comment.

Figure 10-12 shows that the changes were effective because Windows Explorer now reflects them. The full source code for the FileSystemDataAdapter object is in the FileSystemDataAdapter.cs and files available from the companion CD.

Figure 10-12
Changes reflected in Windows Explorer.


Conclusion

ADO.NET provides a whole bunch of general-purpose objects that are particularly appropriate for storing tabular data regardless of where the data originates. These objects work great, and are well integrated, with data adapters—particularly with the data adapters provided by .NET data providers for commercial databases. Of course, you can use the same general-purpose infrastructure with tabular data retrieved through other channels as well.

In this chapter, I demonstrated how to use a simple data provider that offers read and write capabilities to expose nonrelational data.


Some Final Thoughts on the Future of ADO.NET

Afterword
Some Final Thoughts on the Future of ADO.NET

For a programmer who builds data-centric applications, the development of ADO.NET marks a technological quantum leap. Its object model is elegant and clean, and language neutral. The resultant ADO.NET object model is also powerful and effective but, believe me, that’s just one of its many advantages over ADO.

Prior to ADO.NET, the programmer had to opt for ease of development or choose fast, but quirky and error-prone, code that was arduous to write. You could either take the Visual Basic route and fall into the capable hands of ADO or you could be a brave and intrepid programmer and take the enlightening journey into the depths of OLE DB. With .NET, you no longer have to choose between the lesser of two evils. ADO.NET provides a common API for writing data-driven applications no matter what the application model—Web forms, Windows desktop applications, or Web services. The language neutrality of .NET makes the question about what came first—the Visual Basic egg or the C++ chicken—irrelevant. ADO.NET takes the best of both worlds and weds the simplicity of ADO to the effectiveness of raw OLE DB. In doing so, it also gets rid of the code that made ADO somewhat slow, and abstracts the data model that made OLE DB somewhat tedious to understand and boring to use.

For those of us who invested time and money in ADO, the introduction of ADO.NET is a double-edged sword. On one hand, we can appreciate the power and modern design of the object model. But on the other hand, the switch to ADO.NET, turns most ADO code into legacy code. It doesn’t matter when the code was written; legacy is a timeless concept, and we are all writing code today that will be marked as logically obsolete by the time we finish it. Logically obsolete is not a condition that invalidates code; fortunately, the code will still work. It simply means that if you keep writing the same code, you’re going to end up at a dead-end. Of course, you can always change course and the good news is that while we can’t reuse our existing ADO code, we can transfer our ADO skills to ADO.NET. Our knowledge never becomes obsolete.

A key question for someone contemplating switching to ADO.NET is: Can we really rely on ADO.NET, or is it just another trendy data access layer for this summer’s applications? In recent years we have observed a proliferation of data access models, and ADO looked like the definitive tool based on two now-and-forever underlying technologies: COM and OLE DB. Of course, in late 1998 when ADO 2.0 was introduced, no one could have predicted the .NET thunderstorm. But now, as we look into the eye of this storm, we realize that to determine its future, we need to weigh its advantages and see its growth potential.

In several aspects, ADO.NET is a better and richer model than ADO. But are they functionally equivalent? The answer is no. To compare their functionality, we have to talk about the schema object model (ADOX) and server cursor support.

Today, to manipulate the database schema from within .NET applications, you have to come up with work-arounds. You can issue provider-specific queries and work in read/write manner. One alternative is to read (only) schema information through the OLE DB .NET data provider and the adapter’s GetOleDbSchemaTable method, or you can use COM Interop Services to call into the COM-based ADOX object model. In the future, ADO.NET will provide read/write access to table schema information using a .NET object model based on DataSet, Table, Column, and Constraint objects.

Server cursors are not supported in ADO.NET. For positioned updates and long lock operations, you can use multiple SQL statements and keep connections and transactions open; however, a new bunch of ad hoc objects are on the way. The principal behind these ad hoc objects is the ResultSet—which is a provider-specific scrollable and updateable server cursor. Each .NET data provider will be exposing its own implementation of such an object in much the same way as today’s .NET data providers expose data readers and data adapters. A ResultSet will work in a similar way as the writing counterpart of today’s reader objects. Such a ‘data-writer’ object would allow an application to position on a record, read/write its value, and persist updates.

So much for the question of functionality, however, we also need to ask what is the key advantage of the ADO.NET object model over ADO. The answer to this question is the argument we poor programmers can use to convince our bosses to give us the green light for the adoption of ADO.NET. Well, in ADO.NET version 1.0 there is a small, but potentially explosive, feature that will be significantly expanded in future versions. The feature I’m referring to is called typed DataSets.

A typed DataSet is a .NET class built atop the DataSet class. It inherits all the methods, events, and properties of a DataSet and provides strongly typed methods, events, and properties. Each table and column becomes a child object of this data-specific object and can be accessed by name instead of using ad hoc, but necessarily generic, collections like Tables, Columns, and Rows.

A typed DataSet does not necessarily give you more programming power. It does however improve the readability of the resultant code and, when used within Visual Studio .NET, activates the IntelliSense technology and provides auto-completion. In addition, with a typed DataSet, type mismatch errors are caught at compile time rather than at run time. Creating typed DataSets today requires the use of a command-line tool (also buried in the Visual Studio.NET interface) to generate a new class from an inferred XML schema. Aside strongly typed access to data, a second key feature of typed DataSets is that you can use them to expose data inside a DataSet as stand-alone objects. These features will be expanded a bit in the next versions of ADO.NET and evolve into a new software entity called the object space. The object space will dynamically generate the necessary code at run time. It contains objects to store, update, and query data realizing a neat separation between business logic and data access code. Relationships and constraints will still be supported and automatically enforced. A new object query language—that has explicit recalls to XPath—completes the package. From the programmer’s standpoint, you create only an entry point to the object space and then move within the space using a common and generic set of methods. Schema and code that proves to be necessary for further operations is generated on the fly.

There’s still a lot you can expect from ADO.NET, and the goal is to make effective data access as smooth and productive as possible. In this final chapter, I’ve quickly outlined the future developments of ADO.NET. But ADO.NET as it comes out of the box in .NET Framework 1.0 is already a big deal. Enjoy, but stay tuned for more!


Dino Esposito

Dino Esposito is Wintellect’s ADO.NET expert and a trainer and consultant based in Rome, Italy. A frequent speaker at popular industry events such as Microsoft TechEd, DevConnections, and WinSummit, Dino writes the monthly “Cutting Edge” column for MSDN Magazine and the “Diving Into Data Access” column for MSDN Voices. He also regularly contributes to Developer Network Journal (http://www.dnjonline.com), MSDN News, and asp.netPro Magazine (http://www.aspnetpro.com). Dino is the cofounder of http://www.vb2themax.com.


LOC

PUBLISHED BY
Microsoft Press
A Division of Microsoft Corporation
One Microsoft Way
Redmond, Washington 98052-6399

Copyright © 2002 by Microsoft Corporation

All rights reserved. No part of the contents of this book may be reproduced or transmitted in any form or by any means without the written permission of the publisher.

Library of Congress Cataloging-in-Publication Data

Esposito, Dino, 1965-

Building Web Solutions with ASP.NET and ADO.NET / Dino Esposito.

p. cm.

Includes index.

ISBN 0-7356-1578-0

1. Web sites--Design. 2. Active server pages. 3. ActiveX. 4. Microsoft.net framework.

I. Title.

TK5105.8885.A26 E87 2002

005.2'76--dc21

2001059042

Printed and bound in the United States of America.

1 2 3 4 5 6 7 8 9 QWT 7 6 5 4 3 2

Distributed in Canada by Penguin Books Canada Limited.

A CIP catalogue record for this book is available from the British Library.

Microsoft Press books are available through booksellers and distributors worldwide. For further information about international editions, contact your local Microsoft Corporation office or contact Microsoft Press International directly at fax (425) 936-7329. Visit our Web site at www.microsoft.com/mspress. Send comments to mspinput@microsoft.com.

BizTalk, IntelliSense, Microsoft, Microsoft Press, MSDN, Visual Basic, Visual C++, Visual C#, Visual Studio, Win32, Windows, and Windows NT are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries. Other product and company names mentioned herein may be the trademarks of their respective owners.

The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious. No association with any real company, organization, product, domain name, e-mail address, logo, person, place, or event is intended or should be inferred.

Acquisitions Editor: David Clark

Project Editor: Lynn Finnel

Technical Editor: Mark Bukovec