|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionMuch 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 To keep cleaner SQL coding, but simplify the ASP.NET application, I created the Using the ControlThe To define the display layout, specify markup in the <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 <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 A second way to display individual items within a group is to define an The following example demonstrates the use of the <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 In addition to AggregationsTo support aggregations, the When used in conjunction with a The following example demonstrates the use of <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 GroupingViewsFor additional levels of grouping based on secondary fields in the data source, a The following demonstrates the rendering of a three-level hierarchy, with “ <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>
EventsThe
For event-handling and additional About the CodeThe [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 protected override int CreateChildControls
(System.Collections.IEnumerable dataSource, bool dataBinding)
It is important to pay attention to the Generally speaking then, these are the steps for addressing a databinding context:
These are the general steps for addressing a postback context:
Addressing a Databinding ContextIf a databinding context is presented (the protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
// we're in a binding context; create the child controls
// by enumerating over the given datasource; return the
// total number of dataitems enumerated
int totalItemCount = 0;
int groupCount = 0;
// for tracking the number of items per group
int[] itemsPerGroup = null;
// inspect the data source
if (dataSource != null)
{
if (!string.IsNullOrEmpty(GroupingDataField))
{
// start by getting an array of grouping values
string[] groups = GetArrayOfGroupingValues(dataSource);
itemsPerGroup = new int[groups.Length];
...
}
else
{
...
}
}
...
}
protected string[] GetArrayOfGroupingValues(IEnumerable dataSource)
{
// return a string array of distinct values appearing
// in the GroupingDataField for the given dataSource
List<string> list = new List<string>();
try
{
foreach (object dataItem in dataSource)
{
// use the databinder to get the grouping value
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 protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
...
// inspect the data source
if (dataSource != null)
{
if (!string.IsNullOrEmpty(GroupingDataField))
{
...
// then loop through each and create a subset datasource
foreach (string groupValue in groups)
{
// apply a separator?
if (groupCount > 0 && GroupSeparatorTemplate != null)
ApplyGroupSeparatorTemplate_BindingScenario();
// apply the group
IEnumerable groupSource
= DataSubsetForGroup(dataSource, groupValue);
int iCount = CreateChildControlsForGroup_BindingScenario(
groupSource, groupCount, totalItemCount
);
totalItemCount += iCount;
itemsPerGroup[groupCount] = iCount;
groupCount++;
}
}
else
{
// if we don't have a GroupingDataField identified,
// treat the entire listing as one group
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)
{
// use the databinder to get the grouping value
string itemValue
= DataBinder.GetPropertyValue(
dataItem, GroupingDataField
).ToString();
if (itemValue.ToLower() == groupingValue.ToLower())
yield return dataItem;
}
}
finally { }
}
As protected int CreateChildControlsForGroup_BindingScenario(
System.Collections.IEnumerable groupDataSource
, int groupIndex, int itemIndex)
{
// create child controls for the given group datasource by
// applying the GroupTemplate (and within that, ItemTemplates
// are applied);
// return the number of ItemTemplates applied;
if (GroupTemplate != null)
return ApplyGroupTemplate_BindingScenario(
groupDataSource, groupIndex, itemIndex);
else
return 0;
}
protected int ApplyGroupTemplate_BindingScenario(
IEnumerable groupDataSource, int groupIndex, int itemIndex)
{
// using the first record in groupDataSource, apply the GroupTemplate;
// return the total number of ItemTemplates applied across
// the groupDataSource
int itemCount = 0;
// use an if{} rather than a while{} as we'll only need
// the first data item in the group for binding the group
IEnumerator enumerator = groupDataSource.GetEnumerator();
if (enumerator.MoveNext())
{
// instantiate the layout template using the first item
// of the data source
object firstDataItem = enumerator.Current;
GroupingViewItem groupItem
= new GroupingViewGroupItem(
firstDataItem, groupIndex, itemIndex, 0
);
GroupTemplate.InstantiateIn(groupItem);
// add the instantiated groupItem to the controls collection
this.Controls.Add(groupItem);
// if we're databinding and want to autobind other children
// controls, assign all child controls with a DataSource
// property in the group layout to the same groupDataSource
if (AutobindDataSourceChildren)
BindChildControlsToDataSource(
groupItem, groupDataSource
, AutobindInclusionsArray
, AutobindExceptionsArray
);
// the GroupingViewItem is instatiated; before binding,
// fire the GroupCreated event
OnGroupCreated(new GroupingViewEventArgs(groupItem
, groupIndex, itemIndex, 0));
// now evaluate databinding expressions in the group
groupItem.DataBind();
// finally, if the group contains an itemPlacholder, find it
// and populate it for each dataItem in the group
...
// fire the GroupDataBound event
OnGroupDataBound(new GroupingViewEventArgs(
groupItem, groupIndex, itemIndex, 0));
}
return itemCount;
}
protected void BindChildControlsToDataSource(
Control parent, IEnumerable dataSource
, string[] inclusions, string[] exceptions)
{
// loop through all children of the given parent;
// if any exposes a DataSource property, bind it to
// the given datasource;
// if exceptions != null, do not include those control IDs;
// if inclusions != null, only include those control IDs;
foreach (Control c in parent.Controls)
{
// use reflection to get the DataSource property if available
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;
}
}
// set the value to the given datasource
if (bApply)
pi.SetValue(c, dataSource, null);
}
// recursively check for children
if (c.HasControls())
BindChildControlsToDataSource(c, dataSource
, inclusions, exceptions);
}
}
If an Typically a subclass of protected int CreateChildControls_BindingScenario(IEnumerable dataSource)
{
...
// for tracking the number of items per group
int[] itemsPerGroup = null;
// inspect the data source
...
// to support the re-creation of child controls upon postbacks,
// store the itemsPerGroup array in ViewState
ViewState[kViewState_ItemsPerGroup] = itemsPerGroup;
// return the total number of data items iterated
return totalItemCount;
}
Addressing a Postback ContextGiven that we previously stored our integer array identifying group and item counts in protected int CreateChildControls_PostbackScenario()
{
// we're in a postback scenario, in which case the dataSource
// does not contain valid data; we're expected to recreate
// child controls which will then repopulate themselves using
// ViewState; in this case, dataSource is a dummy
// int array of the same length as the total number of
// dataitems initially bound;
// since we're binding based on groups first, we need to
// retrieve the ItemsPerGroup array we previously stored in
// ViewState which gives both the number of groups, and the
// number of items in each group. With that, we'll reapply
// Group and ItemTemplates and child controls will populate
// themselves from ViewState accordingly.
int[] itemsPerGroup = (ViewState[kViewState_ItemsPerGroup] as int[]);
if (itemsPerGroup == null)
return 0;
else
{
// the array length indicates the number of groups; loop through
// each and apply GroupTemplates
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)
{
// create child controls for a group with the given number of items
// (within the context of a Postback scenario)
if (GroupTemplate != null)
return ApplyGroupTemplate_PostbackScenario(
numItems, groupIndex, itemIndex);
else
return 0;
}
protected int ApplyGroupTemplate_PostbackScenario(
int numItems, int groupIndex, int itemIndex)
{
// for a postback scenario, apply the GroupTemplate the given number
// of times to create child controls; individual controls will then
// repopulate themselves through their own viewStates.
// if numItems > 0 then locate the itemPlaceholder within the
// GroupTemplate and apply ItemTemplates within it; return the
// number of items applied
GroupingViewItem groupItem = new GroupingViewGroupItem(
null, groupIndex, itemIndex, 0);
GroupTemplate.InstantiateIn(groupItem);
// add the instantiated groupItem to the controls collection
this.Controls.Add(groupItem);
// fire the GroupCreated event
OnGroupCreated(new GroupingViewEventArgs(
groupItem, groupIndex, itemIndex, 0));
// if we have an itemPlaceholder and numItems > 0, apply ItemTemplates too
if (numItems > 0)
{
...
}
else
return 0;
}
SummaryThe History
| ||||||||||||||||||||||||||||||||