Utility Classes

We'll wrap up this chapter by creating a couple more utility classes that may be generally useful in application development. The first is SafeDataReader , a wrapper around any ADO.NET data reader object that automatically handles any null values that might come from the database. The second is NameValueList , which will implement a generic read-only name -value collection object that can be loaded from a database table.

SafeDataReader

Null values should be allowed in database columns for only two reasons. The first is when we care about the difference between a value that was never entered and a value that is zero (or an empty string). In other words, we actually care about the difference between "" and null , or between 0 and null . There are applications where this matters ”where the business rules revolve around whether a field ever had a value (even an empty one) or never had a value at all.

The second reason for using a null value is where a data type doesn't intrinsically support the concept of an empty field. The most common example is the SQL DateTime data type, which has no way to represent an empty date value; it always contains a valid date. In such a case, we can allow null values in the column specifically so that we can use null to indicate an empty date.

Of course, these two reasons are mutually exclusive. If we're using null values to differentiate between an empty field and one that never had a value, we need to come up with some other scheme to indicate an empty DateTime field. The solution to this problem is outside the scope of this book ”but thankfully, the problem itself is quite rare.

The reality is that very few applications ever care about the difference between an empty value and one that was never entered, so the first scenario seldom applies. If it does apply, then dealing with null values at the database level isn't an issue ”all variables in the application must be of type object , so that they can hold a null value. Obviously, there are negative performance implications here, but if the business requirements dictate that "" or 0 are different from null , then this is the answer.

For most applications, the only reason for using null values is the second scenario, and this one is quite common. Any application that uses date values, and where an empty date is a valid entry, will likely use null to represent an empty date.

Unfortunately, a whole lot of poorly designed databases allow null values in columns where neither scenario applies, and we developers have to deal with them. These are databases from which we might retrieve a null value even if we don't care about it and didn't want it. Writing defensive code to guard against tables where null values are erroneously allowed can quickly bloat our data access code and make it hard to read. To avoid this, we'll create a SafeDataReader class that will take care of these details on our behalf .

As a rule, data reader objects are sealed , meaning that we can't simply subclass an existing data reader class such as SqlDataReader and extend it. However, we can do what we did in Chapter 4 to create our SmartDate class: we can encapsulate or "wrap" a data reader object.

Creating the SafeDataReader Class

To ensure that we can wrap any data reader object, we'll be working with the root IDataReader interface that's implemented by all data reader objects. Also, since we want the SafeDataReader to be a data reader object, we'll implement that interface as well.

Tip  

Because IDataReader is a very large interface, we won't include all the code for the class in the book's text. The complete class is available in the code download associated with the book.

Create the Class

Add a new class to the CSLA project and name it SafeDataReader . Let's start with the following code:

  using System; using System.Data; namespace CSLA.Data {   public class SafeDataReader : IDataReader   {     IDataReader _dataReader;     public SafeDataReader(IDataReader dataReader)     {       _dataReader = dataReader;     }   } }  

We put the class in the CSLA.Data.SafeDataReader namespace. This helps keep the class organized within our framework.

We start by declaring that we'll implement the IDataReader interface ourselves in this class. The class also defines a variable to store a reference to the real data reader that we'll be encapsulating, and we have a constructor that accepts that data reader object as a parameter.

To create an instance of SafeDataReader , we need to start with an existing data reader object. This is pretty easy ”it means that our ADO.NET code might appear as follows :

 SafeDataReader dr = new SafeDataReader(cm.ExecuteReader); 

A command object's ExecuteReader() method returns a data reader object that we can use to initialize our wrapper object. The remainder of our data access code can use our SafeDataReader object just like a regular data reader object, because we've implemented IDataReader .

The implementation of IDataReader is a lengthy business ”it contains a lot of methods ”so we're not going to go through all of it here. However, we should look at a couple of methods to get a feel for how the thing works.

GetString

All the methods that return column data are "null protected" with code like this:

  public string GetString(int i)    {      if( _dataReader.IsDBNull(i))        return string.Empty;      else        return _dataReader.GetString(i);    }  

If the value in the database is null , we return some more palatable value ”typically, whatever passes for "empty" for the specific data type. If the value isn't null , we simply return the value from the underlying data reader object.

There really are a lot of these methods. There has to be one for each data type that we might get from the database!

GetDateTime and GetSmartDate

