Welcome, Guest
Open an Account   Sign In 
Shopping Cart
Articles
Use the navigation arrows to browse additional feature articles. Click the Printable View button to open the contents of the current article in a separate browser window.
Return to Articles
Printable View
Displaying Custom Aggregates with C1WebGrid

Displaying Custom Aggregates with C1WebGrid

Article ID: 1816
Applies To: WebGrid for ASP.NET
Author: John Juback
Published On: 6/26/2007

Overview

The C1WebGrid component for ASP.NET provides a number of options for displaying hierarchical data sets using AJAX callbacks, including Outlook-style grouping and built-in aggregate functions for automatic subtotals, min/max values, averages, and various statistical calculations. An alternative technique for grouping data is to render detail rows using custom aggregates. This method is particularly effective when the detail rows reside in another data source or lend themselves to a "free form" presentation that does not follow the rigid row/column structure of a grid.

This article uses XML data from the National Weather Service to illustrate the use of custom aggregates to implement an AJAX-enabled master-detail layout using a single C1WebGrid control. To display the detail rows, the sample application dynamically instantiates a standard Xml control and applies an XSL transform.

C1WebGrid Component with Custom Aggregates

To see the above application in action, click the following link:

    http://helpcentral.componentone.com/c1webgridcustomaggregates

To download the source code for the application (in C#), click the following link:

    http://helpcentral.componentone.com/c1kb/upload/c1webgridcustomaggregates.zip

XML Data Formats

First, let's take a look at the format of the XML data files used in the sample application. The National Weather Service provides a list of all weather observation stations in XML format at the following URL:

    http://weather.gov/data/current_obs/index.xml

The following listing shows the structure of this index file using the two expanded data rows from the preceding illustration. For clarity, tags that are not used by the sample application are omitted:

<?xml version="1.0" encoding="UTF-8"?>
<wx_station_index>
<station>
<station_id>KBGR</station_id>
<state>ME</state>
<station_name>Bangor International Airport</station_name>
<xml_url>http://weather.gov/data/current_obs/KBGR.xml</xml_url>
</station>
<station>
<station_id>KNHZ</station_id>
<state>ME</state>
<station_name>Brunswick, Naval Air Station</station_name>
<xml_url>http://weather.gov/data/current_obs/KNHZ.xml</xml_url>
</station>
</wx_station_index>

The detail for each station is specified by the inner text of the xml_url tag. The following listing shows a snapshot of the XML file for the first expanded row, again omitting tags not used by the sample application:

<?xml version="1.0" encoding="ISO-8859-1"?> 
<current_observation version="1.0">
<location>Bangor International Airport, ME</location>
<station_id>KBGR</station_id>
<observation_time>Last Updated on Jun 25, 12:53 pm EDT</observation_time>
<weather>Mostly Cloudy</weather>
<temperature_string>75 F (24 C)</temperature_string>
<relative_humidity>43</relative_humidity>
<wind_string>From the West at 8 MPH</wind_string>
<pressure_string>30.11&quot; (1019.4 mb)</pressure_string>
<dewpoint_string>51 F (11 C)</dewpoint_string>
<visibility_mi>10.00</visibility_mi>
<icon_url_base>http://weather.gov/weather/images/fcicons/</icon_url_base>
<icon_url_name>skc.jpg</icon_url_name>
<two_day_history_url>http://www.weather.gov/data/obhistory/KBGR.html</two_day_history_url>
</current_observation>

To view the full version of this file (with current data), visit the following link:

    http://weather.gov/data/current_obs/KBGR.xml

Designing the Form

The sample application contains a single form with two controls. Since there are hundreds of weather observation stations, a DropDownList control is used to filter the displayed records by state. A C1WebGrid control displays the filtered results as a set of expandable rows. The grid will not actually fetch the weather detail for a particular station until the user clicks the plus sign icon for the desired row.

The DropDownList control has the following property settings:

DataTextFieldname
DataValueFieldid
AutoPostBackTrue

The data source for this control is loaded from an XML file at run time (states.xml, in the App_Data folder). The name field contains the full state name displayed to the user; the id field contains the postal abbreviation used to filter the master list of weather observation stations.

The C1WebGrid control has the following property settings:

AutoGenerateColumnsFalse
CallbackOptionsExpanding
DataKeyFieldxml_url
GridLinesNone
GroupIndent15px
HeaderStyle.CssClassgroup
HeaderStyle.HorizontalAlignLeft
ShowHeaderFalse

Since AutoGenerateColumns is False, we must define the grid columns manually, either at design time or in code. For this sample, three columns were created at design time using the C1WebGrid Property Builder (accessible from the Smart Tag menu).

C1WebGrid Property Builder

The data fields used for the columns (from index.xml) are xml_url, station_name, and station_id. The first column does not display any data, but provides an expandable node for displaying detail data. It has the following property values:

DataFieldxml_url
GroupInfo.HeaderText(space)
GroupInfo.OutlineModeStartCollapsed
GroupInfo.PositionHeader
HeaderStyle.CssClassheader

The GroupInfo object, accessible from the Grouping tab in the Property Builder, is used to specify the outlining behavior. Note that the HeaderText property must be set to a space character (not an empty string) in order to prevent the URL from being displayed in the grouped row.

C1WebGrid Property Builder (Grouping properties)

Similarly, the HeaderStyle object is accessible from the Format tab in the Property Builder. Expand the xml_url node in the Columns tree on the right side of the dialog, then select the Header node to reveal the properties of the column's HeaderStyle object. The CssClass named "header" is defined in an inline stylesheet within the .aspx page.

C1WebGrid Property Builder (Format properties)

The second column, station_name, has the following property values:

AggregateCustom
DataFieldstation_name
DataFormatString<b>{0}</b>
HeaderTextStation Name
HeaderStyle.CssClassheader
HeaderStyle.Font.BoldTrue

The third column, station_id, has the following property values:

AggregateCustom
DataFieldstation_id
HeaderTextStation ID
HeaderStyle.CssClassheader
HeaderStyle.Font.BoldTrue

Note that all columns except the first one have their Aggregate property set to Custom. At run time, the grid will fire the GroupAggregate event whenever it needs to render the text of a column in a grouped row. The details are discussed in the next section.

Writing the Code

Binding to XML Data

The Page_Load event handler performs data binding for the DropDownList and C1WebGrid controls. In each case, it creates a new DataSet and calls the ReadXml method to load data from an XML file. It then creates a DataView object from a particular DataTable and sets the appropriate RowFilter/Sort criteria. Finally, it assigns the DataView object to the DataSource property and calls the control's DataBind method.

protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Populate DropDownList with state names
DataSet dsStates = new DataSet();
dsStates.ReadXml(Page.MapPath("~/App_Data/states.xml"));
DataView dvStates = new DataView(dsStates.Tables["state"]);
dvStates.Sort = "name";
DropDownList1.DataSource = dvStates;
DropDownList1.DataBind();
}
 
