Business Class Structure

As we've seen, business objects follow the same sequence of events for creation, retrieval, and updates. Because of this, there's a structure and a set of features that's common to all of them. Although the structure and features are common, however, the actual code will vary for each business objectall we can do is provide some foundations that make it easier for the business developer to know what needs to be done.

Also, there are differences between editable and read-only objects, and between root and child objects. After we've discussed the features common to all business objects, we'll create "templates" to illustrate the structure of each type of business object that we can create based on CSLA .NET.

Common Features

There are some common features or conventions that should be followed as we code any business classes that will inherit from the CSLA .NET base classes. These are as follows :

  • [Serializable()] attribute

  • Common regions

  • private default constructor

  • Nested Criteria class

Let's briefly discuss each of these requirements.

The Serializable Attribute

All business objects must be unanchored so that they can move across the network as needed. This means that they must be marked as serializable by using the [Serializable()] attribute as shown here:

  [Serializable()] public class MyBusinessClass { }  

This is required for all business classes that inherit from any of the CSLA .NET base classes. It's also required for any objects that our business objects reference. If we reference an object that isn't serializable, then we must be sure to mark its variable with the [NonSerialized()] attribute to prevent the serialization process from attempting to serialize that object. If we don't do this, the result will be a runtime exception from the .NET Framework.

Common Regions

When writing code in VS .NET, we can make use of the #region directive to place our code into collapsible regions. This helps organize our code, and allows us to look only at the code pertaining to a specific type of functionality. All our business classes will have a common set of regions, as follows:

  • Business Properties and Methods

  • System.Object Overrides

  • static Methods

  • Constructors

  • Criteria

  • Data Access

The Business Properties and Methods region will contain the methods that are used by UI code (or other client code) to interact with our business object. This includes any properties that allow retrieval or changing of values in the object as well as methods that operate on the object's data to perform business processing.

The System.Object Overrides region will contain overrides for at least the ToString() and Equals() methods from System.Object . By overriding ToString() , we ensure that our business objects will return some meaningful data anytime we attempt to convert the object to text. By overriding and/or overloading the Equals() method, we provide a standardized mechanism by which two of our business objects can be tested for equality.

The static Methods region will contain the static factory methods to create or retrieve our objects, along with the static delete method (if our object is an editable root object).

The Constructors region will contain the constructor methods for the class. At a minimum, each class will contain a private default constructor so that it can be created by the server-side DataPortal .

The Criteria region will contain the nested Criteria class used by the data-portal mechanism to load the class. Many child classes won't include this region, but we'll include it in our template for completeness.

The Data Access region will contain the DataPortal_xyz() methods.

