CodeBetter.Com
CodeBetter.Com
RSS 2.0 via Feedburner
           Do you Twitter? Follow us @CodeBetter

Brendan Tompkins [MVP]

Blog First. Ask Questions Later.

Multi-Table DataSet with DataRelations from a Single-Table DataSet

Recently, I was tasked with creating a reporting application that did the following:

  1. Allow the user to choose Columns in one DataTable in a DataSet to group by.
  2. Render a grouped report from the flat table data.

So, for example, if the DataTable looked like this:

Department EmployeeID Product Count
Purchasing 3322       Apples  1
Purchasing 3322       Oranges 1

Purchasing 3311       Oranges 2
HR         1222       Apples  1
HR         1111       Apples  3

I had to generate a rollup report that looked like this:

Department: Purchasing
  Employee: 3322
    Product: Apples  Count: 1
    Product: Oranges Count: 2
                     Total  3
  Employee: 3311
    Product: Oranges Count: 2
                     Total: 2
          Purchasing Total: 5

Department: HR
  Employee: 1222
    Product: Apples  Count: 1
                     Total: 1
  Employee: 1111
    Product: Apples  Count: 3
                     Total: 3
                  HR Total: 4
               Grand Total: 9

You can see that the solution was going to involve recursion of some sort.  The trouble was, recursion on a flat DataTable is a nightmare!  If you've ever tried it, you know what I mean. Forget using foreach, and bone up on arrays if you're going down this route.

Well, I wrote the code to do this recursion, but I wasn't happy with what I had come up with.  It was ugly code, and hard to follow.  I was complaining to a friend of mine about the complexity, and drawing on a white board.  I drew a picture of an ADO DataSet with DataTables related with DataRelations, and said “If I only had this, this would be easy”...  Then it hit me.  I needed to generate a multi-table DataSet from a flat, one-table DataSet.  I could then use the GetChildRows() method of each data row to find out if I needed to recurse. I could also use a foreach to iterate through the DataRows. 

What I came up with is the following method, that I thought I'd share.  What it does is take a flat DataSet with one DataTable and a string array of ColumnNames that are to be used to generate a DataSet with multiple, related dataTables.  It returns the root table name, which you'll need to start your recursion:

public static string GetNestedDataSet(DataSet flatDs, ref DataSet nestedDs, string [] columnNames)
{
   
string rootTableName = null;
    nestedDs = flatDs.Copy();
    DataTable dataTable = nestedDs.Tables[0];

   
// Store the previous iteration's previous columns
   
DataColumn [] prevUniqueColumns = null;

   
foreach(string columnName in columnNames) 
    {
       
// Add the table
       
DataTable tbl = nestedDs.Tables.Add(columnName + "Table");
       
if(rootTableName == null) rootTableName = tbl.TableName;

        // Store the current iteration's columns
       
ArrayList currentColumns = new ArrayList();
       
if(prevUniqueColumns != null)
        {
           
// Add all the previous columns
           
foreach(DataColumn col in prevUniqueColumns)
            {
                currentColumns.Add(
new DataColumn(col.ColumnName));
            }

           
// Add the current Columns
           
DataColumn [] foriegnKeyCols = (DataColumn []) currentColumns.ToArray(typeof(DataColumn));
            tbl.Columns.AddRange(foriegnKeyCols);

            // Add the parent relations
            tbl.ParentRelations.Add(prevUniqueColumns, foriegnKeyCols);
        }

        // Now add the new column
       
currentColumns.Add(tbl.Columns.Add(columnName)); 

       
// Get the key columns for the current table
       
DataColumn [] primaryKeyCols = (DataColumn []) currentColumns.ToArray(typeof(DataColumn));

       
// Make everything unique
       
tbl.Constraints.Add(tbl.TableName + "_unique", primaryKeyCols, false);

       
// Store columns for next time...
       
prevUniqueColumns = primaryKeyCols;

       
// Now add the data...
       
foreach(DataRow row in nestedDs.Tables[0].Rows)
        {
               
try
               
{
                       
ArrayList al = new ArrayList();
                       
foreach(DataColumn c in primaryKeyCols) al.Add(row[c.ColumnName]);
                        tbl.Rows.Add((
object[]) al.ToArray()); 
                }
               
catch(System.Data.ConstraintException){}
        }
    }
    
    
// Finally, add the table relationship to the base table
    
if(prevUniqueColumns != null)
    {
       
// Add the current Columns
       
DataColumn [] foriegnKeyCols = new DataColumn [prevUniqueColumns.Length];
       
for(int i = 0; i<prevUniqueColumns.Length; i++)
        {
                foriegnKeyCols[i] = dataTable.Columns[prevUniqueColumns[i].ColumnName];
        }
        dataTable.ParentRelations.Add("BaseRelation", prevUniqueColumns, foriegnKeyCols);
    }

    // This happens when we have no rollup columns
    
if(rootTableName == null) rootTableName = dataTable.TableName;
    
return rootTableName;
}

