Download The Code
A recent project had a user interface requirement where data could be displayed two ways:. Either in chronological order (! Easy) or grouped by family member (less easy) Furthermore, if the user selected a family-member sort, the name of the Person Was To Be Shown On A Line Before The Data - In A Different Style Than The DataTseelf - And Subtotals of Some of the Columns Were To Be Shown After the Individual's Data.
Yikes Not so easy -!. No supplied ASP.NET control delivers that type of functionality While one solution would be to loop through the data and dynamically create a table, that's hardly elegant or reusable and not much fun to boot.
A Google search for possible solutions turned up an excellent article by Rob van der Veer, A Grouping Repeater Control for ASP.NET Rob's article presented a control that grouped data based on a single column (with no subtotaling) -. Not quite what I was ....................... ..
Why The Repeater?
The Repeater Is The Natural Choice To Extend Because:
It's a databound templated control, which is what we need It exposes events that come in handy in creating our custom functionality It's unburdened by a lot of unnecessary functionality other templated databound controls like the DataGrid and DataList have -.. We do not need paging And our Sorting Options are constrained.
WE CAN AVOID The Performance Penalties of these Rich Controls by Extending a Control That Provides Only The Base FunctionAlicity and Interface We Require.
The repeater is a lightweight control that lends itself perfectly to the problem at hand, and has a lot of built-in functionality that we get by extending the control - we do not have to re-create what is already working That's the beauty. Of extending an Object! The demo data
I have included an example XML file ( "STRData.xml") that will serve as our data source for this article. We need to sort the data source in this example, so I read it into a DataSet and then return DataSet.Tables ( . 0) to the control A DataTable is used as the base data source because it can be sorted by one or more columns -. a requirement for our control, as it needs a pre-sorted data source Your data will likely come from a database .
Feel Free to Queue Up "Appetite for Destruction," While Working Through this demo.
The Control in Action
THIS Section Describes How The Control Can Be Used on an ASP.NET Page. We'll Look Under The Next See How It Works in The Next Sections.
First, We declare the control at the top of our code-behind class:
Protected Withevents Strpt as Utils.WebControls.groupingRepeater
In The Page_Load Event, We Call a Private Method (Populatesubtotalrepeater) That Configures and Populates The Control:
Private Sub PopulateSubtotalRepeater () Dim dt As DataTable = Me.GetData 'retrieve the data sourceDim dv As DataViewIf Not dt.Rows.Count = 0 Then dv = dt.DefaultView Dim blnIsGroupingDisabled As Boolean Dim strColToSort, strSecondarySort As String If Me.IsSortByPerson Then strColToSort = "TAG_NUMBER" strSecondarySort = ", SERVICE_DATE, CLAIM_ID" blnIsGroupingDisabled = False Else 'do not group or sort by person strColToSort = "SERVICE_DATE" strSecondarySort = ", INITIALS" blnIsGroupingDisabled = True End If dv.Sort = strColToSort & "ASC "& strSecondarySort With Me.strpt .DataSource = dv .ColumnToGroup = strColToSort .IsGroupingDisabled = blnIsGroupingDisabled 'specify columns .AddSubTotalColumn (" CLAIMED_AMOUNT ", GetType (Double)) .AddSubTotalColumn (" ELIGIBLE_AMOUNT ", GetType (Double)) .AddSubTotalColumn (" Deductible_amount ", gettype (double)) .addsubtotalcolumn (" paid_amount ", gettype (double) .da taBind () End WithElse 'empty datatable: do not bother with binding or calculations Me.strpt.DataSource = NothingEnd IfEnd SubThis code looks a lot like what we would use to bind data to a regular Repeater After sorting the source DataTable (via. A DataView, WE Specify The DataView As The Datasource of The Control and Call The DataBind () Method Instol, WE Configure A Couple of Custom Members of Our Control.
We need to specify which column will be used to "group" our results, and do so by setting the public ColumnToGroup property Only one such column can be specified -. For our purposes, it's the person's initials.Our control allows the grouping / subtotaling via behavior to be "turned off" a public property called IsGroupingDisabled We set this based on user input -.. whether we are sorting by date (disable grouping) or person (enable grouping) Our control also requires us to specify which columns we wish . to display group subtotals and a grand total for We use the public method AddSubTotalColumn to do so This interface is analogous to how we would add a column to a DataTable (for good reason, as we'll soon see) That's it.. - Two New Properties To Set and One Method to Call.
The user interface code is also quite similar to what we'd see in a Repeater, with an itemtemplate and alternatingitemtempalte specified (see the default.aspx page code) Three new templates are presented:. Grouptemplate, grouptemplateheader, and grouptemplatetotal.
These custom templates are responsible for displaying the subtotaled amounts, the group "name" row that precedes the grouped data (in our case, the person's name), and the total of all subgroups displayed, respectively. Notice that the grouptemplate and grouptemplatetotal are calling On The Same Column Specified in Our AddsubcolumnTotal Method Calls in The Code-Behind page. All of the data items (IE Column Names) Bound in The Ui Code Come Straight from the DataSource.
Building the control
The implementation details of the control have been hidden from the caller, as they should be, subsumed into three new templates, two public properties and one public method call. Simple! The user manual for this control would be very short. So what's going on under the hood? This control relies on a private DataTable (_dtColumnTotals) to keep track of both the subtotals and totals of the columns specified by the consumer. Each call to AddSubTotalColumn adds a new column to this underlying DataTable.
When the DataBind method is called, the process of looping through the source data and binding to the control begins. This fires the ItemCreated event, whose handler (GroupingRepeater_ItemCreated) is specified in the control's via constructor the AddHandler statement. This insures that each time a DataItem is Bound to Our Control We can Perform Whatver Logic and Processing Are Required First.
Before any items are bound, we initialize the DataTable by adding two rows - one for subtotals, one for totals -.. And initializing all column values to zero This is done by the InitialiseColumnTotals method This method also sets a private boolean property, _dtInitialised, which informs us in the PreRender event whether in fact any data were bound to the control - that is, whether we have two rows of valid data to work with in the final rendering stage of the control's life This is really just a safety check to. Prevent The Code from Bombing If No Data Are Bound To The Control.
Next, after determining whether we are supposed to be grouping data based on a check of the Public boolean property IsGroupingDisabled, we check whether we are on the same group or whether a new group needs to be created via a call to the private ItemComparer property of Our Control.The ItemComparer Class
This simple class, which implements the System.Collections.IComparer interface, is the key to deciding whether a new group needs to be created on each ItemCreated event (thanks to Rob van der Veer for this insight). The interface requires only one member to BE IMPLEMENTED - COMPARE, A Public Function That Returns AN Integer (0 for equality, -1 for inequality).
Our ItemComparer class implements this interface and exposes a single public property, ColumnToCompare, which is set when our control's ColumnToGroup property is set (we also instantiate an instance of ItemComparer when this property is set - the constructor of the class requires ColumnToCompare be passed). Compare Takes Two Objects As Arguments - We Pass The Current DataRowView and The Previous DataRowView (E.Item.DataItem, from The RepeaterItemgs, E, Passed to The Itemcreated Event Handler).
ItemComparer.Compare takes the previous e.Item.DataItem object and the current e.Item.DataItem object as arguments. We know which column from these DataRowView objects we are going to compare, because it was set in the constructor. Now all we need To do as grab the column values for each, and cast them to strings:
Item1 = ctype (v1.row.item (me.columntocompare), string) item2 = ctype (v2.row.Item (me.columntocompare), string)
And Call The Compareto Property of Item1:
Return Item1.Compareto (item2)
So, Returning to the Control's Itemcreated Handler, IF The Call: _Comparer.comPare (LastValue, E.Item.DataItem) <> 0
then we know that we need to start a new group. But first, what if we do not need to start a new group, as will be the case on the first item bound and subsequent items where the value of the grouping column hasn ' T rhanged?
Totaling the columns
On each iteration of the ItemCreated event, we have to make sure we keep track of the totals (and possibly subtotals, if grouping is enabled) of the data source's columns specified through calls to AddSubTotalColumn via This is done the private AddColumnTotals method, which loops THROUGH All of the columns in The DataTable, Grabs Their Current Item Value, And Increme Each Column's Total Via A Call To IncrementColumntotal.
This method is responsible for incrementing the value of a single column, and depending on whether grouping is enabled increments the subtotal and total row appropriately If grouping is enabled, we call this method twice for each column;. If not, we only call it to Track The Total Value. Thus, AS WE LOOP THROUGH Our Datasource, Our Underlying DataTable is Keeping Track of The Column Values for Which We wish To Display Totals (AND POSSIBLY SUBTOTALS).
Back to itemcreated
COLUMN TOTALING HAPPENS ON EVERY CALL TO ITEMCREATED. But Now We need to figureing value change.
IF out to itemcomparer.comPare Indicates That Our Grouping Value Has Changed, WE NEED TO Perform A Series of Tasks in Response:
Create a new Heading row (template) for the previous group. Create a new Subtotal row (template) for the previous group. Provide these new template items with data. Add the new items to the control and DataBind to them. Reset the subtotal values For the next group. add the next group's ited for this runk.here's the code:
If (Me._globalCount> 1 And _comparer.Compare (lastvalue, e.Item.DataItem) <> 0) Then Dim item As New GroupSubTotal Dim itemHeading As New GroupHeader Me._groupTemplate.InstantiateIn (item) Me._groupTemplateHeader.InstantiateIn (itemHeading ) 'the subtotals are bound to the FIRST row of the Private datatable that keeps continuous track of the running totals: item.DataItem = Me.dtColumnTotals.Rows (0)' the header template is bound to the previous data item: itemHeading.DataItem = lastvalue 'check that we are not trying to add the header to a bad index: Dim intCurrentIndex As Integer = Me.Controls.Count If Me._lastHeaderIndex <= intCurrentIndex Then Me.Controls.AddAt (Me._lastHeaderIndex, itemHeading) Else Me.Controls.AddAt (intCurrentIndex, itemHeading) End If 'add the subtotal template to the bottom of the control: Me.Controls.AddAt (intCurrentIndex 1, item)' bind to both templates: item.DataBind () itemHeading.DataBind () 'For t he next / last rendered headers (PreRender event handler), we need to know where to put the last header control in the controls collection Me._lastHeaderIndex = intCurrentIndex 2 'finally: reset the column totals and add the new first row: Me. ResetAllColumnSubTotals () Me.AddColumnTotals (CType (e.Item.DataItem, DataRowView), False) Else 'grouping has not changed: add to subtotals Me.AddColumnTotals (CType (e.Item.DataItem, DataRowView), False) End If
. These steps are all accomplished in the code fragment We create the two new templates, of types GroupSubTotal and GroupHeader, and associate these with our custom Public properties, GroupTemplate and GroupTemplateHeader.Next we assign these templates data: in the case of the Header template , the data is the previous DataItem we processed (since the group has changed on this iteration). for the Subtotal template, it is the first row of our private DataTable, which has been tracking the subtotal values for us through all the previous iterations of The group's items.
We need to tell the control where to render these new template items For the subtotal item, that's easy:.. It goes at the end of the Controls collection For the header, things are a little more tricky: it has to preceed the items already bound to the control for this group (that is, all the individual data items) The private field _lastHeaderIndex helps us in this regard:. the first time we add a header, we add it at the zero position Then we set the _lastHeaderIndex value. TO (Current Number Items 2). The next time we add a header, IT Will Be at this position, Which is one below where the subtotal template is create.
To finish off the group, we call the DataBind () methods for each of our template items, reset the subtotals row of the DataTable (the row at index 0, through a call to ResetAllColumnSubTotals) and add the current DataItem's values to the DataTable.
The prerender evenet
The Last Concern We Have Is The Control's Prerender Event. This Method Takes Care of Two Important Final Chores:
Render The Last Group's Header and Subtotal Templates, if Grouping is enabled. Render the total template.
This is the last event fired before the control is rendered, so it's the appropriate place for these final steps Step 1's logic is essentially the same as the logic described for the ItemCreated event -. We create, provide a data source for, and DataBind the last Header and Subtotal templates if grouping is enabled.The last step is to create the GroupTotal template item, which holds the totals for all the DataItem's bound regardless of group (we assume that if a consumer is using this control, they at least want totals for specified rows; otherwise why not just use a Repeater) The process is much the same as creating a SubTotal template, except now we create a GroupTotal template instead and set it's data source as the second row of the DataTable - where we '?. ve been storing the running total of all item values. We add this last control to the end of the Controls collection, call DataBind, and finally dispose of the DataTable as it is no longer needed and our object is about to expire. Mission Accomplished!
Conclusion
I think this is a great example of the power and flexibility available to a developer through extending an existing control With very little code, we have modified the Repeater control to group, subtotal, and total columns of our choosing -. Behavior not supported by the base control. Furthermore, we now have a control we can reuse to solve this problem next time it arises. Another tool in the developer toolkit! Again, thanks to Rob van der Veer for the initial inspiration for this solution. I hope it helps you Meet a Tough Requirement As Well.