if (!IsCallback)
{
// Populate C1WebGrid with observation stations
DataSet dsWeather = new DataSet();
dsWeather.ReadXml("http://weather.gov/data/current_obs/index.xml");
DataView dvWeather = new DataView(dsWeather.Tables["station"]);
dvWeather.RowFilter = String.Format("state = '{0}'", DropDownList1.SelectedValue);
dvWeather.Sort = "station_name";
C1WebGrid1.ShowHeader = dvWeather.Count > 0;
C1WebGrid1.DataSource = dvWeather;
C1WebGrid1.DataBind();
}
}

Note the subtle difference in the two if-expressions. The DropDownList control is populated if the operation is not a postback, while the C1WebGrid control is populated if the operation is not a callback.

Handling the GroupAggregate Event

The GroupAggregate event fires for all columns except the first one. It saves the starting index position for future reference, then copies text from the appropriate cell into the grouped row using the C1GroupTextEventArgs parameter. This event fires before the group header is actually created.

protected void C1WebGrid1_GroupAggregate(object sender, C1.Web.C1WebGrid.C1GroupTextEventArgs e)
{
// Save the starting index position for subsequent ItemCreated events
_itemIndex = e.StartItemIndex;
 
// Copy this column's cell text into the grouped row
C1BoundColumn col = e.Col as C1BoundColumn;
int colIndex = C1WebGrid1.Columns.IndexOf(e.Col);
e.Text = C1WebGrid1.Items[_itemIndex].Cells[colIndex].Text;
}

Handling the ItemCreated Event

The ItemCreated event fires whenever a grid element such as a data row, column header, or group header is created. In this example, we are only interested in group headers (type C1ListItemType.GroupHeader) during AJAX callback operations. Since the grid's CallbackOptions property is set to Expanding, the IsCallback property of the page will return true if this event fires in response to the user clicking a plus sign in a grouped row.

protected void C1WebGrid1_ItemCreated(object sender, C1.Web.C1WebGrid.C1ItemEventArgs e)
{
// For each group header row, check to see if the item in question
// is the row being expanded, then create the Xml detail control
if (e.Item.ItemType == C1ListItemType.GroupHeader)
{
if (this.IsCallback && e.Item.ItemIndex == GetExpandingIndex())
{
CreateXml();
}
}
}

It is important to understand that the ItemCreated event will fire once for each grouped row in the following scenarios:

  • During postback, when the user selects a state for filtering. In this case, IsCallback is false and the event handler takes no action.
  • During callback, when the user first expands an individual row. In this case, IsCallback is true.

The question is: How do you determine which firing of the ItemCreated event corresponds to the row that the user just expanded? The answer, which involves parsing the callback parameters passed to the page, is encapsulated in the GetExpandingIndex function:

private int GetExpandingIndex()
{
// Return the index of the row being exapnded, -1 otherwise
if (_expandIndex == -1)
{
// For row expansions, the format of "args" is 322,Grp|GH2-1|...
string args = Page.Request.Params["__CALLBACKPARAM"];
string[] cb = args.Split('|');
 
// Verify that this is a row expansion callback
if (cb[0].EndsWith("Grp") && cb[1].StartsWith("GH"))
{
// The hyphen separated numbers following "GH" denote row/column indices
string[] val = cb[1].Substring(2).Split('-');
 
if (Convert.ToInt32(val[1]) == 1)
_expandIndex = Convert.ToInt32(val[0]);
}
}
 
return _expandIndex;
}

