Loading a DataSet from Objects

As we discussed earlier, the biggest single problem we face when trying to do reporting in an object-oriented application is that the commercial report-generation engines don't support the concept of reporting against objects.

There's also the issue that generating reports against large sets of data would require that we create massive numbers of objects ”and there's a lot of overhead involved in that process. The truth is that when we need to create a really large report, we're probably better off simply generating the report directly from the database. For smaller reports or lists, however, it would be nice if we could generate reports based on our objects.

Tip  

Though the code we're about to create is unrelated to the batch processor we just finished, the two can be used in combination. It's quite likely that a batch job will be used to generate a report. In that case, we may use the object-to- DataSet converter within that batch job's worker code as we generate the report.

To overcome the limitations of the reporting tools, we can create a utility that generates a DataSet (or more accurately, a DataTable in a DataSet ) based on an object (or a collection of objects). This isn't terribly difficult, because we can use reflection to get a list of the properties or fields on the objects, and then loop through the objects' properties to populate the DataTable with their values.

Tip  

This isn't a new concept. The OleDbDataAdapter object has a Fill() method that populates a DataTable object from an ADO Recordset . We're going to do basically the same thing, but we'll use a business object or collection of objects as a data source, instead of a Recordset .

What we're creating is somewhat similar to a data-adapter object such as OleDbDataAdapter , in that we'll implement a Fill() method that fills a DataSet with data from an object or collection.

The ObjectAdapter Class

To implement a Fill() method that copies data from a source such as a business object into a DataSet , we need to support a certain amount of basic functionality. In ADO.NET, data is stored in a DataTable , and then that DataTable is held in a DataSet . This means that what we're really talking about here is copying data from a data source into a DataTable object.

We need to be able to examine the data source to get a list of the properties, fields, or columns it exposes. That list will be used to define the list of columns we'll be creating in the target DataTable object. Alternatively, we'll support the concept of a preexisting DataTable that already contains columns. In that case, we'll attempt to find properties, fields, or columns in our data source that match the columns that already exist in the target DataTable object.

Also, rather obviously, we need to be able to retrieve data values from the original data source. To do this, we'll use reflection, because it allows us to create code to retrieve values from the properties or fields of an object dynamically.

Operational Scope

Figure 11-11 illustrates the possible data sources we want to support with our ObjectAdapter class.

image from book
Figure 11-11: Data sources supported by ObjectAdapter
Tip  

We could simplify our code by only supporting binding to an object, but by supporting any valid data source (including ADO.NET objects, or arrays of simple values), we're providing a more complete solution.

Ultimately, we want to be able to retrieve a list of column, property, or field names from a DataView , or from the elements contained within an array or collection ”and those might be simple types (such as int or string ) or complex types (such as a struct or an object).

Helpfully, as you can see, all data sources implement the IList interface that's defined in the .NET Framework. However, sometimes we need to dig a bit to find that interface. Some data-source objects, such as a DataSet , don't expose IList directly. Instead, they expose IListSource , which we can use to get an IList . As you'll see, we'll have to allow for these different possibilities as we build our class.

Setting Up the Class

Add a new class to the CSLA project named ObjectAdapter . This class will contain our code to create a DataTable from an object. To start it off, let's import some namespaces and declare an ArrayList to hold the names of the columns we'll be adding to the DataTable :

  using System; using System.Data; using System.Collections; using System.ComponentModel; using System.Reflection; namespace CSLA.Data {   public class ObjectAdapter   {     ArrayList _columns = new ArrayList();   } }  

The _columns object will be used to maintain a list of the columns of the DataTable that we'll be populating from our object's properties and fields during the fill process.

Before we write the Fill() method, we need to create some helper methods to simplify the process. When we do the fill operation, we'll first need to discover the properties and fields on the source object, and then we'll need to loop through all the objects, and use the object data to populate rows of data in the target DataTable .

Automatic Discovery of Properties and Fields

We'll support two ways of deciding which properties and fields should be copied from our data source into the target DataSet . One way is that the DataTable preexists and already has columns defined. In that case, we'll simply attempt to copy values from the data source into those columns.

However, we'll also support loading data into a new, empty DataTable object. In this case, we want to be able to create the columns in the new DataTable dynamically: one column for each property or field in our data source.

This means that we need to get a list of the public properties and fields on the object (or other data source, such as a DataView , array, or struct ). We can use reflection to do this discovery process.

