This is another instalment of avoiding the use of dynamic controls in ASP.NET. The new ASP.NET ListView control is really rather neat; using this control obviates many of the problems which previously led developers to develop dynamic control-based pages, and can replace reams of nasty control creation loops with a simple and intuitive markup-based alternative.
One problem which is frequently addressed by developers is the display of hierarchical data. A simple solution to this is to use a treeview control. Whilst the built-in ASP.NET treeview control works well for a simple parent-child node structure- it doesn't support more complex scenarios, primarily because because it offers only limited control over the contents of each node.
What I wanted to do was display a drilldown matrix report, with hierarchical categories on the y-axis, and a pre-defined set of categories along the x-axis. I found that this can be a fairly painless task using a ListView.
To illustrate why this might be useful, take the example of a hypothetical multinational software development company; for the sake of argument, let's call this entirely fictional company Macrosoft.
Macrosoft has offices in several countries, each of which is working on a new product which the company initially aimed to release before the end of the financial year. Having roundly missed this deadline, they now aim to get it out in time for the Christmas sales.
Each location has its own team of made up of analysts, developers, designers, and testers, and each team is working on different features of the product.
Their operations director has asked for a report summarising how the product development is progressing at each of the locations. He wants to know if any work is currently outstanding in analysis, design, development, and testing, at each of these locations, and be able to drill down by region.
He also wants the report to be kept as simple as possible: a red mark to indicate outstanding work, a green one to indicate all is complete. Finally, he wants this report to be accessible via a web page. This will enable him to get an instant overview of the overall state of the product, and consequently hassle the regional managers whose departments are lagging behind.
Now, given the above (admittedly somewhat unlikely) scenario, my first though would normally be to do it in SQL Server Reporting Services. This would meet all the above requirements without having to worry about creating the report layout from scratch. But, let's say it has to be done in ASP.NET...
We want the final output to look something like this when fully expanded:
and each of the nodes on the left should initially appear as collapsed.
My implementation made use of a typed dataset, with a recursive relationship, put together as follows:
As you can see in the dataset designer above, each location has an ID and a name. A parent-child relationship has been created between locationID and parentID, which allows all the regions, towns, and office locations to be held within the same table. The table contains 4 boolean flags: anaysisComplete, designComplete, developmentComplete, and testingComplete: these indicate the state of the work at each location.
I also added a couple of fields which are used by the UI to store visual state: isExpanded, and indentationLevel. In a real-world application, I wouldn't mix data and display information in this way- these fields would be declared within, and managed by, the presentation layer. But to keep the example simple, I opted to store them along with the report data.
The ASP.NET markup contains a ListView which looks like this:
1: <asp:ListView ID="lvOffices" runat="server"
4: <table style="width:75%">
7: <td style="width:15%">Analysis</td>
8: <td style="width:15%">Design</td>
9: <td style="width:15%">Development</td>
10: <td style="width:15%">Testing</td>
12: <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
18: <asp:Literal ID="litIndent" runat="server"></asp:Literal>
19: <asp:LinkButton ID="lnkExpandCollapse" runat="server" CausesValidation="false" OnClick="expandCollapse_Click">+</asp:LinkButton>
20: <asp:Label ID="lblLocation" runat="server"></asp:Label>
22: <td><asp:Image ID="imgAnalysisState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
23: <td><asp:Image ID="imgDesignState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
24: <td><asp:Image ID="imgDevelopmentState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
25: <td><asp:Image ID="imgTestingState" runat="server" Visible="false" Height="20px" Width="20px" /></td>
The 4 state images are set dynamically in the ItemDataBound event handler, based on the value of the appropriate boolean flag, and a linkbutton is included to trigger the drilldown.
The rest of the application works like this:
- The data is retrieved from the data store (in this case, a dataset instance populated with test data).
- The top level rows (i.e the rows where parentID is null) are extracted, and bound to the ListView
- Each time the user clicks the drilldown button, the isExpanded state is set for the appropriate row and the ListView control is re-bound
Each time the ListView control is re-bound, the appropriate data rows are extracted from the datatable, first by pulling out those where the parentID is null, then by recursively searching those for child rows where the isExpanded flag is set to true:
1: private void BindData(LocationData dataSet)
3: List<LocationData.LocationRow> dataSourceRows = new List<LocationData.LocationRow>();
5: //gets a collection of top-level rows
6: IEnumerable<LocationData.LocationRow> topLevelRows = from LocationData.LocationRow locationRow
7: in dataSet.Location.Rows
8: where locationRow.IsparentIDNull()
9: select locationRow;
12: List<LocationData.LocationRow> rowsToBind = new List<LocationData.LocationRow>();
14: //recursively builds a collection consisting of all the top level rows
15: //and any children where isExpanded = true in the data row
16: this.BuildDataSetRecursive(topLevelRows, ref rowsToBind, 0);
18: //binds the locations listview
19: this.lvOffices.DataSource = rowsToBind;
23: private void BuildDataSetRecursive(
24: IEnumerable<LocationData.LocationRow> inputRows,
25: ref List<LocationData.LocationRow> outputRows, int indentationLevel)
27: foreach (LocationData.LocationRow inputRow in inputRows)
29: inputRow.indentationLevel = indentationLevel;
31: //add the current row to the output collection
34: //if the row expanded state is set to true, add any children to the output collection.
35: if (inputRow.isExpanded)
37: //using the data relation on the typed dataset to find the children of the current row.
38: DataRow childRows = inputRow.GetChildRows("FK_Location_Location");
40: if (childRows.Length > 0)
42: List<LocationData.LocationRow> locationChildRows = new List<LocationData.LocationRow>(
44: this.BuildDataSetRecursive(locationChildRows, ref outputRows, indentationLevel + 1);
And the methods which toggle the isExpanded flag when the user clicks on the drilldown button complete the picture:
1: protected void expandCollapse_Click(object sender, EventArgs e)
3: LinkButton lbt = (LinkButton)sender;
4: int locationID = int.Parse(lbt.CommandArgument);
6: LocationData ds = (LocationData)Cache["data"];
7: LocationData.LocationRow locationRow = ds.Location.FindBylocationID(locationID);
9: //set the isExpanded state for the row
10: this.SetExpandState(locationRow, !locationRow.isExpanded);
12: //persist the changes
15: //update the label text
16: lbt.Text = (locationRow.isExpanded) ? "-" : "+";
18: //rebind the listview
22: private void SetExpandState(LocationData.LocationRow row, bool state)
24: row.isExpanded = state;
25: if (!state)
27: DataRow childRows = row.GetChildRows("FK_Location_Location");
28: foreach (DataRow childRow in childRows)
30: this.SetExpandState((LocationData.LocationRow)childRow, state);
So, in retrospect, I suppose this turned out to be not so much an example of the functionality of the ListView control (as you can see from the ListView markup above- there's not really much to it) as about traversing recursive relationships in datatables. But a potentially useful bit of code nonetheless...
The full source code for this article can be downloaded using the link at the top.