If the callback parameters contain a substring of the form:

Grp|GHx-y|

where x is the item index and y is the one-based node level, then GetExpandingIndex returns x; otherwise, it returns -1. The ItemCreated event handler compares this result with the ItemIndex specified in the C1ItemEventArgs parameter. If they match, then it is time to create child controls for the newly expanded row.

Instantiating Child Controls

The CreateXml method is called once for each row that the user expands. If the user collapses and then re-expands the same row, CreateXml is not called again, since the detail for the child row has already been loaded.

The first step is to derive the detail row (type C1GridItem) from the item index saved in the previous call to the GroupAggregate event handler. Then, all of the cells in this row are cleared. Next, a blank TableCell is added to the detail row to occupy the space taken up by the plus/minus column.

The URL for the detail data is derived from the grid's DataKeys collection and the saved item index, then loaded into a new XmlDocument. The content of this document is assigned to a new Xml server control, and an XSL transform is then applied.

Finally, the Xml server control is added to a new TableCell, which is in turn added to the detail row. The column span of this cell is set to the number of grid columns, which causes the grid to adjust its width in accordance with the longest detail control.

The complete version of the CreateXml method is as follows:

private void CreateXml()
{
// Get the row that will contain the Xml detail control and clear all of its cells
C1GridItem item = C1WebGrid1.Items[_itemIndex];
item.Cells.Clear();
 
// Add a blank cell to occupy the plus/minus column
TableCell blank = new TableCell();
item.Cells.Add(blank);
 
// Create an XmlDocument and load it from a URL in the DataKeys collection
XmlDocument doc = new XmlDocument();
 
try
{
string url = C1WebGrid1.DataKeys[_itemIndex].ToString();
doc.Load(url);
}
catch
{
doc.Load(Page.MapPath("~/App_Data/error.xml")); // in case of failure
}
 
// Create a new Xml control, load the document, and apply the transform
Xml detail = new Xml();
detail.DocumentContent = doc.OuterXml;
detail.TransformSource = Page.MapPath("~/App_Data/weather.xsl");
 
// Add the Xml control to the grid row
TableCell cell = new TableCell();
cell.Controls.Add(detail);
item.Cells.Add(cell);
 
// Ensure that the Xml control spans the remaining grid columns
cell.ColumnSpan = C1WebGrid1.Columns.Count;
}

Note that if a remote XML document fails to load, the exception is caught and a local document containing an error message is substituted.

Transforming the XML Detail

The file weather.xsl in the App_Data folder specifies the XSL transform used to render detail data. Although an in-depth discussion of XSL is beyond the scope of this article, this example illustrates how you can map XML tags to HTML constructs such as tables, images, lists, and hyperlinks.

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/current_observation">
<div class="detail">
<table width="100%">
<tr>
<td align="center" valign="top" width="72">
<img border="0">
<xsl:attribute name="src">
<xsl:value-of select="concat(//icon_url_base, //icon_url_name)"/>
</xsl:attribute>
<xsl:attribute name="alt">
<xsl:value-of select="//weather"/>
</xsl:attribute>
</img>
<br/>
<xsl:value-of select="//weather"/>
</td>
<td>
<ul>
<li>
Temperature: <xsl:value-of select="//temperature_string"/>
</li>
<li>
Humidity: <xsl:value-of select="concat(//relative_humidity, '%')"/>
</li>
<li>
Wind: <xsl:value-of select="//wind_string"/>
</li>
<li>
Visibility: <xsl:value-of select="concat(//visibility_mi, ' mi')"/>
</li>
<li>
Dew Point: <xsl:value-of select="//dewpoint_string"/>
</li>
<li>
Pressure: <xsl:value-of select="//pressure_string"/>
</li>
</ul>
</td>
</tr>
<tr>
<td>
<a target="_blank">
<xsl:attribute name="href">
<xsl:value-of select="//two_day_history_url"/>
</xsl:attribute>
2-day history
</a>
</td>
<td colspan="2" align="right">
<i>
<xsl:value-of select="//observation_time"/>
</i>
</td>
</tr>
</table>
</div>
</xsl:template>
<xsl:template match="/unavailable">
<div class="detail">
<i>
<xsl:value-of select="//message"/>
</i>
</div>
</xsl:template>
</xsl:stylesheet>

Conclusion

Custom aggregates are a little-known yet powerful feature of C1WebGrid, especially when used in conjunction with the built-in AJAX support for expanding child rows. Although this sample was written using conventional ASP.NET 2.0, the techniques outlined in this article will also work with the ASP.NET AJAX extensions.

Using an Xml control as a rendering agent for detail rows is just one alternative. You can use another C1WebGrid control to implement master-detail relationships, for example. In fact, you can substitute any server control(s) that can be instantiated programmatically.

Site Map Privacy Terms of Use
©1987-2007 ComponentOne LLC  All Rights Reserved.