Most types have "empty" values that are obvious, but two are problematic : DateTime and Boolean . The former, for instance, has no "empty" value:

  public DateTime GetDateTime(int i)    {      if(_dataReader.IsDBNull(i))        return DateTime.MinValue;      else        return _dataReader.GetDateTime(i);    }  

Here, we're arbitrarily assigning the minimum date value to be the "empty" value. This hooks into all the issues that we discussed in Chapter 4, when we created the SmartDate class. We'll provide alternative methods in SafeDataReader that utilize the SmartDate class:

  public CSLA.SmartDate GetSmartDate(int i)    {      if(_dataReader.IsDBNull(i))        return new CSLA.SmartDate(false);   else        return new CSLA.SmartDate(_dataReader.GetDateTime(i), false);    }    public CSLA.SmartDate GetSmartDate(int i, bool minIsEmpty)    {      if(_dataReader.IsDBNull(i))        return new CSLA.SmartDate(minIsEmpty);      else        return new CSLA.SmartDate(_dataReader.GetDateTime(i), minIsEmpty);    }  

Our data access code can now choose either to accept the minimum date value as being equivalent to "empty" or to retrieve a SmartDate object that is more intelligent :

 SmartDate myDate = dr.GetSmartDate(0); 

or

 SmartDate myDate = dr.GetSmartDate(0, false); 
GetBoolean

Likewise, there is no "empty" value for the bool type:

  public bool GetBoolean(int i)    {      if(_dataReader.IsDBNull(i))        return false;      else        return _dataReader.GetBoolean(i);    }  

As you can see, we arbitrarily return a false value in this case. You may need to alter the return value of GetBoolean() (or indeed GetDateTime() ) to suit your specific application requirements.

Other Methods

The IDataReader interface also includes a number of methods that don't return column values, such as the Read() method:

  public bool Read()    {      return _dataReader.Read();  } 

In these cases, we simply delegate the method call down to the underlying data reader object for it to handle. Any return values are passed back to the calling code, so the fact that our wrapper is involved is entirely transparent.

The SafeDataReader class can be used to simplify our data access code dramatically, anytime we're working with tables in which null values are allowed in columns where we really don't want them or care about them. If your application does care about the use of null values, you can simply use the regular data reader objects instead.

We'll make use of SafeDataReader in Chapter 7 as we implement our business objects.

NameValueList

The last class we'll implement in this chapter ”and in the business framework ” is NameValueList . This is a base class on which we can easily build name-value lists for our applications.

Many, if not most, applications need read-only name-value lists to populate combo box controls, list box controls, etc. Typically, these lists come from relatively static configuration tables that are separately managed. Examples of such lists might be a list of product types, customer categories, valid shipping methods, and so forth.

Rather than putting in a lot of effort to implement specific read-only collection objects for each type of list in an application, it would be nice if there were an easy way to create them quickly ”preferably with little or no code. We won't achieve no code here, but by implementing the NameValueList base class, we'll be able to implement read-only name-value list objects with very little code indeed. The NameValueList class will provide core functionality:

  • Retrieve a value, given a name.

  • Retrieve a name, given a value.

  • Retrieve a copy of the entire list as a NameValueCollection .

  • Implement generic data access to populate the list.

  • Data access can be overridden with custom code, if needed.

This functionality should be sufficient to provide most of the name-value collection behaviors that we need in business applications. To create a specific name-value collection object, we can subclass NameValueList , provide an object-specific Criteria class and a static creation method, and we're pretty much done. All the hard work will be done by the base class we're about to create.