Getting Column Names from a DataView

If our data source is a DataView , we can implement a simple routine to retrieve the column names from the DataView to populate our list, as shown here:

  void ScanDataView(DataView ds)     {       for(int field = 0; field < ds.Table.Columns.Count; field++)         _columns.Add(ds.Table.Columns[field].ColumnName);     }  

This is the simplest scenario, since the DataView object provides us with an easy interface to retrieve the list of columns.

Getting Field Names from an IList

Our other main option is that we're dealing with an IList : either an array or a collection object. Here, we have a couple of scenarios: either the elements in the list are simple data types such as int or string , or they're complex struct or object elements. We can create a ScanIList() method to detect these scenarios, as follows :

  void ScanIList(IList ds)     {       object obj = null;       if(ds.Count > 0)       {         // retrieve the first item from the list         obj = ds[0];       }       if(obj is ValueType && obj.GetType().IsPrimitive)       {         // the value is a primitive value type         _columns.Add("Value");       }       else       {       if(obj is string)       {         // the value is a simple string         _columns.Add("Text");       }       else       {         // we have a complex struct or object         ScanObject(obj);       }     }   }  

If we have an IList , it means that we have a collection of elements. In order to detect the fields and properties, we need to get access to the first element in the list. After that, we assume that all the other elements in the list have the same fields and properties.

Tip  

This is the way Windows Forms and Web Forms data binding works as well: They assume that all elements in an array or collection are of the same data type.

Once we have a reference to the first element in the list, we can see if it's a simple data type, as follows:

 if(obj is ValueType && obj.GetType().IsPrimitive)       {         // the value is a primitive value type         _columns.Add("Value");       } 

We can also check to see if it's a string (which isn't actually a primitive type, but we want to treat it as such), as shown here:

 if(obj is string)         {           // the value is a simple string           _columns.Add("Text");         } 

If the first element in the list is neither a primitive data type nor a string , we'll assume that it's a complex structure or object ”in which case, we'll call a ScanObject() method that will scan the object to get a list of its properties and fields, as shown here:

 else         {           // we have a complex struct or object           ScanObject(obj);         } 
Getting Property and Field Names from a Structure or Object

Now we can move on to implementing the routine that scans the object. The ScanObject() method will use reflection to scan the object (or struct ) for all its public properties and fields, as follows:

  void ScanObject(object source)     {       Type sourceType = source.GetType();       // retrieve a list of all public properties       PropertyInfo [] props = sourceType.GetProperties();       for(int column = 0; column < props.Length; column++)         if(props[column].CanRead)           _columns.Add(props[column].Name);     // retrieve a list of all public fields     FieldInfo [] fields = sourceType.GetFields();     for(int column = 0; column < fields.Length; column++)       _columns.Add(fields[column].Name);     }  

First, we loop through all the public properties on the object, adding the name of each readable property to our list of columns, as shown here:

 // retrieve a list of all public properties       PropertyInfo [] props = sourceType.GetProperties();       for(int column = 0; column < props.Length; column++)         if(props[column].CanRead)           _columns.Add(props[column].Name); 

Then we loop through all the public fields on the object and add their names to the list of columns, as shown here:

 // retrieve a list of all public fields       FieldInfo [] fields = sourceType.GetFields();       for(int column = 0; column < fields.Length; column++)         _columns.Add(fields[column].Name); 

Going through the list of fields is particularly important in supporting struct types, since they typically expose their data in that way. The end result is that we have a list of all the properties and fields on our data type, whether it's a DataView , an IList of simple types, or a struct or an object. (In the case of simple types, there's just one column of data.)

Implementing the AutoDiscover Method

Now that we have the scanning methods complete, the AutoDiscover() method can be implemented easily. All we need to do is determine if we're dealing with an IListSource , an IList , or a basic object, and call the appropriate scan method, as follows:

  void AutoDiscover(object source)     {       object innerSource;       if(source is IListSource)       {         innerSource = ((IListSource)source).GetList();       }       else       {         innerSource = source;       }       _columns.Clear();       if(innerSource is DataView)       {         ScanDataView((DataView)innerSource);       }       else       {         if(innerSource is IList)         {           ScanIList((IList)innerSource);         }         else         {           // they gave us a regular object           ScanObject(innerSource);         }       }     }  

The first thing we need to do in our autodiscovery routine is to determine if the data source is an IListSource , which is the case with a DataSet , for instance:

 object innerSource;       if(source is IListSource)       {         innerSource = ((IListSource)source).GetList();       }       else       {         innerSource = source;       } 

At this point, the innerSource variable contains a reference to a DataView , an IList interface, a struct , or an object. Now we can check the type of the innerSource object to see which kind it is. If it's a DataView , we can call a routine that scans the DataView to get its list of columns, as follows:

 if(innerSource is DataView)       {         ScanDataView((DataView)innerSource);       } 

Alternatively, it could be an IList as shown here:

 if(innerSource is IList)         {           ScanIList((IList)innerSource);         } 

If the object is neither a DataView nor an IList , then it must be a regular object. We scan it directly, as follows:

 else         {           // they gave us a regular object           ScanObject(innerSource);         } 

At this point, the _column array contains a list of all the columns we'll be using to populate the DataTable , so we can move on to implement the routine that actually does the data copy.

GetField Method

At the core of what we're doing is reflection. In particular, we need to be able to retrieve a value from the object's property or field. The GetField() function does just that, as shown here:

  #region GetField    string GetField(object obj, string fieldName)    {      if(obj is DataRowView)      {        // this is a DataRowView from a DataView        return ((DataRowView)obj)[fieldName].ToString();      }      else      {        if(obj is ValueType && obj.GetType().IsPrimitive)        {          // this is a primitive value type          if(obj == null)            return string.Empty();          else            return obj.ToString();        }   else        {          if(obj is string)          {            // this is a simple string            if(obj == null)              return string.Empty();            else              return obj.ToString();          }          else          {            // this is an object or struct            try            {              Type sourcetype = obj.GetType();              // see if the field is a property              PropertyInfo prop =                sourcetype.GetProperty(fieldName);              if(prop == null  !prop.CanRead)              {                // no readable property of that                // name exists - check for a field                FieldInfo field = sourcetype.GetField(fieldName);                if(field == null)                {                  // no field exists either, throw an exception                  throw new System.Data.DataException(                    "No such value exists: " + fieldName);                }                else                {                  // got a field, return its value                  return field.GetValue(obj).ToString();                }              }              else              {                // found a property, return its value                return prop.GetValue(obj, null).ToString();              }            }   catch(Exception ex)            {              throw new System.Data.DataException(                "Error reading value: " + fieldName, ex);            }          }        }      }    }    #endregion  

One of the data sources that we said we want to support is an ADO.NET DataView . In that case, the object we're dealing with here will be a DataRowView , as follows:

 if(obj is DataRowView)       {         // this is a DataRowView from a DataView         return ((DataRowView)obj)[fieldName].ToString();       } 

Our data source might also be an array of simple values such as int , in which case we need to detect that and return the following simple value:

 if(obj is ValueType && obj.GetType().IsPrimitive)       {         // this is a primitive value type         return obj.ToString();       } 

Similarly, our data source might be an array of string data, as shown here:

 if(obj is string)         {           // this is a simple string           return obj.ToString();         } 

If our data source was none of these, then it's a more complex type ”a struct or an object. In this case, we have more work to do, since we must use reflection to find the property or field and retrieve its value. The first thing we do is get a Type object, so that we have access to type information about the source object, as follows:

 // this is an object or Structure             try             {               Type sourcetype = obj.GetType(); 

Then we try to see if there's a property with the name of the column we're after, as shown here:

 // see if the field is a property               PropertyInfo prop =                 sourcetype.GetProperty(fieldName);               if(prop == null  !prop.CanRead) 

If there's no such property (or if the property isn't readable), then we must assume we're looking for a field instead. However, if we do find a readable property, we can return its value, as follows:

 else               {                 // found a property, return its value                 return prop.GetValue(obj, null).ToString();               } 

On the other hand, if we didn't find a readable property, we look for a field, as follows:

 // no readable property of that               // name exists - check for a field               FieldInfo field = sourcetype.GetField(fieldName);               if(field == null) 
Tip  

By supporting public fields, we're conforming to the data-binding behavior of Windows Forms. Unfortunately, Web Forms data binding only supports retrieving data from properties, which makes it quite awkward to use with typical struct data types. By conforming to the Windows Forms behavior, we're providing a more complete solution.

If there's no field by this name, then we're stuck, so we throw an exception indicating that we were unsuccessful :

 throw new System.Data.DataException(                 "No such value exists: " + fieldName); 

However, if we do find a matching field, we return its value, as follows:

 else               {                 // got a field, return its value                 return field.GetValue(obj).ToString();               } 

If we get an error as we try to do any of this, we return the error text as a result, instead of the data value:

 catch(Exception ex)               {                 throw new System.Data.DataException(                   "Error reading value: " + fieldName, ex);               } 

The end result is that the GetField() method will return a property or field value from a row in a DataView , from an array of simple values, or from a struct or object. We'll use this functionality as we copy the data values from our data source into the DataTable object during the fill operation.

Populating a DataTable from an IList

The core of this process is a method that copies data from the elements in an IList into a DataTable object, as shown here:

  void DataCopyIList(DataTable dt, IList ds)     {       // create columns if needed       foreach(string column in _columns)       {         if(!dt.Columns.Contains(column))           dt.Columns.Add(column);       }       // load the data into the control       dt.BeginLoadData();       for(int index = 0; index < ds.Count; index++)       {         DataRow dr = dt.NewRow();         foreach(string column in _columns)         {           try           {             dr[column] = GetField(ds[index], column);           }           catch(Exception ex)           {             dr[column] = ex.Message;           }         }         dt.Rows.Add(dr);       }       dt.EndLoadData();     }  

The first thing we do is make sure that the DataTable object has columns corresponding to our list of column names in _columns , as follows:

 // create columns if needed foreach(string column in _columns) {   if(!dt.Columns.Contains(column))     dt.Columns.Add(column); } 
Tip  

It's possible that the DataTable already exists and has some or all of the columns we'll be populating, so we only add columns if they don't already exist.

With that done, we can loop through all the elements in the IList , copying the data from each item into a DataTable row. To get each value from the object, we use the GetField() method we implemented earlier. Since it supports DataView , primitive, string , and complex data sources, it handles all the details of getting the right type of value from the data source.

Copying the Data

We also need to create a method to initiate the data-copy process. Like AutoDiscover() , this method needs to see if the data source is an IListSource , or an IList , or something else. Ultimately, we need an IList interface, so this method ensures that we have one, as shown here:

  void DataCopy(DataTable dt, object source)    {      if(source == null) return;      if(_columns.Count < 1) return;      if(source is IListSource)      {        DataCopyIList(dt, ((IListSource)source).GetList());      }      else      {        if(source is IList)        {          DataCopyIList(dt, (IList)source);        }        else        {          // they gave us a regular object - create a list          ArrayList col = new ArrayList();          col.Add(source);          DataCopyIList(dt, (IList)col);        }      }    }  

Figure 11-12 illustrates the process.

image from book
Figure 11-12: Process of getting an IList from which to copy the data

If the data source is an IListSource , we use it to get an IList reference. If it's already an IList , we can use it directly. If the data source is neither of these, then it's a regular Structure or object and so we need to create a collection object ”in this case an ArrayList ”so that we have an IList reference, as shown here:

 else       {         // they gave us a regular object - create a list         ArrayList col = new ArrayList();         col.Add(source);         DataCopyIList(dt, (IList)col);       } 

Ultimately, we have an IList , so we can call the DataCopyIList() method to copy the values into the DataTable .

Implementing the Fill Methods

All that remains at this point is to implement the Fill() methods that will be used by client code to copy the data into a DataSet . We'll model our Fill() methods after the Fill() methods on ADO.NET data-adapter objects ” specifically , the Fill() method on OleDbDataAdapter that allows us to copy the data from an ADO Recordset into an ADO.NET DataSet .

The most basic Fill() method is the one that copies an object into a DataTable , as shown here:

  public void Fill(DataTable dt, object source)    {      AutoDiscover(source);      DataCopy(dt, source);    }  

First, we run AutoDiscover() to generate a list of the columns to be copied, and then we call DataCopy() to initiate the copy process itself. The end result is that the DataTable is populated with the data from the object (or collection of objects).

We can also implement a Fill() method that accepts a DataSet , the name of a DataTable to create, and the object data source, as follows:

  public void Fill(DataSet ds, string tableName, object source)    {      DataTable dt = ds.Tables[tableName];      bool exists = (dt != null);      if(!exists)        dt = new DataTable(tableName);      Fill(dt, source);      if(!exists)        ds.Tables.Add(dt);    }  

In this case, we check to see if a DataTable by this name already exists. If it doesn't exist, then we create it, populate it, and add it to the DataSet . If it does already exist, we simply add the object's data to the existing DataTable , merging the new data into the DataTable along with whatever data might already be in the DataTable .

The final variation on the Fill() method is one where we're passed a DataSet and the object, in which case we'll generate a DataTable name based on the type name of the object itself, as shown here:

  public void Fill(DataSet ds, object source)    {      string className = source.GetType().Name;      Fill(ds, className, source);    }  

Since we've already implemented a Fill() method that handles the creation of a DataTable by name, we can simply invoke that version of Fill() , passing it the table name we've generated here.

At this point, our ObjectAdapter is complete. Client code can use our Fill() methods to copy data from virtually any object or collection of objects into a DataTable . Once the data is in a DataTable , we can use commercial reporting engines such as Crystal Reports or Active Reports to generate reports against the data.

Reporting Using ObjectAdapter

The actual process of creating a report using Crystal Reports or Active Reports is outside the scope of this book, but it's worth walking through the basic process and some of the key code involved in working with the ObjectAdapter . Now we'll create a report using these tools. In this example, we'll use Crystal Reports, since it's included with many versions of VS .NET.

The complete code for a report is included in the Windows Forms client application for our ProjectTracker application. The ProjectList form in the PTWin project contains the code to generate a Crystal Report based on a Project object.

The key steps involved are as follows:

  • Manually create a DataSet object in the project by using the DataSet designer in VS .NET.

  • Create the report based on the DataSet we created in the designer.

  • Write code to retrieve the business object.

  • Write code to call ObjectAdapter to populate the DataSet we created, using the object.

  • Set the report's data source to the DataSet we just populated.

The first couple of steps may seem odd: Why should we have to create a DataSet object manually, using the designer? The answer is that Crystal Reports requires a DataSet with information about the "tables" (and the columns in each "table") in order to design a report. Without such a DataSet , we can't use the graphical designers to create the report itself.

Tip  

Remember that when we say "table" here, we're really talking about a DataTable generated with values from our objects. When we manually create this DataSet , we should create "tables" and "columns" that match our business objects and their properties and fields, not the structure of any underlying database.

As long as we create the tables and columns in our DataSet based on the object, property, and field names from our business objects, the ObjectAdapter will fill the DataSet with object data just fine. For instance, in the PTWin project you'll find a Projects.xsd file, which describes a DataSet defined using the VS .NET designer. This is illustrated in Figure 11-13.

image from book
Figure 11-13: Using the DataSet designer to define our report data

Using this DataSet , we can create a Crystal Report using the Crystal Reports designer. In this case, we're creating a simple list that displays basic information about a Project object and its ProjectResources . An example layout for a report is shown in Figure 11-14.

image from book
Figure 11-14: Example Crystal Reports layout for a ProjectReport

Once the DataSet is populated with data via the ObjectAdapter , it's a simple matter to tell Crystal Reports to generate the report, using our DataSet as its data source. The code to do this, given a preloaded Project object, is similar to this:

 CSLA.Data.ObjectAdapter da =           new CSLA.Data.ObjectAdapter();         Projects ds = new Projects();         da.Fill(ds, _project);         da.Fill(ds, _project.Resources);         projectReport1.SetDataSource(ds);         crystalReportViewer1.ReportSource = projectReport1; 

We create a new ObjectAdapter object and a new DataSet based on the Projects.xsd file in our project. We then use the ObjectAdapter to fill the DataSet with the data from the Project object, and also from its Resources collection.

Once the DataSet has been populated with our data, we set the data source of the Crystal Report object to be our DataSet , and then we have the Crystal Report Viewer display the report. The end result, as shown in Figure 11-15, is a normal Crystal Reports report, just like we'd get from a database, but instead it's populated from our business objects.

image from book
Figure 11-15: A Crystal Reports report generated based on business objects

Using the ObjectAdapter , we can easily convert our objects and collections into a DataSet , which makes virtually any datacentric reporting tool available for our use.

The primary advantage to doing this is for when we want to generate small reports or lists on the client workstation. We already have the ability to retrieve business objects through our DataPortal mechanism, and once they're on the client, we can easily copy them to a DataSet and generate reports.

Tip  

If we're creating reports that require access to large volumes of data, however, we should generate the report on a server. In such a case, we might also consider using the batch-processing engine that we created in the first half of the chapter!



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net