Using the HierarGrid - Part I | |
[
Back To Top ]
1 What is the HierarGridThe ASP.NET DataGrid is a popular control for displaying tabular data, with editing, paging and sorting capabilities. However, this is only suitable for displaying single DataTables - there is no support for master-detail views using parent-child relations. I have created a custom control called HierarGrid (that derives from the standard ASP.NET DataGrid) and a custom DataGridColumn called HierarColumn. The HierarGrid takes a DataSet containing relations between tables as its DataSource. While iterating over the parent table, it checks the related tables for child rows, and if one is found it dynamically loads a template for the child row(s). The template is rendered invisibly into the custom HierarColumn and when the user clicks the plus icon, the template's content is copied via JavaScript into a newly created TableRow. Sample illustration (Fig. 1): 2 Where can I download the HierarGridI have published the HierarGrid, including the source code, on my homepage http://www.denisbauer.com/ASPNETControls.aspx Please also note the demo that I created, which can be downloaded from the same location. The following explanations are mainly based on, and refer to this demo. The HierarGrid can be used and distributed at no charge in non-commercial and commercial projects. However I am happy to hear about any feedback, suggestions, improvements, criticisms, etc. 3 How to use the HierarGrid3.1 Loading the data into a DataSetThe .NET Framework offers multiple ways to load data into a DataSet. One possible way is to use a SQLCommand to run a SQL query against a table in MS SQL Server. In my example, I used the "Pubs" database, a sample Database of Microsoft SQL Server. I created three SELECT statements that returned a list of publications, the associated authors and the sales in separate tables. The resulting DataSet can be written to a XML file using the method ds.WriteXml. The XML file is contained in the sample application as Testdata.XML, which can be downloaded from the link above. To load the sample data yourself add two Views and a stored procedure into the "Pubs" database with the following three SQL statements: CREATE VIEW dbo.VwAuthors AS SELECT dbo.authors.*, dbo.titleauthor.title_id FROM dbo.titleauthor INNER JOIN dbo.authors ON dbo.titleauthor.au_id = dbo.authors.au_id ORDER BY dbo.titleauthor.title_id CREATE VIEW dbo.VwSales AS SELECT dbo.sales.title_id, dbo.stores.stor_name, dbo.stores.stor_address, dbo.stores.city, dbo.stores.state, SUM(dbo.sales.qty) AS qty, dbo.stores.zip FROM dbo.sales INNER JOIN dbo.stores ON dbo.sales.stor_id = dbo.stores.stor_id GROUP BY dbo.sales.title_id, dbo.stores.stor_name, dbo.stores.stor_address, dbo.stores.city, dbo.stores.state, dbo.stores.zip ORDER BY dbo.sales.title_id CREATE PROCEDURE dbo.TestData AS SELECT title_id as Title_ID, title as Title, type as Type, price as Price, notes as Notes, CAST(pubdate as varchar) as Publication FROM Titles SELECT * FROM VwAuthors WHERE exists (Select * from Titles where au_id = VwAuthors.au_id) ORDER BY title_id SELECT * FROM VwSales WHERE exists (Select * from Titles where title_id = VwSales.title_id) To load the data into a DataSet create a new WebApplication and add the following code into the Page_Load event: [VB] Dim sqlConnection As SqlConnection = New SqlConnection() Dim sqlCommand As SqlCommand = New SqlCommand() sqlConnection.ConnectionString = "data source=(local);initial catalog=pubs;integrated security=SSPI;" sqlCommand.CommandText = "[TestData]" sqlCommand.CommandType = System.Data.CommandType.StoredProcedure sqlCommand.Connection = sqlConnection Dim ds As DataSet = New DataSet() sqlConnection.Open() Dim adap As SqlDataAdapter = New SqlDataAdapter(sqlCommand) adap.Fill(ds) sqlConnection.Close() ds.Tables(0).TableName = "Titles" ds.Tables(1).TableName = "Authors" ds.Tables(2).TableName = "Sales" [C#] SqlConnection sqlConnection = new SqlConnection(); SqlCommand sqlCommand = new SqlCommand(); sqlConnection.ConnectionString = "data source=(local);initial catalog=pubs;integrated security=SSPI;"; sqlCommand.CommandText = "[TestData]"; sqlCommand.CommandType = System.Data.CommandType.StoredProcedure; sqlCommand.Connection = sqlConnection; DataSet ds = new DataSet(); sqlConnection.Open(); SqlDataAdapter adap = new SqlDataAdapter(sqlCommand); adap.Fill(ds); sqlConnection.Close(); ds.Tables[0].TableName = "Titles"; ds.Tables[1].TableName = "Authors"; ds.Tables[2].TableName = "Sales"; 3.2 Binding the Data to a standard DataGridAlternatively to the steps mentioned in the section above, you can download the sample project and use the Testdata.XML file. This file can be loaded into a DataSet and bound to a standard DataGrid with only a few lines of code. If you had not already done in step 3.1 create a new WebApplication and drop a new DataGrid from the toolbar onto the webform and call it DG1. Otherwise skip the next few lines and continue with binding your retrieved DataSet to the DataGrid. The following code shall be placed in the Page_Load event. The first step is to load the data from the sample file into a DataSet or to use the DataSet generated with the code above: [VB] Dim ds as DataSet = new DataSet() ds.ReadXml(AppDomain.CurrentDomain.BaseDirectory + "\\Testdata.xml") [C#] DataSet ds = new DataSet(); ds.ReadXml(AppDomain.CurrentDomain.BaseDirectory + @"\Testdata.xml"); Then the DataSet can be bound to a standard DataGrid that has been dropped on a webform: [VB] DG1.DataSource = ds DG1.DataMember = "Titles" DG1.DataBind() [C#] DG1.DataSource = ds; DG1.DataMember = "Titles"; DG1.DataBind(); The procedure so far is the normal way to bind relational data from a DataSet to an ASP.NET DataGrid and can be looked up in the .NET Framework documentation and Quickstart tutorials. Following are the changes described the HierarGrid requires to display parent-child relations. 3.3 Switching to the HierarGridOne goal when developing the HierarGrid was to enable developers to switch seamlessly between the standard DataGrid view without parent-child relations and the detailed view with the HierarGrid. First, reference the HierarGrid.dll in your VS.NET solution by right-clicking on References, choosing Add and selecting the DLL. Second, add the following declaration to the top of the ASPX file: <%@ Register TagPrefix="DBWC" Namespace="DBauer.Web.UI.WebControls" Now, you just have to change the declaration in the ASPX file from <asp:DataGrid id="DG1" runat="server">to <dbwc:HierarGrid id="HG1" runat="server"> Furthermore please keep in mind to change the code-behind file as well: [VB] Protected WithEvents HG1 As DBauer.Web.UI.WebControls.HierarGrid [C#] protected DBauer.Web.UI.WebControls.HierarGrid HG1; Restarting your application should show that nothing has changed so far. The HierarGrid behaves just like the normal DataGrid does. |
|
Using the HierarGrid - Part II | |
[
Back To Top ]
3.4 Defining relations in the DataSetDataSets offer the ability of adding relations between the different tables contained within it. The HierarGrid uses these relations to get from the parent table to its child data. Note that you have to define the relations prior to binding the DataSet to the HierarGrid. The relations can be defined be specifying the key columns in both tables: [VB] Dim dc1 As DataColumn Dim dc2 As DataColumn 'Relation Title => Author on Title_ID column dc1 = ds.Tables(0).Columns("title_id") dc2 = ds.Tables(1).Columns("title_id") Dim dr As DataRelation = New DataRelation("Title_Author", dc1, dc2, False) ds.Relations.Add(dr) 'Relation Title => Sales dc1 = ds.Tables(0).Columns("title_id") dc2 = ds.Tables(2).Columns("title_id") dr = New DataRelation("Title_Sales", dc1, dc2, False) ds.Relations.Add(dr) [C#] DataColumn dc1; DataColumn dc2; //Relation Title => Author on Title_ID column dc1 = ds.Tables[0].Columns["title_id"]; dc2 = ds.Tables[1].Columns["title_id"]; DataRelation dr = new DataRelation("Title_Author", dc1, dc2, false); ds.Relations.Add(dr); //Relation Title => Sales dc1 = ds.Tables[0].Columns["title_id"]; dc2 = ds.Tables[2].Columns["title_id"]; dr = new DataRelation("Title_Sales", dc1, dc2, false); ds.Relations.Add(dr); 3.5 Adding the HierarColumnThe HierarGrid automatically adds a new column called HierarColumn to the grid at runtime if none is specified at design time. This column contains the image of the plus/minus icon and the content of the child templates invisibly. This row can be created at design time similarly to how we define other BoundColumns or TemplateColumns without any further properties. 3.6 Creating a child templateNow comes the part of visually designing a template that is loaded to display the child row information. Please create a new UserControl by right-clicking the project and selection Add | Add new Item | Web User Control and give it the same name as the first child table (in my example it is called Authors.ascx and stored in the folder Templates underneath the project directory). Drop a TextBox onto the form and choose the DataBindings property from the Property viewer. Select "Text" as the bindable property and choose to enter a custom databinding expression. Enter the following code: [VB] DataBinder.Eval(CType(Container, DataGridItem).DataItem, "au_lname") [C#] DataBinder.Eval(((DataGridItem)Container).DataItem, "au_lname") This expression corresponds to the normal databinding syntax for one exception: The container object has to be cast to the type DataGridItem before the DataItem property can be accessed. For each additional child table a new UserControl can be created. 3.7 Using the TemplateSelection eventLet’s take a look at what exactly happens internally when the DataBind method on the HierarGrid is called: As in the standard DataGrid, it is looped over the first table of the DataSource (or the one given in the DataMember attribute as shown in the sample code above). For each row in the table an object of the type DataGridItem is added to the Items collection of the DataGrid. When the DataGrid is rendered and sent to the client browser later in the page cycle (for further reference I recommend the Article Page cycle by Paul Wilson) each DataGridItem is converted into the HTML tag <TR>. This mechanism is extended in the HierarGrid so that after rendering one row of the parent table, the related tables are checked for child rows. If any child rows exist, their corresponding template is loaded and inserted into HierarGridColumn which is itself a child object of the DataGridItem. To illustrate this, let’s take a look at the sample picture above (Fig. 1). In the standard DataGrid, separate rows are generated for the books with the IDs BU1032, BU1111 and BU2075. That is exactly the same with the HierarGrid except that after generating the row for book ID BU1032 the child tables Authors and Sales are checked for corresponding child rows. When we take a look at the book BU1111 we can see that two child rows in the Authors table and one entry in the Sales table were found that correspond to that book. Now, to specify which UserControl should be loaded for each child row (the two in the Author table and the one in the Sales table), I decided to use a special event called TemplateSelection. This event is raised for each row in every child table. In the example mentioned above, this is three times for the book BU1111- and of course several more times for the other books as well. In your sample main ASPX page, create an event handler as shown below. [VB] Private Sub HG1_TemplateSelection(ByVal sender As Object, ByVal e As DBauer.Web.UI.WebControls.HierarGridTemplateSelectionEventArgs) Handles HG1.TemplateSelection e.TemplateFilename = "Templates\\" + e.Row.Table.TableName + ".ascx" End Sub [C#] private void HG1_TemplateSelection(object sender, HierarGridTemplateSelectionEventArgs e) { e.TemplateFilename = "Templates\\" + e.Row.Table.TableName + ".ascx"; } In C# please add the following line to the InitializeComponent method to wire up the new event handler (note that this method is inside the region “Web Form Designer generated code”): [C#] this.HG1.TemplateSelection += new HierarGridTemplateSelectionEventHandler(this.HG1_TemplateSelection); The TemplateSelection event has a parameter "e" which is of type HierarGridTemplateSelectionEventArgs. This type contains two properties that are important to understand. The Row property contains the child row for which the event has been raised. Returning to the example, when the event is raised for the first time, the property contains exactly one row from the author table with data about Michael O’Leary. Based on this data, the developer can decide which template to load. In this case the filename would be based on the name of the table i.e. "Templates\Authors.ascx". This filename is sent back to the HierarGrid with the TemplateFilename property. This property initially doesn’t contain a value, and is reserved as the return value – it’s a workaround as event handlers do not support return values. The HierarGrid expects to find the filename of the template in this property which is then loaded internally using Page.LoadTemplate or Page.LoadControl (fur further reference please check the MSDN documentation). This construct allows you to decide, based on any criteria, on the child row which the template should load. This can either be based on the table name as in the example, or could also be based on a value in a column or some other logical expression. 3.8 Changing the TemplateCachingBase propertyCalling the TemplateSelection event for every single row of each child table would cause quite a performance hit. Therefore I decided to implement a caching option called TemplateCachingBase. This enum property can either be configured to cache based on the tablename of the childrows. In the example above, the TemplateSelection event would be called once for the Authors table and once for the Sales table. The second option is to cache based on the value of a column which has to be specified in the TemplateCachingColumn property. This could be helpful if the subitems are not able to be distinguished by the table name but by a special column in the tables. For example if the Sales table contained a country column and for each country an individual template should be selected, the country column can be specified as the CachingColumn.The third option is to disable caching at all. 3.9 Displaying one template for all child rowsThe sample described above relies on displaying a template for each child row. E.g. when a book has two authors and has been sold three times the total number of templates loaded is five. Imagine you want to change this to display a nested DataGrid for all the authors and a DataGrid for all the sales under book. This can be achieved by setting the TemplateDataMode to "Table" instead of "SingleRow" (its default value). Doing so loads only one template per child table. You could change the Authors.ASCX file described above by dropping a DataGrid instead of a TextBox. In the code-behind you can then use the normal DataBinding syntax to bind a DataSet to the grid. This DataSet of child rows can be taken from the BindingContainer property: [VB] Dim dgi as DataGridItem = CType(Me.BindingContainer, DataGridItem) Dim ds as DataSet = CType(dgi.DataItem, DataSet) DG1.DataSource = ds DG1.DataMember = "Authors" DG1.DataBind() [C#] DataSet ds = (DataSet) ((DataGridItem) this.BindingContainer).DataItem; DG1.DataSource = ds; DG1.DataMember = "Authors"; DG1.DataBind(); 3.10 LoadTemplate vs LoadControlI noticed slight differences with event processing between loading the template using Page.LoadTemplate and Page.LoadControl. This is why I decided to include a property called LoadControlMode to enable both possibilities. So, when experiencing problems with the one method try switching to the other. 4 ConclusionThis tutorial explains how to use the HierarGrid to display parent-child relations in a DataGrid-like control. It is important to remember to bind the data just like you would bind it to a standard datagrid and then use the TemplateSelection event to specify which template to load for which child row. Additionally there are some properties in place to allow easy customization for the most common usage scenarios. I hope this control will be a useful tool in one of your future web applications developed with ASP.NET, and that it will save you some time writing code. If you have any feedback regarding the HierarGrid or this article, I’ll be happy if you let me know. My contact information is available at http://www.denisbauer.com. Updated information about the HierarGrid can also be found in my Weblog. I want to thank Paolo Pignatelli for helping me write this article. |