Our end goal is to make it so that the business developer can create a name-value list class with very little code. When we're done, the business developer will be able to create a typical name-value list like this:

 [Serializable()] _ public class MyDataList : NameValueList {   #region Static Methods   public static MyDataList GetMyDataList()   {     return (MyDataList)DataPortal.Fetch(new Criteria());   }   #endregion   #region Constructors   private MyDataList()   {     // Prevent direct creation   }   // This constructor overload is required because the base class   // (NameObjectCollectionBase) implements ISerializable   protected NameValueList(SerializationInfo info, StreamingContext context) :     base(info, context)   {     // we have nothing to serialize   }   #endregion   #region Criteria   [Serializable()] _   private class Criteria   {     // Add criteria here   }   #endregion   #region Data Access   // Called by DataPortal to load data from the database   protected override void DataPortal_Fetch(object Criteria)   {     SimpleFetch("myDatabase", "myTable", "myNameColumn", " myValueColumn");   }   #endregion } 

The resulting business object will be similar to a read-only version of a regular .NET NameValueCollection . It will load itself with data from the database, table, and columns specified.

Tip  

The extra private constructor is a bit odd. It turns out that when a class implements the ISerializable interface, any class that inherits from it must implement a special constructor, as shown here. The NameObjectCollectionBase class from which we'll ultimately inherit implements ISerializable , forcing us to write this extra code. Presumably, the default serialization provided by [Serializable()] was insufficient for NameObjectCollectionBase , so Microsoft had to implement ISerializable , leaving us to deal with this unfortunate side effect.

While we already have a ReadOnlyCollectionBase class on which we can build read-only collections, it's not particularly useful when we want to build a name-value object. A name-value collection is different from a basic collection, because it not only stores values like a collection, but also it stores names associated with each of those values. Because ReadOnlyCollectionBase is a subclass of CollectionBase , it isn't well suited to acting as a name-value collection.

However, by creating a subclass of NameObjectCollectionBase that integrates with our CSLA .NET framework, we can easily create a base class that provides read-only name-value data that can be loaded from data in a database table. We can think of NameValueList as being a peer to ReadOnlyCollectionBase within our framework.

Creating NameValueList

Add a new class to the CSLA project and name it NameValueList . As usual, we'll be using a number of namespaces, so let's use them now:

  using System; using System.Data.SqlClient; using System.Collections.Specialized; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary;  

Then we can start creating the class, which will be very like the ReadOnlyBase and ReadOnlyCollectionBase classes from Chapter 4. It will be serializable, and it will implement the ICloneable interface. To get all the prebuilt collection behaviors to track both names and values, we'll inherit from NameObjectCollectionBase :

  [Serializable()]   abstract public class NameValueList :     NameObjectCollectionBase, ICloneable   {     #region ICloneable     public object Clone()     {       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, this);       buffer.Position = 0;       return formatter.Deserialize(buffer);     }     #endregion   }  
Making the Data Read-Only

The NameObjectCollectionBase provided by .NET is a lot like the other collection base classes, in that it manages the collection of data internally, leaving it up to us to implement methods such as Item() and Add() . In fact, NameObjectCollectionBase is even nicer, because it doesn't automatically expose methods like Remove() , RemoveAt() , or Clear() , so we don't have to write extra code to make our object read-only.

Add the following code to the class:

  #region Collection methods    public string this [int index]    {      get      {        return (string)base.BaseGet(index);      }    }    public string this [string name]    {      get      {        return (string)base.BaseGet(name);      }    }   protected void Add(string name, string newvalue)    {      base.BaseAdd(name, newvalue);    }    public string Key(string item)    {      foreach(string keyName in this)        if(this[keyName] == item)          return keyName;      // we didn't find a match - throw an exception      throw new ApplicationException("No matching item in collection");    }    #endregion  

Here, we've implemented two indexers so that we can retrieve a value based either on its text key value or by its numeric location in the list. This mirrors the behavior of the .NET NameValueCollection .

We've also implemented a Key() property that returns the text of the key that corresponds to a specific value. The rationale behind this is that we often load a list control with values and allow the user to select one. When that happens, we need to convert that value back to the key, since it's typically the key that's important to the database. The value itself is typically human-readable text, which is nice for display but useless for storage.

If an invalid value is supplied to Key() , we'll throw an exception. This is a powerful validation tool, because it means that we can write very simple code to validate user entry. The business logic might look like this:

 _key = myList.Key(_userInput); 

If the user input isn't a valid item in our list, this will result in an exception being thrown.

Finally, we included an Add() method. Notice that this is protected in scope, so only subclasses based on our base class can add data to the collection. We don't want to allow other business objects or the UI to change the data in the collection ” it should be loaded from the database, and then be read-only.

Constructor Methods

We need to implement two constructor methods, which is a bit different from what we've seen so far. We'll explain them following the code:

  #region Create and Load    protected NameValueList()    {      // prevent public creation    }    protected NameValueList(SerializationInfo info, StreamingContext context) :      base(info, context)    {      // we have nothing to serialize    }    #endregion  

The first constructor is a default one that allows the business developer to inherit from our base class. Normally, we don't need to include default constructors like this, because the C# compiler creates them for us. However, if we write a parameterized constructor, the compiler won't generate a default, so we must add it explicitly.

The second constructor is required because, as stated earlier, the NameObjectCollectionBase class implements the ISerializable interface. When a class implements this interface, it must provide a special constructor that the serialization infrastructure can call to deserialize the object. Unfortunately, this also means that any classes that inherit from a class that implements ISerializable must also implement this specific constructor, delegating the call to the base class itself.

Standard Data Access Methods

As with all our other business base classes, we must provide the DataPortal_xyz() methods. Since we're creating a base for read-only objects, the only method we need to make non- private is DataPortal_Fetch() . That's the only one the business developer needs to worry about overriding:

  #region Data Access    private void DataPortal_Create(object criteria)    {      throw new NotSupportedException("Invalid operation - create not allowed");    }    virtual protected void DataPortal_Fetch(object criteria)    {      throw new NotSupportedException("Invalid operation - fetch not allowed");    }    private void DataPortal_Update()    {      throw new NotSupportedException("Invalid operation - update not allowed");    }   private void DataPortal_Delete(object criteria)    {      throw new NotSupportedException("Invalid operation - delete not allowed");    }    protected string DB(string databaseName)    {      return ConfigurationSettings.AppSettings["DB:" + databaseName];    }    #endregion  

Now the business developer can provide an implementation of DataPortal_Fetch() to load the collection with name-value pairs from his database or from some other data store (such as an XML file).

A Data Access Helper

The code to load a list with name-value pairs is often very generic. Rather than forcing the business developer to write the same basic code over and over, we'll implement a helper function that she can use to do most name-value list data access. This means that the implementation of DataPortal_Fetch() in a list object will often be just one line of code that calls into the base class code we'll implement here, for instance:

 protected override void DataPortal_Fetch(object Criteria)   {     SimpleFetch("myDatabase", "myTable", "myNameColumn", " myValueColumn");   } 

For cases where this generic helper routine can't retrieve the data, the business developer will need to write his own data access code in DataPortal_Fetch() . This approach allows total customization of data access, if that's what's required, while allowing most list objects to contain virtually no data access code at all.

The generic helper function, SimpleFetch() , will retrieve a list of name-value data. It accepts the name of the database and the table from which the data will come as parameters. It also accepts the column names for the name and value columns. Given this information, it connects to the database and runs a simple SELECT query to retrieve the two columns from the specified table.

Add the following code to the class within the Data Access region:

  protected void SimpleFetch(string dataBaseName,       string tableName, string nameColumn, string valueColumn)     {       using(SqlConnection cn = new SqlConnection(DB(dataBaseName)))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandText =             "SELECT " + nameColumn + "," + valueColumn + " FROM " + tableName;           using(SqlDataReader dr = cm.ExecuteReader())           {             while(dr.Read())             {               if(dr.IsDBNull(1))               {                 Add(dr.GetValue(0).ToString(), string.Empty);               }               else               {                 Add(dr.GetValue(0).ToString(), dr.GetValue(1).ToString());               }             }           }         }       }     }  

