Server-side ASP.NET Data Binding, Part 2: Customizing the DataGrid Control | |
Dino Esposito | |
Download the code for this article:Cutting0104.exe (46KB) Browse the code for this article at Code Center: ASP.NET Data Binding 2 |
|
ast month (March 2001) I covered the basic techniques of binding ASP.NET server controls to fields of Microsoft® .NET managed data sources. In particular, I focused on the main features of the DataGrid control, which is probably the richest of all the ASP.NET list-bound controls. Unless you have very special needs, using a DataGrid is as easy as binding the .NET data source with the control and setting the size of the page. The control is capable of auto-generating the columns and exposes a wide variety of graphical properties. As a result, you can get a table report like the one in Figure 1 with surprisingly simple server-side code. Everything you need is only a few properties away. Hooking DataGrid Events Last month I only touched on DataGrid events and item creation. This month I'll expand on these topics because they're extremely important when building advanced functionality. Figure 2 lists the events exposed by the Web control. For now, let's skip the XxxCommand events, which I'll cover later when talking about bindable columns. Init, Load, and Unload are basic events in any control lifecycle. You normally don't need to take any particular action to respond to these events. PageIndexChanged is fundamentally important if you want to page your list of records. This event lets you refresh the data source to reflect the current page. You'll also have to use it if you plan to implement a custom pager, as I'll be doing in a moment. SelectedIndexChanged is equally important in the context of the item selection functionality.
Next, all the child controls are enumerated through the pager's Controls collection:
Notice that you must move the index by two. This is because the standard pager generates a code structure like the one in Figure 3. It's a table cell whose content is a blank-separated string formed by just one <SPAN> and as many <A> tags as needed. The <SPAN> represents the index of the current page. The <A> tags represent all the other pages. In nonnumeric mode, you only have two tags, either <SPAN> or <A>. Between these tags there's always a blank space ( ). Even this simple character in ASP.NET is rendered through a Web controlâ€"a System.Web.UI.LiteralControl in particular. That's why you must step by two.
You instantiate a generic variable of type WebControl and then check its type against the two possible .NET types representing <SPAN> and <A> tags: System.Web.UI.WebControls.Label and System.Web.UI.WebControls.DataGridLinkButton.
In Figure 4 you can see a slightly modified, yet perfectly acceptable, pager obtained with the previous code. You can use the Text property to set the text and the CssClass property to set all the user interface settings. Notice that behaviors can be extremely useful for adding more action to selected buttons. Don't be scared by the scarce support for behaviors you get outside Microsoft Internet Explorer 5.0 and higher. They are just ignored by all other browsers. A Custom Pager Control Just by hooking the ItemCreated event, you can introduce significant modification to the pager. However, you could do more; you could completely replace the standard pager with a brand new component. To do so, simply hide the system-provided pager and insert your own paging controls. In this case, you're responsible for manually setting which page to display, but you can decide to move any number of pages at a time, jump to a specific page, or display extra text. Let's see how to create a pager with a descriptive text and a textbox to type in the page number.
At this point, there's just no way for you to page back and forth unless you define your own controls and teach them how to comply with the DataGrid's current page. Your custom pager is a piece of HTML code that's separate from the DataGrid. You could place it just below the table of recordsâ€"where the built-in pager would beâ€"or in any other corner of the page. Since it is a completely distinct piece of HTML, there's nothing to prevent you from displaying both the built-in and the custom pagers. In this case, you could consider the custom interface to be an extension of the standard one.
Suppose you're going to use the code in Figure 5. The pager is composed of a label followed by a TextBox and a button. In particular, the button component looks like this:
It has a CommandArgument property and an OnClick handler. CommandArgument represents the verb behind the button. It is a word, or a verb, that describes what's going to happen when the button is pressed. Of course this is supposed to have a unique name, in case you introduce more buttons. You don't strictly need it if you have only a single button. The OnClick handler points to the function responding to the user's click.
To access the button object and read the command argument, do the following:
At this point, you only need to update the current page index and refresh the data source. CurrentPageIndex is a zero-based counter that ranges up to the value of the PageCount property of the DataGrid object minus one. You might want to make sure that any assigned value falls in this range. So the value read from the textbox
will be modularized against 0 and PageCount. To handle incorrect nonnumeric text being typed in the textbox, just use a try/catch block:
Once you've properly set the CurrentPageIndex property, you need to rebind the data source to make sure that the content shown reflects the current page. The code in Figure 5 always uses the same function (BindData) to load the ADO.NET dataset, regardless of the particular page to be displayed. As you may have guessed already, this is not the optimal solution. There's no need for the client to download the entire dataset created by the query. You should either apply filters to the SQL command or force the CreateDataSource function to return a smaller collection of rows based on the current page index. However, if you manage to use page-sized datasets, then remember to set the DataGrid's AllowCustomPaging property to True. If you don't, the first record displayed will be the one that occupies this position:
As you can see, this will result in erroneous results if you try to view page two with a total of PageSize records in the dataset. When custom paging is allowed instead, the DataGrid always displays the records from position 0, which would be perfect in this case. Figure 6 shows Figure 5's code in action. Similar examples can be found at https://www.gotdotnet.com/quickstart/aspplus. Sorting Columns The DataGrid component also provides basic support for sorting. This ability depends on the sorting capabilities of the underlying data source. The data source is always responsible for returning a sorted set of records based on the specified sort fields. The DataGrid, in turn, provides a straightforward programming interface to turn this feature on. The built-in sorting mechanism is triggered by the AllowSorting property:
When AllowSorting is set to True, the DataGrid utilizes a LinkButton to render the caption of the column. The OnSortCommand event is fired whenever you click on any of the column captions.
The built-in sorting mechanism doesn't allow you to choose which column to sort. All displayed columns are therefore sortable. By handling the sort command, you are supposed to rebind to the data source and obtain sorted data to display again. The field on which to sort is passed through the SortField member of the DataGridSortCommandEventArgs class. Assuming you have a function called BindData to refresh the link between the DataGrid and data source, this is the typical body of OnSortCommand:
The problem with sorting is keeping track of the current sorting field. Some of the samples available with the .NET Framework documentation use a global variable. While this solution is perfectly functional, I'd rather use the Attributes collection of the DataGrid object. You initialize the page like this:
SortField becomes a custom attribute of the DataGrid object. If not defined, it returns an empty string. BindData will then use the argument as the sort field when creating the data source:
To establish a sort order in an ADO.NET DataTable, just set the view's Sort field properly:
Sorting the standard way is extremely easy, but it's not an all purpose solution. If you don't want to allow sorting for all columns, or if you want to put a bitmap on the header, you need custom sorting. You define custom sorting by including bound columns and template columns in the grid. Binding Columns So far I've only considered DataGrids whose columns were generated automatically and ended up perfectly matching the total number of columns in the dataset. To disable this feature just set
Once you do this, you're responsible for providing columns yourself. The <asp:BoundColumn> tag allows you to do this. The following code is functionally equivalent to the code shown in Figure 6, but lets you handle the various columns as distinct objects.
This code has an immediate effect: you can now assign a custom caption to the columns through the HeaderText attribute. DataField is the name of the data source field to which the column is bound. You can adjust the interface of the columns by setting a number of graphical properties for the column as well as its header and footer. These attributes include font, colors, styles, borders, and alignment and apply on a per-column basis. For example, you can have a column with a different font style and a uniform background color regardless of each item's corresponding settings. Sorting Bound Columns To make a column sortable, just add the SortField attribute to the <asp:BoundColumn> element. For example:
This attribute works in conjunction with AllowSorting and OnSortCommand, which I examined earlier. The SortField attribute simply allows you to decide which columns must be sortable, when, and by which field. You can also use an image to paint the column's caption. In this case, just use the HeaderImageUrl property, and set the URL to an image to display (instead of text) on the header of the column. If you want both text and image, you can embed an <img> in the HeaderText property.
The final effect is shown in Figure 8, where you can see how the page looks in Internet Explorer 5.5. Types of Columns In order to manipulate the content and, more importantly, the layout of the columns, you can bind them dynamically by choosing from five different types: bound columns, button, hyperlink, edit, and template columns. Bound columns are just ordinary columns that mirror the content of a data field. Using bound columns is advantageous with respect to auto-generated columns only because you can decide on the styling, sorting, and position. But what if you need to associate an action with the items of a certain column? And what if you need it to show the result of a dynamically evaluated expression rather than the static text coming from the data source? What if you want to select, click, and edit the underlying row? In these cases you just need more powerful tools like the other types of DataGrid columns.
HeaderText is the title of the column, and Target represents the window or target frame that is used to display the contents resulting from the navigation. Use _self if you want to navigate to the same frame. The key attribute is DataNavigateUrlFormatString. It usually looks like this:
The {0} substring is a placeholder for the corresponding value in the column specified by DataNavigateUrlField. There's no way to use more than one replaceable symbol. DataTextField is the name of the data source field you might want to associate with the column's Text property. If you just want a constant text to be displayed for all the columns, use the Text property instead. You're given some leeway to customize the data-bound text through the DataTextFormatString. The following code originates the page on the left-hand side of Figure 9.
On the right-hand side of Figure 9 you can see a button column that originated from the following text:
A button column draws a button with text. DataTextField and DataTextFormatString work as they do for HyperLink columns. A button can be rendered in two ways: like a hyperlink (the default) or like a pushbutton. In the latter case, just add
To handle the click on a button, in the DataGrid define an attribute called OnItemCommand and make it point to a function with the following prototype:
In the code, you first check the value of the button's CommandName property, then call the appropriate handler code. For instance, the asp:ButtonColumn declaration shown earlier sets MoreInfo as the CommandName when the "More Info" column is clicked. The handler might look like this:
The difference between button and hyperlink is minimal anyway. You should use a hyperlink if the consequent action requires the use of a new page. Otherwise, a button is more appropriate. Template Columns Before I get into template columns, let me just mention edit columns. These columns are extremely powerful because they allow you to create in-place editing dialogs. However, since this is an important topic that deserves more space, I'll revisit it next month.
However, template columns are the perfect solution to create combinations of HTML text and controls, and provide a completely custom and data-bound layout for a column. For example, you can use a template column to render a data source Boolean value through a read-only checkbox control or to dress up the column's layout with images. Let's see how it works. You can insert a template anywhere in the DataGrid and have it host any combination of columns.
The column template is specified through the <template> tag. A template is characterized by a Name attribute, which defines how items have to be rendered. For DataGrids, the Name attribute must be ItemTemplate:
The body of <template> can contain any combination of static text, HTML elements, and ASP.NET server controls. Let's get rid of the EmployeeName column and ask the data source to return the following query:
Now let's see how to get the same EmployeeName column through templates. In this case, you don't need more than a label control to render any column cell.
The Text attribute is data-bound. Here's a possible value for it:
You can use any kind of element in the data binding expression provided that there's a way to evaluate it at runtime. DataBinder.Eval is a static method that evaluates late-bound data binding expressions. The first two arguments must be the naming container for the data item and the data field name. In a DataGrid control, the naming container is always Container.DataItem. Another possible naming container is Page, but this applies to simple data binding and outside template-based data controls such as DataGrid, DataList, and Repeater. DataBinder.Eval can also take a third optional argument, which is the format string for the resulting text. Using DataBinder.Eval is not a must, but it saves you from creating less readable code. For example,
evaluates to
In Figure 10 you can see that the employee name is the combination of three different fields and the last name has been rendered in bold. You can also see a checkbox in one of the columns.
Now the template is given by an <asp:checkbox> control, whose state is determined by the result of the HasBoss function. The function takes an int argument which is the content of the ReportsTo field.
When I first ran this code, I came across a rather weird invalid cast exception. After a bit of investigation, I found that it happened because the ReportsTo column can contain NULL values and you cannot cast from System.DBNull to int. The solution was straightforward: just make SQL Server return a slightly modified dataset.
Now any NULL value is automatically replaced by 0, and everything works just fine. What's Up Next? The previous caveat is useful because it reminds you that bindable and templated columns are extremely useful, but you'll benefit greatly from a solid understanding of the SQL language. The next step in this tour of ASP.NET and ADO.NET data binding is adding a bit of interactivity to an otherwise static grid, namely particular item selection and in-place editing. |
|
Dino Esposito is a trainer and consultant based in Rome, Italy. Author of several books for Wrox Press , he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (https://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com. |
From the April 2001 issue of MSDN Magazine.