Introduction
Much of my work in building Web sites involves data reporting. For a recent project, several reports shared the same need for grouping and computing aggregations. To support these kinds of requirements, and assuming a dedicated reporting engine isn't in use, a developer is typically forced to make a choice between more complicated ASP.NET coding, or more complicated SQL coding.
For example, for a given report we could return a distinct list of categories and use rollups or additional aggregation queries all within the same underlying SQL stored procedure. This may allow for simpler ASP.NET code, but requires a degree of complexity in the SQL that I was hoping to avoid.
A much simpler SQL stored procedure would return a flat tabular data set, with the first column representing group categories and other columns potentially requiring aggregation. With this type of result set, one can imagine the kind of repetitive category- and aggregation-tracking required in the ASP.NET application, most likely in a RowDataBound
event of a GridView
. It can be effective, but for multiple pages this approach is again somewhat tedious as each page typically has to be coded individually.
To keep cleaner SQL coding, but simplify the ASP.NET application, I created the GroupingView
ASP.NET control presented in this article. It inherits from CompositeDataBoundControl
and thus may be bound to any ASP.NET 2.0+ data source control or IEnumerable
source. The GroupingView
automatically recognizes groups based on a user-defined data field in the source, and applies display logic based on user-defined templates. Multiple aggregation functions are supported through the use of a supplemental Aggregation
control, which can provide summations at a group level or over the complete data source.
Using the Control
The GroupingView
control is similar to a Repeater
, in that user-defined templates are used to provide the display layout. Unlike the Repeater
, the GroupingView
automatically categorizes the display according to a user-specified field in the data source. Set the GroupingDataField
property of the GroupingView
to the name of the field within the data source by which you wish to group. If GroupingDataField
is left blank, then the data source is treated as a single group of records.
To define the display layout, specify markup in the GroupTemplate
property. GroupTemplate
is the only required template for the GroupingView
control. During databinding, the GroupTemplate
is instantiated once for each group, using the first data item in the group for resolving binding expressions. For example, assuming a data source with the field "Region
", the following markup will render a list of distinct region names:
<cc1:GroupingView id="gv1" runat="server"
GroupingDataField="Region"
>
<GroupTemplate>
<%# Eval("Region") %>
<br />
</GroupTemplate>
</cc1:GroupingView>
There are two ways to display individual items within a group. One is to use a databound control such as a GridView
or BulletedList
in the GroupTemplate
. The GroupingView
exposes the boolean
property AutobindDataSourceChildren
, which if set to true
(the default) will automatically bind any databound control in the GroupTemplate
(those that expose a DataSource
property) to the subset of data items from the source that represent the group. For example, we can expand the above markup to list items in a region through a GridView
with the following addition:
<cc1:GroupingView id="gv1" runat="server"
GroupingDataField="Region"
AutobindDataSourceChildren="true"
>
<GroupTemplate>
<%# Eval("Region") %>
<br />
<asp:GridView id="grid1" runat="server" />
</GroupTemplate>
</cc1:GroupingView>
Note that the child GridView
control that lists individual items is not declaratively bound to the data source. The GroupingView
control automatically performs the binding if AutobindDataSourceChildren
is set to true
. To explicitly specify which controls should be included in or excluded from the automatic binding, set either the AutobindInclusions
or AutobindExceptions
property of the GroupingView
control respectively.
A second way to display individual items within a group is to define an ItemTemplate
for the GroupingView
. The ItemTemplate
defines the layout for individual items. To position the items within the group, it is necessary to include a placeholder control with an ID of itemPlaceholder
in the GroupTemplate
. This can be a bonafide PlaceHolder
control, or other container control. To use a different ID for this control, set the ItemPlaceholderID
property of the GroupingView
(again, this property defaults to itemPlaceholder
).
The following example demonstrates the use of the itemPlaceholder
control and an ItemTemplate
definition. Each region name is displayed as before, followed by a bulleted list of cities within the region:
<cc1:GroupingView id="gv1" runat="server"
GroupingDataField="Region"
AutobindDataSourceChildren="true"
>
<GroupTemplate>
<%# Eval("Region") %>
<ul>
<asp:PlaceHolder id="itemPlaceholder" runat="server" />
</ul>
</GroupTemplate>
<ItemTemplate>
<li><%# Eval("City") %>, <%# Eval("State") %></li>
</ItemTemplate>
</cc1:GroupingView>
Use of the itemPlaceholder
and ItemTemplate
affords the developer complete control over the rendering markup.
In addition to GroupTemplate
and ItemTemplate
, the GroupingView
provides the GroupSeparatorTemplate
and ItemSeparatorTemplate
properties. If used, their literal markup is rendered between groups and items respectively.
Aggregations
To support aggregations, the GroupingView
assembly contains a supplemental Aggregation
control. Like GroupingView
, Aggregation
is a databound control and may be used independently to compute and display summarizations when bound to a data source. The Aggregation
control exposes the Function
property, which defines the aggregation operation, and the DataField
property, which defines the field in the data source to summarize. The Aggregation
control renders its computed value in the same fashion as a Label
control, and exposes the FormatString
property to control the numeric formatting display.
When used in conjunction with a GroupingView
control, the Aggregation
control supports a number of different aggregation functions that may be applied across a group of records, such as Count
, Sum
, Avg
, Min
, Max
, and others. To display an aggregation for a group, place an Aggregation
control in the GroupTemplate
of the GroupingView
control, set its Function
property to the desired operation (one of the AggregationFunction
options), and specify the data field to aggregate in the DataField
property. As long as the GroupingView
's AutobindDataSourceChildren
is set to true
, Aggregation
controls are databound and evaluated using the appropriate subset of the data source.
The following example demonstrates the use of Aggregation
controls to compute sales totals and averages for each region.
<cc1:GroupingView id="gv1" runat="server"
GroupingDataField="Region"
AutobindDataSourceChildren="true"
>
<GroupTemplate>
<%# Eval("Region") %>
<table>
<%-- table header --%>
<tr><th>City</th><th>Sales</th></tr>
<%-- data items --%>
<asp:PlaceHolder id="itemPlaceholder" runat="server" />
<%-- computed totals and averages for the group --%>
<tr>
<td>Total:</td>
<td>
<cc1:Aggregation runat="server" Function="Sum"
DataField="Sales" FormatString="{0:#,##0.00}" />
</td>
</tr>
<tr>
<td>Average:</td>
<td>
<cc1:Aggregation runat="server" Function="Avg"
DataField="Sales" FormatString="{0:#,##0.00}" />
</td>
</tr>
</table>
</GroupTemplate>
<ItemTemplate>
<tr>
<td><%# Eval("City") %></td>
<td><%# Eval("Sales","{0:#,##0.00}") %></td>
</tr>
</ItemTemplate>
</cc1:GroupingView>
Nesting GroupingViews
For additional levels of grouping based on secondary fields in the data source, a GroupingView
control may be nested within the GroupTemplate
of a parent. A child GroupingView
is just another data source control that will be automatically bound to group data provided AutobindDataSourceChildren
is true
.
The following demonstrates the rendering of a three-level hierarchy, with “Region
” providing the primary grouping and “State
” a secondary grouping.
<ul>
<cc1:GroupingView id="gv1" runat="server"
GroupingDataField="Region"
AutobindDataSourceChildren="true"
>
<GroupTemplate>
<li>
<%# Eval("Region") %>
<cc1:GroupingView id="gv2" runat=""server""
GroupingDataField="State"
AutobindDataSourceChildren="true"
>
<GroupTemplate>
<ul>
<li>
<%# Eval("State") %>
<asp:BulletedList id="bul1" runat=""server""
DataTextField="City"
/>
</li>
</ul>
</GroupTemplate>
</cc1:GroupingView>
</li>
</GroupTemplate>
</cc1:GroupingView>
</ul>
Events
The GroupingView
control provides the following events:
Event
| Description
|
GroupCreated
| Occurs when a new group is created in the GroupingView control (i.e. when a GroupTemplate is instantiated). This event is often used to modify the content of a group when it is created.
|
GroupDataBound
| Occurs when a data item is bound to a group in a GroupingView control. This event is often used to modify the content of a group after it is bound to data.
|
ItemCreated
| Occurs when a new item is created within a group in the GroupingView control (i.e. when an ItemTemplate is instantiated). This event is often used to modify the content of a group's item when it is created.
|
ItemDataBound
| Occurs when a data item is bound to an item within a group in a GroupingView control. This event is often used to modify the content of a group's item after it is bound to data.
|
Command
| Occurs when a button is clicked in the GroupingView control. This event is often used to perform a custom task when a button is clicked in the control.
|
For event-handling and additional GroupingView
examples, see the demo and control documentation download links at the top of the article.
About the Code
The GroupingView
control inherits from CompositeDataBoundControl
, which manages the association of data sources and provides common properties such as DataSource
, DataSourceID
, and DataMember
. To support categorization, GroupingView
adds the GroupingDataField
property.
[ToolboxData("<{0}:GroupingView runat="server"></{0}:GroupingView>")]
[Designer(typeof(GroupingViewDesigner))]
public class GroupingView : CompositeDataBoundControl
{
...
private string _groupingDataField = "";
...
public string GroupingDataField
{
get { return _groupingDataField; }
set { _groupingDataField = value; }
}
...
}
The only requirement for inheriting from the CompositeDataBoundControl
class is to provide an implementation of the CreateChildControls
method. The method takes two arguments: an IEnumerable
data source, and a Boolean
flag that indicates the context in which the method is called.
protected override int CreateChildControls
(System.Collections.IEnumerable dataSource, bool dataBinding)
It is important to pay attention to the dataBinding
flag, as the context in which the method is called will distinguish how the control should create its children. A value of true
indicates a databinding context, with the supplied IEnumerable
data source representing the data to bind. A value of false
indicates a postback context. In this case, the control must recreate its children without binding to a data source. The IEnumerable
passed in is not valid data, but rather an array whose length matches the number of previously enumerated data items.
Generally speaking then, these are the steps for addressing a databinding context:
- Enumerate the passed-in
IEnumerable
data source. - For each individual data item, use the data to create appropriate child controls, adding each to the
Controls
collection of the main control. - Return as an
int
the number of data items iterated. This is important to allow the control to recreate its children in postback contexts. The base CompositeDataBoundControl
stores this value in ViewState
.
These are the general steps for addressing a postback context:
- The passed in
IEnumerable
is a dummy array. Inspect its Length
– this value is the same as what was previously returned in the databinding context, and represents the total number of data items previously iterated. - Create child controls
Length
number of times and add them to the Controls
collection of the main control. There is no need to assign data to the children, as each will each use ViewState
to restore its own state.
Addressing a Databinding Context
If a databinding context is presented (the databinding
parameter passed to CreateChildControls
is true
) our control has the responsibility of enumerating the passed in data source to create our display. For the GroupingView
, we first enumerate through the complete source to retrieve the list of distinct GroupingDataField
values which define each group.
protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
int totalItemCount = 0;
int groupCount = 0;
int[] itemsPerGroup = null;
if (dataSource != null)
{
if (!string.IsNullOrEmpty(GroupingDataField))
{
string[] groups = GetArrayOfGroupingValues(dataSource);
itemsPerGroup = new int[groups.Length];
...
}
else
{
...
}
}
...
}
protected string[] GetArrayOfGroupingValues(IEnumerable dataSource)
{
List<string> list = new List<string>();
try
{
foreach (object dataItem in dataSource)
{
string groupingValue
= DataBinder.GetPropertyValue(
dataItem, GroupingDataField
).ToString();
if (!list.Contains(groupingValue))
list.Add(groupingValue);
}
}
catch
{...}
return list.ToArray();
}
We then loop through this array of group values and create subsets of the original data source. The subset is an IEnumerable
representing those items in the data source with a matching GroupingDataField
value. The C# yield return
statement is helpful in establishing the subset.
protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
...
if (dataSource != null)
{
if (!string.IsNullOrEmpty(GroupingDataField))
{
...
foreach (string groupValue in groups)
{
if (groupCount > 0 && GroupSeparatorTemplate != null)
ApplyGroupSeparatorTemplate_BindingScenario();
IEnumerable groupSource
= DataSubsetForGroup(dataSource, groupValue);
int iCount = CreateChildControlsForGroup_BindingScenario(
groupSource, groupCount, totalItemCount
);
totalItemCount += iCount;
itemsPerGroup[groupCount] = iCount;
groupCount++;
}
}
else
{
totalItemCount = CreateChildControlsForGroup_BindingScenario(
dataSource, 0, 0
);
groupCount = 1;
itemsPerGroup = new int[1];
itemsPerGroup[0] = totalItemCount;
}
}
...
}
protected IEnumerable DataSubsetForGroup(
IEnumerable dataSource, string groupingValue)
{
try
{
foreach (object dataItem in dataSource)
{
string itemValue
= DataBinder.GetPropertyValue(
dataItem, GroupingDataField
).ToString();
if (itemValue.ToLower() == groupingValue.ToLower())
yield return dataItem;
}
}
finally { }
}
As GroupingView
is a templated control, we create child controls by instantiating the GroupTemplate
once for each group. To support automatic binding of instantiated child controls to the group data source, we call the BindChildControlsToDataSource
method which is shown at the end of the following code block:
protected int CreateChildControlsForGroup_BindingScenario(
System.Collections.IEnumerable groupDataSource
, int groupIndex, int itemIndex)
{
if (GroupTemplate != null)
return ApplyGroupTemplate_BindingScenario(
groupDataSource, groupIndex, itemIndex);
else
return 0;
}
protected int ApplyGroupTemplate_BindingScenario(
IEnumerable groupDataSource, int groupIndex, int itemIndex)
{
int itemCount = 0;
IEnumerator enumerator = groupDataSource.GetEnumerator();
if (enumerator.MoveNext())
{
object firstDataItem = enumerator.Current;
GroupingViewItem groupItem
= new GroupingViewGroupItem(
firstDataItem, groupIndex, itemIndex, 0
);
GroupTemplate.InstantiateIn(groupItem);
this.Controls.Add(groupItem);
if (AutobindDataSourceChildren)
BindChildControlsToDataSource(
groupItem, groupDataSource
, AutobindInclusionsArray
, AutobindExceptionsArray
);
OnGroupCreated(new GroupingViewEventArgs(groupItem
, groupIndex, itemIndex, 0));
groupItem.DataBind();
...
OnGroupDataBound(new GroupingViewEventArgs(
groupItem, groupIndex, itemIndex, 0));
}
return itemCount;
}
protected void BindChildControlsToDataSource(
Control parent, IEnumerable dataSource
, string[] inclusions, string[] exceptions)
{
foreach (Control c in parent.Controls)
{
PropertyInfo pi = c.GetType().GetProperty(
"DataSource", BindingFlags.Public | BindingFlags.Instance
);
if (pi != null)
{
bool bApply = true;
string id = c.ID;
if (!string.IsNullOrEmpty(id))
{
id = id.ToLower();
if (inclusions != null)
{
if (Array.IndexOf<string>(inclusions, id) < 0)
bApply = false;
}
else if (exceptions != null)
{
if (Array.IndexOf<string>(exceptions, id) >= 0)
bApply = false;
}
}
if (bApply)
pi.SetValue(c, dataSource, null);
}
if (c.HasControls())
BindChildControlsToDataSource(c, dataSource
, inclusions, exceptions);
}
}
If an itemPlaceholder
control is supplied in the GroupTemplate
, and an ItemTemplate
defined, it is applied once for each data item in the group’s data subset in a similar manner. Also note the call to DataBind()
after the group is instantiated. This is important as it allows the evaluation of databinding expressions within the template.
Typically a subclass of CompositeDataBoundControl
will pass as the return value from CreateChildControls
the total number of data items evaluated to assist in future postback contexts. Though the GroupingView
follows this convention, it won't help us to recreate controls on a postback. We don't need the total number of items to recreate our controls, but rather the total number of distinct groups, and for each group, the number of items within that group. So in addition to returning the total number of items for the base class to store, we also explicitly store in ViewState
our own array of integers. The length of this array is the total number of distinct groups, and each integer in the array is the number of subset items within a group.
protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
...
int[] itemsPerGroup = null;
...
ViewState[kViewState_ItemsPerGroup] = itemsPerGroup;
return totalItemCount;
}
Addressing a Postback Context
Given that we previously stored our integer array identifying group and item counts in ViewState
, it is a simple matter to recreate child controls in a postback context (i.e. the databinding parameter passed to CreateChildControls
is false
). We don't have to worry about restoring the state of the child controls – ViewState
allows the controls to manage that responsibility themselves. We just need to create the correct number of controls, in the correct order. Once for each group, we apply the GroupTemplate
with the appropriate number of items. If necessary, we also apply the ItemTemplate
within the group the correct number of times.
protected int CreateChildControls_PostbackScenario()
{
int[] itemsPerGroup = (ViewState[kViewState_ItemsPerGroup] as int[]);
if (itemsPerGroup == null)
return 0;
else
{
int itemCount = 0;
for (int i = 0; i < itemsPerGroup.Length; i++)
{
if (i > 0 && GroupSeparatorTemplate != null)
ApplyGroupSeparatorTemplate_PostbackScenario();
itemCount += ApplyGroupTemplate_PostbackScenario(
itemsPerGroup[i], i, itemCount);
}
return itemCount;
}
}
protected int CreateChildControlsForGroup_PostbackScenario(
int numItems, int groupIndex, int itemIndex)
{
if (GroupTemplate != null)
return ApplyGroupTemplate_PostbackScenario(
numItems, groupIndex, itemIndex);
else
return 0;
}
protected int ApplyGroupTemplate_PostbackScenario(
int numItems, int groupIndex, int itemIndex)
{
GroupingViewItem groupItem = new GroupingViewGroupItem(
null, groupIndex, itemIndex, 0);
GroupTemplate.InstantiateIn(groupItem);
this.Controls.Add(groupItem);
OnGroupCreated(new GroupingViewEventArgs(
groupItem, groupIndex, itemIndex, 0));
if (numItems > 0)
{
...
}
else
return 0;
}
Summary
The GroupingView
is a templated, data-bound control for ASP.NET 2.0 that groups data items according to a field in the source. It provides for a hierarchical display of an otherwise flat data set with the use of its GroupTemplate
property. The developer has control over the display of individual items within a group through the use of an itemPlaceholder
container control and the ItemTemplate
property. Alternatively, the developer may take advantage of the AutobindDataSourceChildren
property and use other databound controls such as a GridView
in the GroupTemplate
to render individual items. A number of aggregation operations are supported through the supplemental Aggregation
control. As a subclass of CompositeDataSourceControl
, the GroupingView
manages the creation of its user interface in both databinding and postback contexts.
History
- 11-Jul-2008: Updated to include
.Groups
, .Items
, and .NestedItems
properties for inspection of created items in a GroupingView
control; also added the EmptyDataTemplate
template property. - 04-Jul-2008: Initial submission to The Code Project