This means that the skeletal structure of a business object, with these regions, is as follows:

  [Serializable()] public class MyBusinessClass : CSLA.baseclass {   #region Business Properties and Methods   #endregion   #region System.Object Overrides   #endregion   #region Static Methods   #endregion   #region Constructors   #endregion   #region Criteria   #endregion   #region Data Access   #endregion }  

To reiterate, all of the business objects in this chapter will follow this basic region scheme.

Private Default Constructor

All business objects will be implemented to make use of the "class-in-charge" scheme that we discussed in Chapter 1. Factory methods are used in lieu of the new keyword, which means that it's best if we prevent the use of new , thereby forcing the UI developer to use our static factory methods instead.

Tip 

The concept of an object factory is a common design pattern, and is discussed in the Gang of Four Design Patterns book. [1]

We also know, from Chapter 5, that the DataPortal mechanism requires our classes to include a default constructor. As we reviewed the create, fetch, update, and delete processes for each type of object earlier in this chapter, each sequence diagram showed how the server-side DataPortal created an instance of our business object. Because this is done through reflection, we must provide a default constructor for the server-side DataPortal to invoke.

By making this constructor private (and by not creating other constructors) we ensure that UI code must use our static factory method to get an instance of any object. These two facts together mean that every business object will have a private default constructor:

 [Serializable()] public class MyBusinessClass : CSLA.baseclass {   #region Business Properties and Methods   #endregion   #region System.Object Overrides   #endregion   #region Static Methods   #endregion   #region Constructors  private MyBusinessClass()   {     // prevent direct creation   }  #endregion   #region Criteria   #endregion   #region Data Access   #endregion } 

This constructor both prevents the new keyword from working, and provides the DataPortal with the ability to create the object via reflection. As we'll see later, our classes might also include other constructors, but this one is required for all objects.

Nested Criteria Class

Any object that can be retrieved from the database must have a nested Criteria class. Also, any object that loads default values from the database (including root and child objects) must have a nested Criteria class.

The Criteria class simply contains the data that's required to identify the specific object to be retrieved, or the default data to be loaded. Since it's passed by value to the DataPortal , this class must be marked as [Serializable()] .

Tip 

Technically, the Criteria class can have any name as long as it's [Serializable()] and nested within the business class. In some cases, we might have two Criteria classesone defining the data to retrieve an object, the other to define the data needed to find the right default values to load when creating an object.

Since this class is no more than a way to ferry data to the DataPortal , it doesn't need to be fancy. Typically, it's implemented with a constructor to make it easier to create and populate the object all at once. For example, here's a Criteria class that includes an EmployeeID field:

 [Serializable()] public class MyBusinessClass : CSLA.baseclass {   #region Business Properties and Methods   #endregion   #region System.Object Overrides   #endregion   #region Static Methods   #endregion   #region Constructors   private MyBusinessClass()   {     // prevent direct creation   }   #endregion   #region Criteria  [Serializable()]   private class Criteria   {     public string EmployeeID;     public Criteria(string employeeID)     {       EmployeeID = employeeID;     }   }  #endregion   #region Data Access   #endregion } 

All Criteria classes are constructed using a similar scheme. The class is scoped as private because it's only needed within the context of our business class. Even though we'll pass the Criteria object through the data-portal mechanism, it's passed as a type object , so the DataPortal code doesn't need access to the object's code. This is ideal, because it means that UI developers, or other business-object developers, won't see the Criteria class, thus improving our business object's overall encapsulation.

The Criteria class will typically contain public fields containing the criteria data needed to identify our specific type of business object. A Project , for instance, is uniquely identified by its ID, so the Criteria class for our Project class will look like this:

 // Criteria for identifying existing object   [Serializable()]   private class Criteria   {     public Guid ID;     public Criteria(Guid id)     {       ID = id;     }   } 

Several times so far, we've noted that exposing fields as public is bad practice. The reason we're doing it here is that we want to make the Criteria class as easy to create as possible. Although we could declare private variables and create public property methods, this is a lot of extra work to force on the business developer. The reason a public field is acceptable here is because the class itself is private this class and its fields can only be used by code within our Project class. Though we're breaking encapsulation of the Criteria class, we're not breaking it for the Project class, which is what really counts in this case.

We're also including a constructor that accepts the criteria data value. This is done to simplify the code that will go into the static factory methods. Rather than forcing the business developer to create a Criteria object and then load its values, this constructor allows the Criteria object to be created and initialized in a single statement. In many cases, this means that our static factory methods will contain just one line of code!

Many Criteria classes will contain a single value (as in our examples here), but they can be more complex, providing for more control over the selection of the object to be retrieved. If we have a root collection in which we're directly retrieving a collection of child objects, the Criteria class may not define a single object, but rather act as a search filter that returns the collection populated with all matching child objects.

In other cases, we may have no criteria data at all. In that case, we still need a Criteria class, but it would be empty as follows:

  [Serializable()]   private class Criteria   { }  

We can still create an instance of this class and pass it to the DataPortal ; it simply provides no criteria data. This is typically used when retrieving a root collection object where we want all the child objects in the database returned at all times. We'll use this technique when we create our ProjectList and ResourceList collection classes later in the chapter.

Class Structures

At this point in the chapter, we've walked through the life cycle of our business objects, so we know the sequence of events that will occur as they are created, retrieved, updated, and deleted. We've also discussed the code concepts and structures that are common to all business classes. Now let's dive in and look at the specific coding structure for each type of business class that we can create based on the CSLA .NET Framework. These include the following:

  • Editable root

  • Editable child

  • Editable, "switchable" (that is, root or child) objects

  • Editable root collection

  • Editable child collection

  • Read-only object

  • Read-only collection

  • Name-value lists

For each of these object types, we'll create the basic starting code that belongs in the class. In a sense, these are the templates from which our business classes can be built.

Tip 

Ideally, we'd put these templates directly into VS .NET, so that they're available as true templates. Unfortunately, this is a much more difficult prospect now than it was in VB 6, because it requires the use of Enterprise Templates or the manual editing of cryptic configuration files. Due to this complexity, and the relatively small amounts of template code involved, we won't make these into true VS .NET templates.

Editable Root Business Objects

The most common type of object will be the editable root business object, since any object-oriented system based on CSLA .NET will have at least one root business object or root collection. (Examples of this type of object include our own Project and Resource objects.) These objects often contain collections of child objects as well as their own object-specific data.

As well as being common, an editable object that's also a root object is the most complex object type, so its code template is the largest we'll be creating. The basic structure for an editable root object is described in the following sections.

Class Header and Instance Variables

We start by declaring the instance variables for our objectthe variables that will contain the data used by the object itself. This also means declaring variables to hold any collections of child objects that might be contained by our business object.

  [Serializable()] public class EditableRoot : BusinessBase {   // Declare variables here to contain object state   // Declare variables for any child collections here  
Business Properties and Methods

In the Business Properties and Methods region, we'll write the business-specific properties and methods for our object. These properties and methods typically interact with the instance variables, performing validation, calculations, and other manipulation of the data, based on our business logic. We might also include properties to expose our child collections for use by the UI or other client code.

  #region Business Properties and Methods    // Implement properties and methods here so the UI or    // other client code can interact with the object   #endregion  
System.Object Overrides

In the System.Object Overrides region, we should override ToString() , Equals() , and GetHashCode() .

  #region System.Object Overrides   public override string ToString()   {     // Return text describing our object   }   public bool Equals(EditableRoot obj)   {     // Implement comparison between two objects of our type   }   public new static bool Equals(object objA, object objB)   {     if(objA is EditableRoot && objB is EditableRoot)       return ((EditableRoot)objA).Equals((EditableRoot)objB);     else       return false;   }   public override bool Equals(object obj)   {     if(obj is EditableRoot)       return this.Equals((EditableRoot)obj);     else       return false;   }   public override int GetHashCode()   {     // Return a hash value for our object   }   #endregion  

The ToString() method is used throughout .NET to convert an object to a text representation, and we should override it to provide a meaningful text representation for our object. In many cases, this will be the object's primary identifying datasuch as the ID field for a Project object:

 public override string ToString()   {     return _id.ToString();   } 

The Equals() method is used throughout .NET to compare two objects to each other. The System.Object base class defines a generic comparison, but it may not be as efficient or accurate as one we could create here. In most cases, we'll determine whether two objects of the same type are equal by simply comparing their primary identifying data, such as the ID field for a Project object:

 public bool Equals(Project project)   {     return _id.Equals(project.ID);   } 

The GetHashCode() method is used by the .NET Framework whenever we add our object to a Hashtable (or some other hashtable-based collection). Like the Equals() method, the default behavior of this method as provided by System.Object isn't particularly accurate or useful. We should override it to return a hash code based on our object's primary identifying data. This would be the ID field for a Project object, for instance:

 public override int GetHashCode()   {     return _id.GetHashCode();   } 
static Methods

In the static Methods region, we see a set of static methods to create, retrieve, and delete the object.

  #region static Methods   public static EditableRoot NewEditableRoot()   {     return (EditableRoot)DataPortal.Create(new Criteria());   }   public static EditableRoot GetEditableRoot()   {     return (EditableRoot)DataPortal.Fetch(new Criteria());   }   public static void DeleteEditableRoot()   {     DataPortal.Delete(new Criteria());   }   #endregion  

Of course, these are just skeletons that must be fleshed out as appropriate. Typically, these methods will accept parameters so that client code can indicate which object is to be retrieved or deleted. That parameter data would then be used to populate the Criteria object, as discussed earlier in the chapter.

Constructors

We place our constructors in the Constructors region. As noted earlier, all business objects at least declare a private default constructor to prevent the UI code from directly creating an instance of our object. By including this method, we ensure that they will use our static factory methods to create the object.

  #region Constructors   private EditableRoot()   {     // Prevent direct creation   }   #endregion  

We also ensure that the class has a default constructor, which is required by the server-side DataPortal when it uses reflection to create instances of our class.

Criteria

In our template code, the Criteria class is empty; it will need to be customized for each business class so that it contains the criteria values required to identify a specific business object.

  #region Criteria   [Serializable()]   private class Criteria   {     // Add criteria here   }   #endregion  
Data Access

Finally, in the Data Access region, we have the four DataPortal_xyz() methods. These methods must include the code to load defaults, to retrieve object data, to update object data, and to delete object data, as appropriate. In most cases, this will be done through ADO.NET, but this code could just as easily be implemented to read or write to an XML file, or to any other data store you can imagine.

  #region Data Access   protected override void DataPortal_Create(object criteria)   {     Criteria crit = (Criteria)criteria;     // Load default values from database   }   protected override void DataPortal_Fetch(object criteria)   {      Criteria crit = (Criteria)criteria;     // Load object data from database   }   protected override void DataPortal_Update()   {     // Insert or update object data into database   }   protected override void DataPortal_ Delete(object criteria)   {     Criteria crit = (Criteria)criteria;     // Delete object data from database   }   #endregion }  
Tip 

Many organizations use an abstract, metadata-driven data-access layer. In environments like this, the business objects don't use ADO.NET directly. This works fine with CSLA .NET, since the data-access code in our DataPortal_xyz() methods can interact with an abstract data-access layer just as easily as it can interact with ADO.NET directly.

The key thing to note about this code template is that there's very little code in the class that's not related to the business. Most of the code in an editable root business class will be the business logic that we implement, or the business- related data-access code that we write. The bulk of the nonbusiness code is already implemented in the CSLA .NET Framework.

Object Creation Without Defaults

The template code shows NewEditableRoot() using DataPortal.Create() to load default values from the database, which is great if that's what we need, but an unnecessary overhead if we don't. For objects that don't need to load default values, we can change the static factory method as follows:

 public static EditableRoot NewEditableRoot()   {  return new EditableRoot();  } 

This also means that we can remove the implementation of DataPortal_Create() , because it will never be called:

  // protected override void DataPortal_Create(object criteria) // { //   Criteria crit = (Criteria)criteria; // //   // Load default values from database // }  

This approach is faster because it avoids the overhead of using the data portal, but of course it doesn't provide us with any way to load defaults from a database. We can, however, set defaults in the default constructor as shown here:

 private EditableRoot()   {     // Prevent direct creation  // Set any default values here  } 
Immediate or Deferred Deletion

As implemented in the template, we can delete the object by calling the static delete method and providing the criteria to identify the object to be deleted. As you know, however, the BusinessBase class also defines a Delete() method that provides for deferred deletion, whereby the object must be retrieved, marked as deleted, and then updated in order for it to be deleted. The object's data is then deleted as part of the update process.

If we only want to support deferred deletion, we can simply remove the static delete method:

  // public static void DeleteEditableRoot() // { //   DataPortal.Delete(new Criteria()); // }  

Then, the only way to delete the object is by calling the Delete() method and updating the object to the database.

Editable Child Business Objects

Most applications will have some editable child objects, or even grandchild objects. Examples of these include our ProjectResource and ResourceAssignment objects. In many cases, the child objects are contained within a child collection object, which we'll discuss later. In other cases, the child object might be referenced directly by the parent object. Either way, the basic structure of a child object is the same; in some ways, this template is very similar to the editable root.

Class Header and Variable Declarations

We start out by declaring our variables, along with any collections of child objects (which would be grandchild objects at this point).

  [Serializable()] public class EditableChild : BusinessBase {   // Declare variables here to contain object state   // Declare variables for any child collections here  
Business Properties and Methods

The Business Properties and Methods region will contain the properties and methods used by client code, such as the UI, to interact with our object. This is no different from what we would do in a root object.

  #region Business Properties and Methods    // Implement properties and methods here so the UI or    // other client code can interact with the object   #endregion  
System.Object Overrides

Again, as in a root object, we override key System.Object() methods. This is even more important when dealing with child objects, because they're very likely to be contained in collections. It's critical that the Equals() method should operate efficiently and accurately, because it will be used by the collection classes to determine whether the collection contains a specific child object.

  #region System.Object Overrides   public override string ToString()   {     // Return text describing our object   }   public bool Equals(MyBusinessClass obj)   {     // Implement comparison between two of our types of object   }   public override int GetHashCode()   {     // Return a hash value for our object   }   #endregion  
static Factory Methods

The static Methods region differs from the root object. We need to support creation of a new child object, but that static method is now scoped as internal . This way, only the parent object or collection can create an instance of the child objectnot client code such as the UI.

We also include a GetEditableChild() method, which is called by the parent object or collection during the fetch process. As we discussed earlier in the object life-cycle discussion, the parent object calls this method and passes it a data-reader object so that our child object can load its data.

  #region static Methods  internal static EditableChild NewEditableChild()  {     return (EditableChild)DataPortal.Create(new Criteria());   }  internal static EditableChild GetEditableChild(IDataReader dr)  {     EditableChild obj = new EditableChild();     obj.Fetch(dr);     return obj;   }   #endregion  
Tip 

If we're obtaining our data from somewhere other than ADO.NET, we might pass a different type of object here. For instance, if we're loading our data from an XML document, we might pass an XmlNode instead of a data reader.

Constructors

Like all business classes, editable child classes include a private default constructor. This constructor includes a call to MarkAsChild() to tell CSLA .NET that this is a child object. This call is requiredif it weren't there, the framework would view this object as being an editable root object, rather than a child object.

  #region Constructors   private EditableChild()   {     // Prevent direct creation     // Tell CSLA .NET that we're a child object     MarkAsChild();   }   #endregion  
Criteria

We also include a Criteria class, but this is only required if we want to load default values from the database as new child objects are created.

  #region Criteria   [Serializable()]   private class Criteria   {     // Add criteria here   }   #endregion  
Data Access

The biggest difference from a root object comes in the Data Access region. We implement DataPortal_Create() to support the loading of default values from the database on the creation of a new child object, but otherwise all retrieval and updating of child objects is handled directly by the parent object. To support this concept, we include a private method named Fetch() that's called from our static factory method, and an internal method named Update() that will be called by the parent object or collection.

  #region Data Access   protected override void DataPortal_Create(object criteria)   {     Criteria crit = (Criteria)criteria;     // Load default values from database   }   private void Fetch(IDataReader dr)   {     // Load object data from database   }   internal void Update(EditableRoot Parent)   {     // Insert or update object data into database   }   #endregion }  

Typically, the parent object calls our static factory method, passing it a populated data reader. The static factory method calls our private Fetch() method, which takes the data from the data reader and populates the object with the values.

Tip 

We could also implement Fetch() so that each child object retrieves its own data from the database. In that case, the parameters to the method would be the key values needed to find the right child object. This approach is usually less efficient than having the parent or collection object retrieve all the data at once, however, so the template's approach is recommended in most cases.

The Update() method is called by the parent or collection object when we should insert, update, or delete our data from the database. This method typically accepts a parameter containing a reference to our parent object so that we can get any property values from it as needed. Each child object usually calls the database directly to save its data.

As an example, our ProjectResource child object will need the ID property from its parent Project object so that it can store it as a foreign key in the database. By getting a reference to its parent Project object, the ProjectResource gains access to that value as needed.

Object Creation Without Defaults

As implemented, the template uses DataPortal.Create() to load the child object with default values from the database. As we discussed earlier, if our object doesn't need to load default values from the database, we can provide a more efficient implementation by changing the static factory method to create the child object directly. Then we can remove the Criteria class and the DataPortal_Create() method (since they won't be used), and use the default constructor to set any default values that are hard-coded into the class.

Switchable Objects

It's possible that our projects will contain classes that are to be instantiated as root objects on some occasions, and as child objects on others. This can be handled by conditionally calling MarkAsChild() , based on how the object is being created.

Typically, this can't be done in the default constructor, because there's no way to determine there whether we're being created as a root or a child object. Instead, we need to go back to our object's life cycle to see where we can make this decision. In fact, since the default is for an object to be a root object, all we need to do is determine the paths by which a child object can be created, and make sure to call MarkAsChild() only in those cases.

The template for creating a "switchable" object is the same as the editable root template, with the following exceptions.

Object Creation

Our Criteria class must include a flag to indicate whether we're being created as a root or a child object (this is in addition to any object-specific criteria fields that we would also have in this class).

 [Serializable()]   private class Criteria   {  public bool IsChild;  public Criteria(bool IsChild)  {       this.IsChild = IsChild;     }  } 

Then, instead of a single static factory method to create the object, we'll have two methodsone public , the other internal .

 public static Switchable NewSwitchable()   {  return (Switchable)DataPortal.Create(new Criteria(false));  }   internal static Switchable NewSwitchableChild()   {  return (Switchable)DataPortal.Create(new Criteria(true));  } 
Fetching the Object

Likewise, our static factory Fetch() methods will be doubled :

 public static Switchable GetSwitchable()   {  return (Switchable)DataPortal.Fetch(new Criteria(false));  }  internal static Switchable GetSwitchableChild()   {     return (Switchable)DataPortal.Fetch(new Criteria(true));   }  
Immediate Deletion

And if we support immediate deletion, we'll adapt the static delete method as well.

 public static void DeleteSwitchable()   {  DataPortal.Delete(new Criteria(false));  } 
Data-Access Methods

Then, in our DataPortal_Create() method, we can use the flag as follows:

 protected override Sub DataPortal_Create(object criteria)       {         Criteria crit = (Criteria)criteria;         if(crit.IsChild)  MarkAsChild();  // Load default values from database       } 

We'll also implement both DataPortal_Fetch() and Fetch() , and DataPortal_Update() and Update() methods. The private Fetch() method is responsible for calling MarkAsChild() to mark the object as a child, since it's being loaded via the child process as shown here:

 private void Fetch(IDataReader dr)   {     // Load object data from database  MarkAsChild();  } 

When we're doing updates, we don't need to worry about calling MarkAsChild() (because the object already exists), but we still need to support both styles of update, since DataPortal_Update() will be used if we're in root mode, and the internal Update() method will be used if we're in child mode.

Object Creation Without Defaults

If we're creating the object using the new keyword instead of calling DataPortal.Create() , we can simply alter the static factory method to call MarkAsChild() as shown here:

  internal static Switchable newSwitchableChild()   {     Switchable obj = new Switchable();     obj.MarkAsChild();     return obj;   }  

From the parent object's perspective, there's no differenceit just calls the static methodbut this approach is faster because it doesn't load default values from the database.

Editable Root Collection

There will be times when we'll want to retrieve a collection of child objects directly that is, where the collection itself is the root object. For instance, we may have a Windows Forms UI consisting of a DataGrid control, in which we want to display a collection of Contact objects. By making the root object a collection of fully populated Contact objects, we can simply bind the collection to the DataGrid , and the user can do in-place editing of the objects within the grid.

The benefit of this approach is that we start with a collection of fully populated and editable child objects. The user can interact with all of them, and save them all at once when all edits are complete. However, this is only subtly different from having a regular root object that has a collection of child objects. Figure 7-10 shows the regular root-object approach on the left, and the collection root-object approach on the right.

image from book
Figure 7-10: Comparing simple-root vs. collection-root objects

This approach isn't recommended when there are large numbers of potential child objects, because the retrieval process can become too slow, but it can be very useful in cases where we can specify criteria to limit the number of objects returned. To create an editable root collection object, we can use a template like this:

  [Serializable()] public class EditableRootCollection : BusinessCollectionBase { #region Business Properties and Methods   public EditableChild this [int index]   {     get     {       return (EditableChild)List[index];     }   }   public void Add(string data)   {     List.Add(EditableChild.NewEditableChild());   }   public void Remove(EditableChild child)   {     List.Remove(child);   }   #endregion }  

The Business Properties and Methods region includes the typical collection behaviors: retrieving a specific item, adding an item, and removing an item. Our BusinessCollectionBase class already implements other features like a Count property.

Whenever we implement a collection object, we need to overload the Contains() and ContainsDeleted() methods from BusinessCollectionBase to provide type-specific comparisons of our child objects. This is placed in a Contains region as follows:

  #region Contains   public overload bool Contains(EditableChild item)   {     foreach(EditableChild child in List)       if(child.Equals(item))         return true;     return false;   }   public bool ContainsDeleted(EditableChild item)   {     foreach(EditableChild child in deletedList)       if(child.Equals(item))         return true;     return false;   }   #endregion  

It seems like this ought to be unnecessary, because the .NET base collection code should properly compare our child objects. Unfortunately, this isn't the casethe default .NET behavior doesn't call the Equals() methods that we implemented in our child objects. This means that we must implement type-specific comparisons within our business collection classes to ensure that our Equals() methods are invoked.

There's no System.Object Overrides region in this template. While there may be a few collection objects in which these overrides would be of value, in most cases the collection object can't be consolidated into something simple enough to be represented by a simple line of text, or easily compared to another collection in an Equals() method.

  #region static Methods   public static EditableRootCollection NewCollection()   {     return new EditableRootCollection();   }   public static EditableRootCollection GetCollection()   {     return (EditableRootCollection)DataPortal.Fetch(new Criteria());   }   public static void DeleteCollection()   {     DataPortal.Delete(new Criteria());   }   #endregion  

The static Methods section includes a method to create an empty collection. While this could call DataPortal.Create() , it's very unlikely that a collection object will have default values that need retrieving from the database. (If your collection object does require this functionality, call DataPortal.Create() and implement a DataPortal_Create() method.) There are also methods for retrieving and deleting the collection. The former calls DataPortal.Fetch() , and triggers the process of creating and returning all the child objects belonging to the collection. The latter triggers the process of deleting all the data for all child objects belonging to the collection.

The template also includes Constructor and Criteria regions, just like those in the EditableRoot template.

  #region Data Access   protected override void DataPortal_Fetch(object criteria)   {     Criteria crit = (Criteria)criteria;     // Retrieve all child data into a data reader     // then loop through and create each child object     while(dr.Read())       List.Add(EditableChild.GetEditableChild(dr));   }   protected override void DataPortal_Update()   {     // Loop through each deleted child object and call its Update() method     foreach(EditableChild child in deletedList)       child.Update(this);     // Then clear the list of deleted objects because they are truly gone now     deletedList.Clear();     // Loop through each nondeleted child object and call its Update() method     foreach(EditableChild child in List)       child.Update(this);   }  protected override void DataPortal_Delete(object criteria)  {     Criteria crit = (Criteria)criteria;     // Delete all child-object data that matches the criteria   }   #endregion }  

Finally, the data-access methods for collections are somewhat different from those for simple editable objects. The DataPortal_Fetch() method is responsible for getting the data from the database, typically via a data reader. It then calls the static factory method of the child class for each row in the data reader, thereby allowing each child object to load its data. As we've seen, the static factory method in the child class calls its own private Fetch() method to actually load the data from the data reader.

The DataPortal_Update() method must loop through all the child objects contained in the collection, calling each object's Update() method in turn . We must run through the list of deleted child objects and the list of nondeleted child objects.

Note 

It's critical that the deleted child objects be processed first.

It's quite possible for the user to delete a child object from the collection, and then add a new child object with the same primary key value . This means that we'll have the original child object marked as deleted in our list of deleted child objects, and the new child object in our list of nondeleted objects. This new object will have its IsNew property set to true , because it's a new object. If we don't first delete the original child object, the insertion of the new child object will fail.

Thus, in the template, we first process the list of deleted child objects, and then move on to process the list of nondeleted child objects.

Editable Child Collection

The most common type of collection is one where we have a collection of child objects, like ProjectResources and ResourceAssignments in our sample application.

Tip 

Note that the parent object here might be a root object, or it might be a child itselfchild objects can be nested, if that's what our business object model requires. In other words, we support not only root-child, but also child-grandchild and grandchild to great-grand-child relationships.

A child collection class inherits from BusinessCollectionBase and calls MarkAsChild() during its creation process to indicate that it's operating in child mode. This also means that it won't be directly retrieved or updated by the DataPortal , but instead will be retrieved or updated by its parent object.

Class Header and Collection Behaviors

The basic code structure is as follows:

  [Serializable()] public class EditableChildCollection : BusinessCollectionBase {   #region Business Properties and Methods   public EditableChild this [int index]   {     get     {       return (EditableChild)List[index];     }   }   public void Add(string data)   {     List.Add(EditableChild.NewEditableChild(data));   }   public void Remove(EditableChild child)   {     List.Remove(child);   }   #endregion  

The Business Properties and Methods regions are no different from those for a root collection. As with any collection, we expose a strongly typed indexer and Add() and Remove() methods.

Contains Methods

The Contains region is no different from that for a root collection either, as shown here:

  #region Contains   public overload bool Contains(EditableChild item)   {     foreach(EditableChild child in List)       if(child.Equals(item))         return true;     return false;   }   public bool ContainsDeleted(EditableChild item)   {     foreach(EditableChild child in deletedList)       if(child.Equals(item))         return true;     return false;   }   #endregion  

Again, we must implement type-specific methods so the overloaded Equals() methods of our child objects are properly invoked.

static Methods and Constructors

The static Methods and Constructors regions, however, are quite different. Here's the former:

  #region static Methods   internal static EditableChildCollection NewEditableChildCollection()   {     return new EditableChildCollection();   }   internal static EditableChildCollection GetEditableChildCollection(     IDataReader dr)   {     EditableChildCollection col = new EditableChildCollection();     col.Fetch(dr);     return col;   }   #endregion  

Since only a parent object can create or fetch an instance of this class, the static factory methods are scoped as internal . The static method to create an object simply returns a new collection object. As with the EditableChild template, our constructor will call MarkAsChild() to indicate that this is a child object. Likewise, the static method to load the child collection with data creates a new collection object and then calls its private Fetch() methodjust like we did in the EditableChild template.

The constructor looks like this:

  #region Constructors   private EditableChildCollection()   {     // Prevent direct creation     MarkAsChild();   }   #endregion  

The constructor method calls MarkAsChild() to mark this as a child collection object. This is the same as our code in the EditableChild class template.

Since there's virtually never a reason to load a child collection with default values, we don't include a Criteria region or Criteria class. If you have a need to load the child collection with default values, you can add that region, alter the static factory method, and add a DataPortal_Create() method, as we discussed for the EditableChild class template.

Data Access

The Fetch() method is very similar to the one in the EditableChild template. It accepts a data reader (or some other data source) as a parameter and then loops through the data, creating a child object for each row and then adding that child object to the collection.

  #region Data Access   private void Fetch(IDataReader dr)   {     // Create a child object for each row in the data source     while(dr.Read())       List.Add(EditableChild.GetEditableChild(dr));   }   internal void Update(EditableRoot parent)   {     // Loop through each deleted child object and call its Update() method     foreach(EditableChild child in deletedList)       child.Update(parent);     // Then clear the list of deleted objects because they are truly gone now     deletedList.Clear();     // Loop through each nondeleted child object and call its Update() method     foreach(EditableChild child in List)       child.Update(parent);   }   #endregion }  

The Update() method is similar to the DataPortal_Update() method in our EditableRootCollection . It loops through the list of deleted child objects, calling their Update() methods, and then does the same for the nondeleted child objects.

Read-Only Business Objects

Sometimes, we may need an object that provides data in a read-only fashion. If we want a list of read-only data, we'll create a read-only collection; but if we want a single object containing read-only data, we'll inherit from ReadOnlyBase . This is one of the simplest types of object to create, since it does nothing more than retrieve and return data as shown here:

  [Serializable()] public class ReadOnlyObject : ReadOnlyBase {   // Declare variables here to contain object state   // Declare variables for any child collections here   #region Business Properties and Methods   // Implement read-only properties and methods here so the UI or   // other client code can interact with the object   //public string Data   //{   //  get   //  {   //  }   //}   #endregion   #region System.Object Overrides   public override string ToString()   {     // Return text describing our object   }   public bool Equals(ReadOnlyObject obj)   {      // Implement comparison between two objects of our type   }  public override int GetHashCode()  {     // Return a hash value for our object   }   #endregion   #region static Methods   public static ReadOnlyObject GetReadOnlyObject()   {     return (ReadOnlyObject)DataPortal.Fetch(new Criteria());   }   #endregion   #region Constructors   private ReadOnlyObject()   {     // Prevent direct creation   }   #endregion   #region Criteria  [Serializable()]   private class Criteria  {     // Add criteria here   }  #endregion  #region Data Access   protected override void DataPortal_ Fetch(object criteria)   {     Criteria crit = (Criteria)criteria;     // Load object data from database   }   #endregion }  

Like other business objects, a read-only object will have instance variables that contain its data. It will typically also have read-only properties or methods that allow client code to retrieve values. As long as they don't change the state of the object, these may even be calculated values.

In the static Methods region, we have a static factory method that retrieves the object by calling DataPortal.Fetch() . This means that we also have a Criteria class, which should be modified to contain the criteria data needed to select the correct object for retrieval.

The Data Access region just contains DataPortal_Fetch() , which is the only virtual method from the base class. Of course, there's no need to support updating or deleting a read-only object.

Read-Only Collections of Objects

We may also need to retrieve a collection of read-only objects. When we implemented the CSLA .NET Framework, we created the ReadOnlyCollectionBase class, which is a read-only collection. It throws an exception any time we attempt to change which items are in the collection by adding or removing objects.

However, there's no way for the collection object to stop client code from interacting with the child objects themselves . Typically, the items in the collection will be derived from ReadOnlyBase , so they too are read-only. If we put read-write objects into the collection, client code will be able to alter their data. A read-only collection only guarantees that objects can't be added or removed from the collection.

The code for a typical read-only collection object looks like this:

  [Serializable()] public class ReadOnlyCollection : CSLA.ReadOnlyCollectionBase {   #region Business Properties and Methods   public ReadOnlyObject this [int index]   {     get     {       return (ReadOnlyObject)List[index];     }   }   #endregion  

The only valid operation on a read-only collection is to retrieve items, so we have an indexer, but no methods to add or remove elements. We also overload the Contains() method to provide a type-specific version that invokes the overloaded Equals() method on our child class as shown here:

  #region Contains   public bool Contains(ReadOnlyObject item)   {     foreach(ReadOnlyObject child in List)       if(child.Equals(item))         return true;     return false;   }   #endregion  

In the static Methods region, we have a static factory method to return a collection loaded with data. It calls DataPortal.Fetch() , and so we have a Criteria class as well as a private constructor. This is no different from the classes we've looked at already.

  #region static Methods   public static ReadOnlyCollection GetCollection()   {     return (ReadOnlyCollection)DataPortal.Fetch(new Criteria());   }   #endregion   #region Constructors   private ReadOnlyCollection()   {     // Prevent direct creation   }   #endregion   #region Criteria   [Serializable()]   public class Criteria   {}   #endregion  

Finally, we have DataPortal_Fetch() , in which we set the locked flag to false , then load the data from the database, and then restore its value to true . Although locked is set to true , any attempt to add or remove items from the collection will result in an exception being thrown, so we need to set it to false for long enough to load the data from the database and insert all the appropriate child objects into the collection:

  #region Data Access   protected override void DataPortal_Fetch(object criteria)   {     locked = false;     Criteria crit = (Criteria)criteria;     // Retrieve all child data into a data reader     // then loop through and create each child object     while(dr.Read())       List.Add(ReadOnlyObject.GetReadOnlyObject(dr));     locked = true;   }   #endregion }  

Name-Value List Objects

Perhaps the simplest business object that we can create is a name-value list that inherits from the NameValueList class in the CSLA .NET Framework. The base class provides virtually all the functionality we need, including basic data access and the static factory method, so all we need to do is provide a Criteria class and a DataPortal_Fetch() method. It turns out that the only code specific to each list is the implementation of DataPortal_Fetch() .

Basic Name-Value Code

Here's the code for almost any name-value list object:

  [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     private MyDataList(       System.Runtime.Serialization.SerializationInfo info,       System.Runtime.Serialization.StreamingContext context) :       base(info, context)     {}   #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   }  

To customize this for almost any data from a single table, simply change the database, table, and column names specified in the DataPortal_Fetch() implementation:

 // Called by DataPortal to load data from the database   protected override void DataPortal_Fetch(object criteria)   {  SimpleFetch("database", "table", "namecolumn", "valuecolumn");  } 

This will populate the object with the appropriate columns from the specified table in the specified database. The database must have a corresponding entry in the application configuration file so that the DataPortal code can retrieve the connection string, as we discussed in Chapter 5.

Complex Name-Value Data Retrieval

In some cases, it may not be possible to retrieve the name-value data using the simple retrieval method provided by the NameValueList base class. Perhaps a table join or some other sophisticated scheme is required; or perhaps the list must be filtered so that some criteria are required to support the retrieval process.

This can be handled by changing the static factory method to accept criteria data, thereby enhancing the class to store that data and implementing our own data-access code in DataPortal_Fetch() rather than calling the SimpleFetch() method, just like we did in the EditableRoot class template.

At this point, we've examined the basic structure of the code for each type of business object or collection that we can create based on the CSLA .NET Framework. Let's move on now and implement our sample project-tracker application classes.

[1] Erich Gamma, et al., Design Patterns (Reading, MA, Addison-Wesley, 1995), p. 87.



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