Now, to use this, do the following:

DataSet nestedDataSet = null;
string rootTableName = GetNestedDataSet(MyFlatDataSet, ref nestedDataSet, string [] {“Department“, “EmployeeID“, “Product“});

Here's an example (warning: Air Code) of the recursive routine to do the rendering:

private int RenderGroup(DataRow parentRow)
{
      // Render out something here, like parentRow[0]

     // Now get the children rows, if any and recurse
      
DataRelation childRelation = (parentRow.Table.ChildRelations.Count > 0) ? parentRow.Table.ChildRelations[0] : null;
     
if(childRelation != null)
         foreach(DataRow row in parentRow.GetChildRows(childRelation))
            RenderGroup(row, showDetail, container);
}

That's it!  I put this method in my Utils class for ease of use.   I hope that posting this will save someone else the agony of trying to write recursive routines against a flat DataTable.

-Brendan



Comments

Darrell said:

Very nice.
# April 15, 2004 4:21 AM

Dave Burke said:

# April 19, 2004 1:58 AM

vvn said:

good
# March 30, 2005 1:57 AM

Srivaths said:

hi. its really good. but i thinkthe naming conventions can be better. and i have a problem. i have written an aored procedure which returns 3 tables. how do i populate thosethree tables in a dataset and how do i generate a report using that dataset? it would be really great if you could answer my qurstion.bye.
# March 6, 2006 10:40 PM

tom said:

Hi Brendan,

fantastic post. exactly what i needed (i think) for my 3 level data report (from a flat dataset)

could you point me in the direction for implementing your dataset into nested repeaters?

thanks in advance

# August 6, 2007 5:03 PM

tom said:

never mind, worked it out!

fantastic code again thanks.

the following may help future people:

i bound my first repeater to:

nestedDataSet.Tables["myTopGroupingColumn"];

in my first nested repeater (2nd repeater)

DataSource='<%# ((System.Data.DataRowView)Container.DataItem).CreateChildView("Relation1") %>'

in my final repeater (3rd)

DataSource='<%# ((System.Data.DataRowView)Container.DataItem).CreateChildView("BaseRelation") %>'

in each item template its as easy as <%# Eval("eventId")%>

# August 6, 2007 9:32 PM

Leave a Comment

(required)  
(optional)
(required)  

Enter the numbers above:
Add

About Brendan Tompkins

Brendan has been programming with .NET since the first public beta and is owner and operator of Port Technology Services, a consultancy company providing .NET application development services to the Maritime industry. In July, 2007, he was awarded the Microsoft MVP award for ASP.NET. He's also a proud co-founder of failed .COM startup Intrinsigo, and has had a hand in the failure of numerous other businesses. He currently runs CodeBetter.Com and Devlicio.us, and lives in Norfolk, Virgina with his wife Tiara and son Ian.

View Brendan's profile on LinkedIn

Check out Devlicio.us!

Our Sponsors

Free Tech Publications