he DataGrid Web control is by far the most powerful and versatile of all ASP .NET controls. Not only does it have many properties that you can set programmatically, but it lends itself to more advanced forms of customization. In this column, I'll build a few flavors of DataGrid-based Web controls. In doing so, I'll review the major design issues that characterize these controls and answer some of the questions most frequently raised by readers after they tackled the series of DataGrid columns in the March through June 2001 installments of Cutting Edge. In particular, you'll see how to add a custom toolbar to a DataGrid control to accomplish common tasks such as New and Unselect. Designing DataGrid-based Controls
When you use a DataGrid control in your ASP .NET code, you inevitably end up using a lot of code. This is the natural result of the control's extremely rich programming interface. For example, the chunk of code in Figure 1 merely sets some visual properties of the grid. It disables the automatic generation of columns, sets fonts, borders, and colors, defines the style of some commonly used items, and appends columns to the grid. Figure 2 shows the output of a page that employs this code. .gif) Figure 2 Output
All controlsnot just DataGrid controlsoffer great flexibility. You can replace, restrict, or extend existing functionality. Or you can encapsulate some common settings to reduce the amount of code necessary to build a given solution. As an illustration, let's see how to build a custom Web control that saves you from the tedium of repeating the code to set the same visual settings. See Figure 3 for the complete code for the DefaultGrid control.
The DefaultGrid class derives from DataGrid and, at this stage of development, only defines a custom constructor. public class DefaultGrid : DataGrid
{
public DefaultGrid ()
{ ... }
}
In the constructor, I set some of the properties to new default values. It can still accept programmer input, but by using these default settings, the code necessary to use the DefaultGrid control will shrink significantly. By specifying attributes in the ASP .NET code, you can still override the DefaultGrid's default settings like you do with the normal DataGrid control. The DefaultGrid control is in no way forced to ignore tag-level settings.
Make sure that all the default settings you implement are set only in the class constructor. If you set properties elsewhere, like in the control's OnLoad event, then you make the control insensitive to the external, tag-level settings. A control's lifecycle begins when it's initialized and its constructor is called. Then the control's state is updated with information from the page. Only after that does the control fire its OnLoad event. You hook into the OnLoad event if you need to perform some action only after the control's state has been completely restored. public DefaultGrid ()
{
// Set defaults here ...
// Set the OnLoad handler
Load += new EventHandler(OnLoadGrid);
}
For example, you can register a control-level OnLoad handler if you need to validate the state of the control and update some properties according to the value of some others.
In Figure 4, you can see an extremely simplified version of the code necessary to produce the output in Figure 2. Specifically, there are many fewer properties set in the DataGrid declaration because the default settings are accepted. Incidentally, the code looks even simpler due to the use of the code-behind technique that I described in the August 2001 Cutting Edge. Taking Customization to the Next Level
Since a custom control is so good at hiding a lot of boilerplate code, why not take this to the next level, plugging in a derived control and some features like pagination and sorting? Let's do that after seeing how to change the style of the pager bar child controls and how to automate the display of a glyph in the column heading whenever that column is sorted. Custom pager controls are the hyperlinks and the labels that display the current page and other pages that are available for selection.
You should know that the DataGrid control is composed of a number of heterogeneous items. You can have up to eight different types of items in your grid: normal and alternating items, separators, headers, pager and footer, the currently selected item, and the item that is currently being edited. When any of these items are created, the DataGrid control fires an ItemCreated event, against which you can write a proper handler. public void OnItemCreated(Object sender, DataGridItemEventArgs e)
Within the control's code you can define a custom version of this handler to carry out all the actions that you want to execute at that time (for example, changing the CSS style of a pager's controls). As a loyal reader of this column, I expect you to be extremely familiar with the source code necessary to get this. public void OnItemCreated(Object s, DataGridItemEventArgs e)
{
ListItemType itemType = e.Item.ItemType;
if (itemType == ListItemType.Header)
{ ... }
if (itemType == ListItemType.Pager)
{ ... }
}
In particular, when the pager is being created, you might want to get a reference to the controls that actually comprise it and apply some changes to them. In the HTML code that's generated from the control, the pager appears as a table row (a <TR> tag). The goal is to make the link buttons clickable. Those controls are defined with a child <TD> tag in the pager row. The following code returns the link buttons container as a TableCell object. TableCell pager = (TableCell) e.Item.Controls[0];
At this point you're making the assumption that the pager mode is still NumericPages, as you set it in the DefaultGrid constructor. While this is a reasonable assumption for plain DataGrid code, it may raise some exceptions if you blindly apply it to a derived control. You set the pager mode in the control's constructor, but you (or one of your users) could have overwritten it at the tag level. If this happens, the next chunk of code that works with link buttons and labels will produce unpredictable results or errors.
There are a few ways to work around this particular issue, but there will be other problems when you try to package the DataGrid in very specialized controls. Figure 5 shows the source code for the new PageableGrid control. It features two custom properties, which store the CSS styles for the links and the current page label in the pager. The two properties can be declaratively set at the DataGrid level in the ASP .NET code, like this: PagerCurrentPageCssClass="CurrentPage"
PagerOtherPageCssClass="HotLink"
The two strings refer to CSS styles that must be defined in the page.
The most generic approach to setting this is to check the current pager mode and act accordingly. Alternatively, you could override the PagerStyle's Mode property in the OnLoad event and make sure it is always set to the right value. .gif) Figure 6 PageableGrid in Action
Figure 6 shows the new control (that's similar to the one used earlier) in action in an ASP .NET page. The code necessary to use the PageableGrid control is still rather short. Notice also that the code-behind class for this page has not changed from the previous example. You can compare the TestDefaultGrid.cs and TestPageableGrid.cs files to the corresponding .aspx files, available with this month's source code at the link at the top of this article. What about Sorting?
Wouldn't it be nice to have a grid control that automatically reverted to the previous sorting order when you double-clicked on a column? And what about denoting this with a special icon? To add a glyph on each sorted column to denote the direction of the sorting, you need a couple of pieces of information that aren't available at the DataGrid level. In particular, you need to know the current sort expression and the current sorting direction.
The custom DataGrid would have to expose two new properties; let's call them SortExpression and IsSortedAscending. I made these properties read-only (get statements, but no set statements). public String SortExpression {
get { return Attributes["SortExpression"]; }
}
public bool IsSortedAscending {
get { return Attributes["SortedAscending"]=="yes"; }
}
You can implement these properties with local members. If you do, make sure that those new members are correctly persisted across multiple invocations using the ViewState object. If you don't, the control can't remember the previous settings when the page is posted back. When you click on a column to sort, you cause a postback event. As I explained last month, when the page that contains the control is invoked, it first creates a new instance of the control. This instance, of course, has all properties set to default values. Next, the state of the control is updated to reflect the settings in force during the previous call. However, only the properties explicitly persisted through ViewState can be successfully restored. The base DataGrid control persists a number of properties, but newly added properties can only be persisted manually by the developer of the control. To make a property persistent, store its value in a ViewState slot rather than in a local member variable.
As an alternative, you can use the DataGrid's Attributes collection. Members of the Attributes collection are already persisted by the base control and the collection was designed to be a generic data container. So a good compromise would be to define two custom slots within Attributes to represent your extra information. When you set up your control this way, it will be automatically persisted across multiple page invocations.
The sort expression and direction information comes in handy when you decide which glyph to display and where. Figure 7 shows the source of a new DataGrid control, SortableGrid, which sums up sorting and paging capabilities. When a header item is going to be created, the code reads (from Attributes) the current sort expression and the direction. Since Attributes can manage only strings, the Boolean value of the ascending sort direction is stored as a "yes" or "no" string. This information is then translated into a Boolean when someone queries for the IsSortedAscending property.
The glyph is simply a Label control that uses the Webdings® font. This label is added to the Controls collection of the cell that represents the column heading. Looping through the grid's Columns collection lets you know the column to sort by. strSort = Attributes["SortExpression"];
for (int i=0; i<Columns.Count; i++)
{
if (strSort == Columns[i].SortExpression)
{
// do some sorting here
}
. . .
}
When the currently stored SortExpression matches the SortExpression property of a given column, your search can end since you have found the column that needs the glyph. Notice that SortExpression is already a property of DataGridColumn objects. This has nothing to do with the SortExpression property that I've just defined for the SortableGrid control. The base DataGrid has no SortExpression property, and this is just one of the problems SortableGrid had to solve.
The following code adds a glyph to a column after sorting: TableCell cell = e.Item.Cells[i];
Label lblSorted = new Label();
lblSorted.Font.Name = "webdings";
lblSorted.Font.Size = FontUnit.XSmall;
lblSorted.Text = strGlyph;
cell.Controls.Add(lblSorted);
The glyph is rendered as the character 5 or 6 of the Webdings font.
So far I've just tuned the code to show when a column is sorted and in which direction. I need to do a lot more work to make sorting itself work properly. First, AllowSorting must be set to True. You can do this in the DataGrid constructor. Since the grid is meant to be sortable, you might want to make AllowSorting a read-only setting. You can certainly move the code that sets it from the constructor to the OnLoad handler, overriding any value that might have been set at the tag level. However, unlike PagerStyle.Mode, which was a field of a DataGrid property, AllowSorting is a property itself. In a derived control, you can always override the property and make it read-only. This is probably the best approach because any wrong action results in a compiler error.
When AllowSorting is set to True, clicking on a sortable column header causes the OnSortCommand event to fire. You normally handle this in the code behind the page. However, because the code is rather vanilla and you want to keep the page as slim as possible, let's see what you need to do to move it into a control. SortCommand += new DataGridSortCommandEventHandler(OnSortCommand);
This line sets an event handler within the SortableGrid control for the SortCommand event. The handler is responsible for storing the new sort expression, taking it from the SortExpression property of the clicked column. In addition, the sort handler will check whether the clicked column is already the sorted column. If so, it updates the value of the SortedAscending attribute by reverting it.
All of this stuff doesn't really need to be run outside the boundaries of a control. Once you have properly updated the internal state to reflect the new sort expression and direction, you still need to update the user interface. In other words, you need to fire an event in response to which the page, or the control itself, rebinds to its data source and updates the view.
Normally, you do this at the page level with the following code: void SortByColumn(Object s, DataGridSortCommandEventArgs e)
{
UpdateDataView();
}
UpdateDataView is a page-specific function (see Figure 8). It has three tasks to accomplish. First, UpdateDataView must recover the data source from Session or from any other place where it has been stored. This is a key point that I'll discuss in a moment. Second, it must apply the sorting schema to the data and ensure it will be displayed properly. Third, it must at least trigger the redrawing of the control.
Desktop DataGrids are self-contained controls that you use out of the box. You don't know anything about their internals and cannot change anything they don't explicitly allow you to change. The Web DataGrid has been designed differently. When you use a DataGrid control in your Web Forms you basically end up assembling the various pieces of the programming interface. To obtain a pageable and sortable grid, you need to add a lot of code. This code is the same in all DataGrids you use, so it's reasonable to wrap it all in a new control.
Desktop DataGrids don't tell you where they store the data you bind to them. However, they often store it in memory to improve their efficiency. Can you do the same with Web controls? You could do it, but that doesn't necessarily mean you should. While the available memory of a desktop system depends upon the number of applications the user has open at the same time, a Web server's memory is affected by the number of sessions currently active. Deciding to keep all of your grid data in memory means that you store it in Session, or if data can be shared across the application, you can even store it in Application or Cache. If you choose to store your grid data in Session, the application's scalability can be seriously affected, if not compromised completely. Choosing where the grid has to read data from is a key aspect of Web application design. For this reason, the design of the control is generic and requires a lot of code.
Session may or may not be the right place to store your grid's data. An alternative is to read data from the database each time you need it for a new page or sort. Although this process can be optimized in various ways, it requires frequent access to the data.
Another rather scalable alternative is making use of session-based XML files that keep the Web server free of memory while not requiring frequent database access. In addition, such an approacha sort of server-side XML data islandcan be successfully employed to survive the worst-case scenario in which you have millions of records to fetch and handle. I'll delve into this topic in more detail in a future column. For now, I'll just reiterate that to have a fully self-contained grid control, you must decide and handle within the control how the data is stored and retrieved. Using the Sortable Grid
So far I've built a DataGrid-based controlthe SortableGrid classwhich embeds two keys features. It knows how to page and update the pager bar and how to handle sorting. When you sort by a certain column (only columns marked as sortable in the DataGrid ASP .NET source code), the grid automatically shows a glyph near the column name to denote the direction (ascending or descending), and if you click on an already sorted column, then the direction is reversed. You get all this for free if you use the SortableGrid control instead of the DataGrid control. Figure 9 shows the sortable grid in action. Figure 10 shows the full source code for a page that uses the sortable grid. .gif) Figure 9 Sortable Grid in Action
The sortable grid still needs <Columns> tags, because columns are what characterize a grid and it's a parameter that typically changes every time you use it. The grid also needs SortCommand and PageIndexChanged handlers. If you look carefully at the source code, you'll realize that these handlers simply call the local function, UpdateDataView, which refreshes the content of the grid. All the logic behind these functions has been moved into the body of the SortableGrid control's code.
You can even hide the call to UpdateDataView once you port the storage and retrieval to inside the grid. But this approach requires you to make a choice; you must decide to create a session-based Web grid control rather than a control that's based on XML or database access.
As you may have guessed already, the handler in the body of the control is called first; then it's the turn of the handler that you may have defined in the page. This is yet another great example of the flexibility and power that characterize the DataGrid Web control. Adding a Custom Toolbar
A grid is rendered as an HTML table. It has a caption bar, a list of items, an optional footer, and a pager bar. However, a lot of useful functions, like adding a new item or deselecting the currently selected item, find no place in the overall user interface. What about adding a new toolbar at the bottom of the grid? This toolbar could, for instance, expose frequently needed functionality such as adding a new item to the list or deselecting a previously selected item.
You can wrap the DataGrid with any collection of controls through the <table> construct. Basically, you show the grid as a cell of a table. Figure 11 shows a sample application that makes use of this schema. The row below the grid was created by combining the grid and a child table as the topmost and the bottommost cells of a single-column, two-row parent table. .gif) Figure 11 The Grid as a Table
In Figure 11 you can also see the footer. It is the row placed just above the pager bar that shows all the keywords listed in the grid. I'll have more to say about footers shortly.
The hyperlinks providing Clear Selection, Delete, and New functionalities are ordinary ASP .NET link buttons (see Figure 12). All link buttons are disabled by default with the exception of New. This is no surprise if you consider that you are allowed to clear a selection and delete an item only if a selected item is present. The status of those two link buttons is programmatically updated when an item is selected. The code that does this runs after the SelectedIndexChanged event fires. Remember, you can select rows in Web DataGrids if you have one column of type ButtonColumn for which you have written the code to set the CommandName attribute to the keyword "select."
The code for Clear Selection, Delete, and New is defined in the body of the page. The result of clearing a selection or deleting an item is rather straightforward. When you clear the selection, just reset the value of the SelectedIndex property to -1 and launch other internal routines to refresh other specific parts of the application. void OnClearSelection(Object s, EventArgs e)
{
DataGrid1.SelectedIndex = -1;
UpdateInfoPanel(null);
UpdateBottomBar();
}
It's the same story for the Delete link button. You take the proper measures to delete the particular item the user has selected, depending upon the data source that outfits the application. For example, if your data source is SQL-based, you can use an ordinary DELETE statement (see Figure 13).
Bear in mind that you should refresh the DataGrid's data source after any operation that could affect the state of the physical database. This ensures that possible conflicts, triggers, auto-increment fields, or fields with default values don't change the data you sent. Of course, if you're completely sure that the data you're displaying is perfectly aligned with the records in the data source, that's fine. Adding New Items
It's not easy to devise a general-purpose way to add new items to a DataGrid. This is because the user interface is so dependent on the nature of the data you're dealing with. I can see two possible approaches to this problem.
One makes sense when you're relying heavily on ADO .NET DataSets to feed the grid's data source. In this case, you simply add a blank row to the DataSet and refresh the control. Then you can fill the fields of this record using standard or custom in-place editing. The TableRowCollection class, namely the class behind the DataTable's Rows property, features both Add and AddAt methods. This gives you the maximum flexibility when it comes to inserting new rows. If the DataTable that's located where you're adding data has an in-memory primary key set or any other form of constraint, adding a blank record may cause problems. Handling this kind of runtime issue is completely up to you, but keep in mind that if there are any errors, the new blank row won't be added to the table.
Another possible approach is using a special form. Using validators and custom logic, you make sure that you have all the information needed to successfully update the table. Where do you place this new form? It's a good idea to make it dynamic so that it appears only when needed and stays hidden at all other times. .gif) Figure 14 Hidden Panel Trick
In the sample application shown in Figure 14, I've used a hidden ASP .NET panel (which evaluates to an HTML <DIV> tag). This panel is turned on when the user clicks on the New link button. If you manage it through the <asp:panel> server control, then you can turn it on with the Visible property. Otherwise, if you prefer using HTML and the DOM, you must resort to the following: panelNewRow.Attributes["style"] = "display:";
The <DIV> tag has no direct counterpart in the HtmlControls namespace, so it is rendered through the HtmlGenericControl server control. This means that you must be coding against its style property manually.
At the same time that you show the panel for the new row, you must swap the New button with the OK/Cancel pair. These new buttons let you handle the operation's update and abort functions. To insert a new row is, again, application-specific, but a plain old SQL INSERT command is always a good solution. About the Footer
In a DataGrid, 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 through the ShowFooter attribute. <asp:DataGrid runat="server"
ShowFooter="True"
•••
>
The footer style can be modified via the FooterStyle template property. Through it you can change font, colors, and borders. One thing that you cannot change declarativelyand indeed one thing that you often need to changeis the footer's actual structure. The footer is rendered as yet another row added at the bottom of the table used to render the whole DataGrid. As expected, by default this last row maintains a structure identical to the previous ones. If this suits you, that's fine. Otherwise, if you need a different number of columns for the footer, or columns with different widths, you must intervene when the footer is first created.
Once again, this is not exactly something that you can reasonably package in an off-the-shelf component. The structure of the footer, and the footer itself, is specific to the application. However, if you want a footer that's just one big cell that works as a summary row, you must remove the unneeded cells at runtime. Here's how to accomplish this. void OnItemCreated(Object s,
DataGridItemEventArgs e)
{
if (itemType == ListItemType.Footer)
{
e.Item.Cells.RemoveAt(1);
e.Item.Cells.RemoveAt(1);
e.Item.Cell[0].ColumnSpan = 3;
e.Item.Cell[0].Text = "...";
}
}
The code in this snippet removes the first two cells from the row and sets the value of the ColumnSpan property to three. Of course, I'm assuming here that the table to which the footer belongs has exactly three columns.
To populate the footer's cells with controls, you simply apply the same logic and follow the same approach that you would with any other part of an ASP .NET application. Send your questions and comments for Dino to cutting@microsoft.com. |