This is pretty straightforward ADO.NET code. We use the database name to retrieve a connection string from the application's configuration file and open the connection:

 SqlConnection cn = new SqlConnection(DB(dataBaseName));      SqlCommand cm = new SqlCommand();      cn.Open(); 

Using this connection, we set up a SqlCommand object to retrieve the name-value columns from the specified table:

 cm.Connection = cn;       cm.CommandText =         "SELECT " + nameColumn + "," + valueColumn + " FROM " + tableName; 

Notice that we aren't using parameters in the SQL statement here. This is because the values we're providing are the actual names of columns and the table, not of data values, so there's no way to make them parameter driven.

The remainder of the code simply uses a SqlDataReader to retrieve the data and populate the list.

Note that we aren't using the strongly typed GetString() method to retrieve the data. Rather, we're calling GetValue() and then converting the value to a string . The reason for this is that we can't be sure that the underlying database column type is a string ”it could be numerical or a date, for instance. By using this technique, we ensure that we can read the value and convert it to a string representation regardless of the underlying data type of the database column.

Since we're using the GetValue() method, we have to deal with null values. This code does assume that the key column won't contain nulls and that the key values are unique. It doesn't make that assumption about the values in the data column. They could be null, in which case we convert them to empty string values.

Remember that if we need more sophisticated data access than is provided by SimpleFetch() , we'd just implement our own code rather than calling into the base class.



Expert C# Business Objects
Expert C# 2008 Business Objects
ISBN: 1430210192
EAN: 2147483647
Year: 2006
Pages: 111
Authors: Rockford Lhotka
BUY ON AMAZON

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