Project-Tracker Objects

In Chapter 6, we created an object model for our sample project-tracking application. This object model, shown in Figure 7-11, includes some editable root business objects ( Project and Resource ), some editable child objects ( ProjectResource and ResourceAssignment ), some collections of child objects ( ProjectResources and ResourceAssignments ), and a name -value list ( RoleList ). It also includes two read-only collections ( ProjectList and ResourceList ).

image from book
Figure 7-11: ProjectTracker application static class diagram

By implementing these objects, we should get a good feel for the practical process of taking the class templates we've just examined and applying them to the creation of real business classes.

Setting Up the Project

Before creating the classes, we need to create a project in which to build them. On this occasion, this will be a Class Library project, or DLL assembly.

By putting our business classes in a DLL, we make it easy for them to be referenced by various different "front ends." This is important for our example, because we'll be using exactly the same DLL to support the Windows Forms, Web Forms, and web-services interfaces we'll be creating. It's equally important in "real-world" applications, since they too often have multiple interfaces. Even if an application starts with a single interface, the odds are good that at some time in the future, it will need a new one.

Open the ProjectTracker solution that we created in Chapter 6. At the moment, it only contains the PTrackerDB database project, so add a new Class Library project and give it a name of ProjectTracker.Library . This will be a library of classes that we can use to construct the ProjectTracker application.

Since we'll be using the CSLA .NET Framework, we need to reference CSLA.dll . As usual, this is done through the Add Reference dialog box, so click the Browse button and navigate to the CSLA\bin directory, where we created the assembly in Chapters 3 through 5. Choose all the DLLs in the directory, as shown in Figure 7-12.

image from book
Figure 7-12: Referencing the CSLA assemblies

Technically, we only need to reference those libraries that contain classes that CSLA.dll inherits fromwhich is just CSLA.BindableBase.dll but we also need to consider deployment. Referencing a DLL causes VS .NET to copy it into our application directory automatically, thereby simplifying deployment because we can just copy our application's bin directory to a client machine to install the application.

As a result, CSLA.Server.DataPortal.dll and CSLA.Server.ServicedDataPortal.dll are referenced and will end up on the client workstations of Windows Forms clients . This might appear to be a problem for workstations running Windows 98, ME, or NT 4.0, since CSLA.Server.ServicedDataPortal.dll requires COM+. However, these DLLs won't be accessed (or even loaded into memory) unless they're used.

Note 

If we plan to run the server-side DataPortal on older client workstations, we must not use the [Transactional()] attribute on our data-access methods .

As long as we don't use the [Transactional()] attribute on our DataPortal_xyz() methods, none of our code will use transactional components , and so CSLA.Server.ServicedDataPortal.dll will never be loaded into memory. If we do use the [Transactional()] attribute and attempt to run the server-side DataPortal code under an older operating system, we'll get runtime exceptions due to the lack of COM+ support on such machines.

If we need the two-phase distributed transactional support provided by Enterprise Services, then we must ensure that the server-side DataPortal code runs on machines with Windows 2000 or higher, where COM+ is available. If we're using older client workstations, then we'll typically do this by running the server-side DataPortal remotely on a Windows 2000 application server.

Finally, delete Class1.cs from the projectwe'll add our own classes as needed. At this point, we're all ready to create our business classes.

Business Class Implementation

The business classes that we'll implement here follow the object-oriented design we created in Chapter 6. In that chapter, we came up with not only the classes to be created, but also their major data elements. Furthermore, we identified which CSLA .NET base classes each one will subclass.

In this section, we'll walk through the first few classes in detail. The other classes will be very similar, so for those we'll discuss only the key features. Of course, the complete code for all classes is available in the code for this book, which is available for download.

The Project Class

The Project class is an editable root class that represents a single project in our application. It will follow the EditableRoot template, as we discussed earlier in the chapter. This means that it inherits from BusinessBase as shown in Figure 7-13.

image from book
Figure 7-13: Project class subclasses BusinessBase

To create the Project class, we'll start with the editable root template and fill in the specific code for a project. Since our data-access code will interact with SQL Server, it needs to import the SqlClient namespace as follows :

  using System; using System.Threading; using System.Data; using System.Data.SqlClient; using CSLA; using CSLA.Data;  namespace ProjectTracker.Library  {   [Serializable()]   public class Project : BusinessBase   {  
Variable Declarations

We know that our class will contain some instance variables , so let's declare those next :

  Guid _id = Guid.NewGuid();     string _name = string.Empty;     SmartDate _started = new SmartDate(false);     SmartDate _ended = new SmartDate();     string _description = string.Empty;  

These correspond to the data fields that we described in our object model in Chapter 6.

Notice that the date values are of type SmartDate , rather than just DateTime . We're taking advantage of our SmartDate class that understands empty dates, and in fact, we're specifying that _started should treat an empty date as the maximum possible date value, while _ended will treat it as the minimum value.

Note 

All string instance variables should be initialized with a default value when they're declared. This is because Windows Forms data binding throws a runtime exception when attempting to data bind against string properties that return null.

Our Project objects will also contain a collection of ProjectResource child objects. Since we haven't yet created the child collection class, this will result in an error at the moment, but we'll add the declaration regardless:

  ProjectResources _resources =       ProjectResources.NewProjectResources();  
Business Properties and Methods

Next up is the Business Properties and Methods region, where we implement the bulk of our business logic as well as expose appropriate data and functionality to any client code. Let's start with the ID property, as shown here:

  #region Business Properties and Methods     public Guid ID     {       get       {         return _id;       }     }     #endregion  

Since this is the primary key for the data in the database, and the unique ID field for the object, it's a read-only property. Making the property read-only is possible because the value is a GUID, and we can easily generate a unique value when the object is initialized. As we'll see when we implement the Resource class, things are more complex when a non-GUID value is used.

Now let's try something a bit more interesting. In Chapter 6, we walked through some use cases that described the application we're now building. Included in the use cases were descriptions of the data the users required, and several rules governing that data. Included in the description of a project was a required Name field.

The Name property isn't only read-write, but it includes some business rule checking, so we'll make use of the BrokenRules functionality that we built into our base class. Add this to the same region:

  public string Name      {        get        {         return _name;        }        set        {         if(value == null) value = string.Empty;         if(_name != value)         {           _name = value;           BrokenRules.Assert("NameLen", "Name too long", (value.Length > 50));           BrokenRules.Assert("NameRequired", "Project name required",             (value.Length == 0));           MarkDirty();         }        }      }  

When a new value is provided for the Name property, we first check to see if the provided value actually is newif it's the same as the value we already have, then there's no sense in doing any work. After that, we check a couple of business validation rules, such as whether the length of the value exceeds our maximum length, as shown here:

 BrokenRules.Assert("NameLen", "Name too long", (value.Length > 50)); 

If the length exceeds 50, an entry will be added to our list of broken rules. If it's 50 or less, any matching entry in the broken-rules list will be removed.

The same is done if the length of the value is zero. Since this is a required field, a zero length results in a broken rule, while a nonzero length is acceptable.

In any case, we update our internal value ( _name ) with the value provided. This might put our object into an invalid state, but that's all rightour base class provides the IsValid property, which would then return false to indicate that our object is currently invalid. The IsValid property is automatically maintained by the CSLA .NET Framework as we mark rules as broken or unbroken via the Assert() method.

This also ties into the implementation of the Save() method in the base class, which prevents any attempt at updating the database when the object is in an invalid state.

When we change the value of the _name variable, we also make this call:

  MarkDirty();  

This tells the CSLA .NET Framework that our object has been altered . If we don't make this call, our object may not be updated into the database properly, and Windows Forms data binding will not work properly.

Note 

Whenever we change the value of one of our instance variables, we must call MarkDirty() . In fact, we should call it anywhere that our instance variables are changed, whether that's in a property like this, or within a method that implements business logic that updates our object's values.

Moving on, the two SmartDate fields also include business validation as shown here:

  • The started date must be prior to the ended date, or the ended date must be empty.

  • The ended date can be empty, or it must be after the started date.

  • If there's an ended date, there must be a started date.

By storing the date fields in SmartDate objects, this becomes easy to implement. We expose the date fields as string properties so that they can be easily bound to controls in either Windows Forms or Web Forms interfaces, but behind the scenes we're using the SmartDate data type to manage the date values:

  public string Started     {       get       {         return _started.Text;       }       set       {         if(value == null) value = string.Empty;         if(_started.Text != value)         {           _started.Text = value;           if(_ended.IsEmpty)             BrokenRules.Assert("DateCol", "", false);           else             BrokenRules.Assert("DateCol",               "Start date must be prior to end date",               _started.CompareTo(_ended) > 0);            MarkDirty();          }        }      }     public string Ended     {       get       {         return _ended.Text;       }       set       {         if(value == null) value = string.Empty;         if(_ended.Text != value)         {           _ended.Text = value;           if(_ended.IsEmpty)             BrokenRules.Assert("DateCol", "", false);           else           {              if(_started.IsEmpty)                BrokenRules.Assert("DateCol",                 "Ended date must be later than started date", true);             else                BrokenRules.Assert("DateCol",                 "Ended date must be later than started date",                 _ended.CompareTo(_started) < 0);            }            MarkDirty();          }        }      }  

These business rules are a bit more complex, since they affect not only the field being updated, but also the "other" field. Notice the use of the features that we built into the SmartDate data type as we check to see if each field is empty, and then perform comparisons to see which is greater than the other. Remember that an empty date can represent either the largest or the smallest possible date, which helps keep our code relatively simple.

The user can easily set either date to be empty by clearing the text field in the Windows Forms or Web Forms UI. Remember that our SmartDate code considers an empty string value to be equivalent to an empty DateTime value.

As with the Name property, the calls to Assert() automatically add and remove entries from the list of broken rules, potentially moving our object into and out of a valid state as the data changes.

We also have a Description property as shown here:

  public string Description     {       get       {         return _description;       }       set       {         if(value == null) value = string.Empty;         if(_description != value)         {           _description = value;           MarkDirty();         }       }      }  

Since this is an optional text field that can contain almost unlimited amounts of text, there's no real need for any business validation in this property.

The final business property we need to implement in this region provides client code with access to our child objects (this code will initially be displayed as an error because we haven't yet implemented the child collection).

  public ProjectResources Resources     {       get       {         return _resources;       }     }  

We don't want the client code to be able to replace our collection object with a new one, so this is a read-only property. Because the collection object itself will be editable, however, the client code will be able to add and remove child objects as appropriate.

Overriding IsValid and IsDirty

We need to do one last bit of work before moving on to the remaining code regions . Since this is a parent object that has a collection of child objects, the default behavior for IsValid and IsDirty won't work.

Note 

The default IsValid and IsDirty properties must be enhanced for all objects that subclass BusinessBase and contain child objects.

A parent object is invalid if its own variables are in an invalid state or if any of its child objects is in an invalid state. Likewise, a parent object is dirty if its own data has been changed, or if any of its child objects or collections have been changed. To handle this properly, we need to override the IsValid and IsDirty properties, and provide a slightly more sophisticated implementation of each, as shown here:

  public override bool IsValid      {        get        {          return base.IsValid && _resources.IsValid;        }      }      public override bool IsDirty      {        get        {          return base.IsDirty  _resources.IsDirty;        }      }  

In both cases, we first see if the Project object itself is invalid or dirty. If so, we can simply return true . Otherwise , we check the child collection to see if it (and by extension, any of its child objects) is invalid or dirty. If base.IsValid returns false , there's no need to go through all the work of checking the child objectswe can simply return false .

System.Object Overrides

To be good .NET citizens , our objects should at least override the ToString() method and provide a meaningful implementation of the Equals() method. These are placed in our System.Object Overrides region as follows:

  #region System.Object Overrides     public override string ToString()     {       return _id.ToString();     }     public new static bool Equals(object objA, object objB)     {       if(objA is Project && objB is Project)         return ((Project)objA).Equals((Project)objB);       else         return false;     }     public override bool Equals(object project)     {       if(project is Project)         return Equals((Project)project);       else         return false;     }     public bool Equals(Project project)     {       return _id.Equals(project.ID);     }     public override int GetHashCode()     {       return _id.GetHashCode();     }     #endregion  

Since a Project object's unique identifying field is its GUID, we can return that value in ToString() , thus providing a meaningful text value that identifies our object. Likewise, to compare two Project objects for equality, all we really need to do is see if their GUIDs match. If they do, then the two objects represent the same project.

static Methods

We can now move on to create our static factory methods, as shown here:

  #region static Methods  // create new object     public static Project NewProject()     {  return (Project)DataPortal.Create(new Criteria(Guid.Empty));  }     // load existing object by id  public static Project GetProject(Guid id)     {       return (Project)DataPortal.Fetch(new Criteria(id));     }     // delete object     public static void DeleteProject(Guid id)     {       DataPortal.Delete(new Criteria(id));     }     #endregion  

We need to support the creation of a brand-new Project object. This can be done in two ways: either by calling DataPortal.Create() to load default values from the database, or by using the new keyword to create the object directly.

Though in our application our Project objects won't load any defaults from the database, we've nevertheless implemented the NewProject() method so that it calls DataPortal.Create() , just to illustrate how it's done. Since the DataPortal.Create() method requires a Criteria object, we create one with a dummy GUID. When we implement our DataPortal_Create() method, we'll load the object with a set of default values.

We also implement the GetProject() static factory method to retrieve an existing Project object, which is populated with data from the database. This method simply calls DataPortal.Fetch() , which in turn ends up creating a new Project object and calling its DataPortal_Fetch() method to do the actual data access. The Criteria object is passed through this process, so our data-access code will have access to the GUID key.

Finally, we include a static method to allow immediate deletion of a project. Our Project object also supports deferred deletion (where the client code calls the Delete() method on the object, then calls the Save() method to update the object and complete the deletion process), but this static method provides for a more direct approach. The client code provides the GUID key, and the project's data is removed from the database directly, without the overhead of having to retrieve the object first.

Constructors

As we noted earlier, all business objects must include a default constructor, as shown here:

  #region Constructors       private Project()       {         // prevent direct instantiation       }       #endregion  

This is straight out of the template we discussed earlier in the chapter. It ensures that client code must use our static factory methods to create or retrieve a Project object, and it provides the DataPortal with a constructor that it can call via reflection.

Criteria

Next, let's create our Criteria class. The unique identifier for a project, in both the database and our object model, is its GUID ID value. Since the GUID value will always identify a single project, it's the criterion that we'll use to retrieve a Project object. Add it in the Criteria region:

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

As we've discussed before, the sole purpose of a Criteria object is to pass data from the client to the DataPortal in order to select the right object. To make our static factory methods easier to implement, we include a constructor that accepts the criterion as a parameter. To make a Criteria object easy to use during the data creation, retrieval, and delete processes, we store the criterion value in a public variable that can be directly accessed by our data-access code.

At this point, we've completed the business properties and methods, and the static factory methods. All that remains is to implement the data-access logic so that the object can be created, retrieved, updated, and deleted.

Data Access

In the Data Access region, we'll implement the four DataPortal_xyz() methods that support the creation, retrieval, updating, and deletion of a Project object's data. In our sample application, the data-access code is relatively straightforward. Keep in mind, however, that these routines could be much more complex, interacting with multiple databases, merging data from various sources, and doing whatever is required to retrieve and update data in your business environment.

For the Project object, we'll make use of Enterprise Services' transactional support by using our [Transactional()] attribute on the DataPortal_Update() and DataPortal_Delete() methods. With the Resource object, we'll go the other way, using the faster ADO.NET transactional support.

Tip 

Remember that using Enterprise Services transactions has a big performance impactour data-access code will run roughly 50 percent slower than when using ADO.NET transactions. If we're updating multiple databases, we must use Enterprise Services to protect our data with transactions. If we're updating a single database (as in our sample application), such transactions aren't required, but they can simplify our codethey free us from dealing manually with ADO.NET transaction objects.

As a general rule, I recommend using manual ADO.NET transactions when updating a single database, because I consider the performance impact of Enterprise Services transactions to be too high.

As we go through this code, notice that we never actually catch any exceptions. We use using blocks to ensure that database connections and data-reader objects are disposed properly, but we don't catch any exceptions. The reasons for this are twofold:

  • First, we're using the [Transactional()] attribute, which causes our code to run within COM+ using autocomplete transactions. An exception causes the transaction to be rolled back, which is exactly what we want to happen. If we caught exceptions, then the transaction wouldn't be rolled back by COM+, and our application would misbehave.

  • Second, if an exception occurs, we don't want normal processing to continue. Instead, we want the client code to know that the operation failed, and why. By allowing the exception to be returned to the client code, we're allowing the client code to know that there was a problem during data access. The client code can then choose how to handle the fact that the object couldn't be created, retrieved, updated, or deleted.

DataPortal_Create Method

The DataPortal_Create() method is called by the DataPortal when we're creating a brand-new Project object that needs to have default values loaded from the database. Though our example is too simple to need to load such values, we're implementing this scheme to illustrate how it would work. The DataPortal_Create() implementation here simply loads some default, hard-coded values rather than talking to the database:

  #region Data Access     // called by DataPortal so we can set defaults as needed     protected override void DataPortal_Create(object criteria)     {       // for this object the criteria can be ignored on creation       _id = Guid.NewGuid();       Started = DateTime.Today.ToShortDateString();       Name = String.Empty;     }     #endregion  

There are a couple of things to notice here. First, we're directly altering the instance variable of our object's ID value by setting _id to a new GUID value. Since the ID property is read-only, this is the only way to load the ID property with a new value.

However, the Started and Name properties are set by their property accessors. This allows us to take advantage of any business validation that exists in those methods. For instance, in this case the Name property is required; so by setting the empty value into the property, we've triggered our business-validation code, and our object is now marked as being invalid by the framework's BrokenRules functionality.

Tip 

In a more complex object, where default values come from the database, this method would contain ADO.NET code that retrieved those values and used them to initialize the object's variables. The implementation shown here isn't ideal, because we're incurring the overhead of the DataPortal to initialize values that could have been initialized in a constructor method. We'll see an example of that implementation when we create the Resource class.

DataPortal_Fetch Method

More interesting and complex is the DataPortal_Fetch() method, which is called by the DataPortal to tell our object that it should load its data. We get a Criteria object as a parameter, which contains the criteria data we need to identify the data to load.

  // called by DataPortal to load data from the database     protected override void DataPortal_Fetch(object criteria)     {       // retrieve data from db       Criteria crit = (Criteria)criteria;       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           cm.CommandText = "getProject";           cm.Parameters.Add("@ID", crit.ID);  using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))  {             dr.Read();             _id = dr.GetGuid(0);             _name = dr.GetString(1);             _started = dr.GetSmartDate(2, _started.EmptyIsMin);             _ended = dr.GetSmartDate(3, _ended.EmptyIsMin);             _description = dr.GetString(4);             // load child objects             dr.NextResult();             _resources = ProjectResources.GetProjectResources(dr);           }           MarkOld();         }       }     }  

We use the DB() method from our base class to retrieve the database connection string and open a connection to the database as follows:

 using(SqlConnection cn = new SqlConnection(DB("PTracker"))) 

Then, within a using block, we initialize and execute a SqlCommand object to call our getProject stored procedure as shown here:

 using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           cm.CommandText = "getProject";           cm.Parameters.Add("@ID", crit.ID);           using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader())) 

The interesting thing here is that we're creating and using an instance of our SafeDataReader class, rather than the normal SqlDataReader . This way, we get automatic protection from errant null values in our data, and we also get support for the SmartDate data type.

Once we've got the data-reader object, we use its data to populate our object's variables like this:

 _id = dr.GetGuid(0);           _name = dr.GetString(1);           _started = dr.GetSmartDate(2, _started.EmptyIsMin);           _ended = dr.GetSmartDate(3, _ended.EmptyIsMin);           _description = dr.GetString(4); 

Since we're using a SafeDataReader , we can use its GetSmartDate() method to retrieve SmartDate values rather than simple date values, thereby automatically handling the translation of a null value into the appropriate empty date value.

Also notice that we're using numeric indexing to get the appropriate column from the data-reader object. Though this reduces the readability of our code, it's the fastest data-access option.

After the Project object's variables are loaded, we also need to create and initialize any child objects. Although we haven't created the child and child collection classes yet, we know the basic structure that we want to follow, which is to call the Fetch() method on the child collection, passing it the data-reader object with the child object data as shown here:

 // load child objects           dr.NextResult();           _resources = ProjectResources.GetProjectResources(dr); 

Remember that our stored procedure returns two result setsthe first with the project's data; the second with the data for the child objects. The NextResult() method of the data reader moves us to the second result set so the child collection object can simply loop through all the rows, creating a child object for each.

The rest of the code simply closes the data-access objects, and also calls MarkOld() . This is important because it changes our object from being a new object to being an old objectan object that reflects a set of data stored in our database. The MarkOld() method also turns the dirty flag to false , indicating that the values in the object haven't been changed, but rather reflect the actual values stored in the database.

DataPortal_Update Method

The DataPortal_Update() method handles adding, updating, and removing Project objects and their child objects. The decision of whether to do an add, an update, or a delete is driven by the IsNew and IsDeleted flags from our base class.

  // called by DataPortal to delete/add/update data into the database     [Transactional()]     protected override void DataPortal_Update()     {  // save data into db       using(SqlConnection cn = new SqlConnection(DB("PTracker")))  {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           if(this.IsDeleted)           {             // we're being deleted             if(!this.IsNew)             {               // we're not new, so get rid of our data               cm.CommandText = "deleteProject";               cm.Parameters.Add("@ID", _id.ToString());               cm.ExecuteNonQuery();             }             // reset our status to be a new object             MarkNew();           }           else           {             // we're not being deleted, so insert or update             if(this.IsNew)             {               // we're new, so insert               cm.CommandText = "addProject";              }              else              {                // we're not new, so update                cm.CommandText = "updateProject";              }              cm.Parameters.Add("@ID", _id.ToString());              cm.Parameters.Add("@Name", _name);              cm.Parameters.Add("@Started", _started.DBValue);              cm.Parameters.Add("@Ended", _ended.DBValue);              cm.Parameters.Add("@Description", _description);              cm.ExecuteNonQuery();              // make sure we're marked as an old object              MarkOld();            }         }       }       // update child objects       _resources.Update(this);     }  

There's a lot going on here, so let's break it down. The method is marked as [Transactional()] , so we don't need to deal manually with transactions in the code itselfit will be running within the context of a COM+ transaction as follows:

  [Transactional()]       protected override void DataPortal_Update()  

This will cause the client-side DataPortal to invoke the ServicedDataPortal on the server automatically, so our code will run within the context of a COM+ transaction. Technically, we could just use ADO.NET or stored procedure transactions for this purpose, but this illustrates how to make a method use COM+ transactions through the CSLA .NET Framework.

Note 

Because this method is marked as [Transactional()] our data-access code must run on a machine that has COM+, which means Windows 2000 or higher.

The first thing we do is find out if the IsDeleted flag is true. If it is, then we know that the object should be deleted from the databasethat is, if it already exists. If the IsNew flag is also set, then we don't need to do any real work, otherwise we call the deleteProject stored procedure as shown here:

 if(this.IsDeleted)         {           // we're being deleted           if(!this.IsNew)           {             // we're not new, so get rid of our data             cm.CommandText = "deleteProject";             cm.Parameters.Add("@ID", _id.ToString());             cm.ExecuteNonQuery();           }           // reset our status to be a new object           MarkNew();         } 

In any case, after the object has been deleted from the database, it no longer reflects data in the database, and so it's technically "new." To signal this, we call the MarkNew() method. If IsDeleted was true , we're all done at this pointthe database connection is closed, and the data-portal mechanism will return our updated object back to the client.

Since the DataPortal mechanism returns our object by value to the client, we don't have to worry about having the DataPortal_Update() method return any values. All it needs to do is update the database and any object variables. Once that's done, the server-side DataPortal will return the object by value to the client-side DataPortal , which returns it to the UI.

If IsDeleted was false , on the other hand, then we know that we're either adding or updating the object. This is determined by checking the IsNew flag and setting ourselves up to call either the addProject or the updateProject stored procedure, as appropriate:

 // we're not being deleted, so insert or update           if(this.IsNew)           {             // we're new, so insert             cm.CommandText = "addProject";           }           else           {             // we're not new, so update             cm.CommandText = "updateProject";            } 

Either way, we add the appropriate parameter values, and execute the stored procedure as follows:

 cm.Parameters.Add("@ID", _id.ToString());           cm.Parameters.Add("@Name", _name);           cm.Parameters.Add("@Started", _started.DBValue);           cm.Parameters.Add("@Ended", _ended.DBValue);           cm.Parameters.Add("@Description", _description);           cm.ExecuteNonQuery(); 

Whether we're adding or updating, we now know that our object represents data stored in the database and that our data fields are the same as those in the database. To mark this, we call MarkOld() , which ensures that both IsNew and IsDirty are false .

The final step in the process, after we've closed the database connection, is to tell our child collection to update the child objects as well.

 // update child objects       _resources.Update(this); 

Since we're using Enterprise Services transactions, the child updates can be handled on their own database connections, which will automatically be enrolled in the same overall transactional context.

DataPortal_Delete Method

If the client code calls our static deletion method, the DataPortal will create a Project object, and then call our DataPortal_Delete() method to delete the appropriate data. A Criteria object is passed as a parameter, so we'll have the GUID key of the project to be deleted. Then, it's just a matter of calling our deleteProject stored procedure as follows:

  [Transactional()]     protected override void DataPortal_Delete(object criteria)     {       Criteria crit = (Criteria)criteria;       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           cm.CommandText = "deleteProject";           cm.Parameters.Add("@ID", crit.ID.ToString());           cm.ExecuteNonQuery();         }       }     }  

Like DataPortal_Update() , this method is marked as [Transactional()] so that it will run within Enterprise Services and be protected by a COM+ transactional context. We just open the database connection, configure the SqlCommand object to call the deleteProject stored procedure, and we're all done.

Security

The final functionality that we need to implement in the Project object is security. According to our use cases in Chapter 6, not all users can add, edit, or remove a Project object. Only a project manager can add or edit a Project , and only project managers or administrators can remove them.

When you think about it, security checks are just another form of business logic. Who can do what with our objects is a business decision that's driven by business requirements, and so these checks must be implemented in our objects.

In reality, these security checks are the last defense. The UI developer should incorporate security checks directly into the UI, and hide (or at least gray out) the buttons , menu options, or links so that users are physically unable to add, update, or delete objects if they aren't in the right role. Sometimes, however, these details are missed in the UI, or a later UI is written without full understanding of the business requirements. In cases like these, making sure to include the security checks in the business objects ensures compliance.

Our CSLA .NET Framework supports either Windows' integrated security or our custom, table-driven security model. Either way, the security implementation relies on the underlying .NET security model, which is based on principal and identity objects. To do role checking, we use the Principal object. Our business code is the same regardless of which type of security is being used behind the scenes.

This means that we can easily enhance the static method that creates a new Project object to do a security check, as shown here:

 // create new object     public static Project NewProject()     {       if(!Thread.CurrentPrincipal.IsInRole("ProjectManager"))  throw new System.Security.SecurityException(  "User not authorized to add a project");       return (Project)DataPortal.Create(new Criteria(Guid.Empty));     } 

If the user isn't in the ProjectManager role, we throw a security exception to indicate that the user can't add a new project. If we're using Windows' integrated security, the user would need to be in a Windows group named ProjectManager . If we're using our custom, table-based security, the user would need to have an entry in the Roles table indicating they have the role of ProjectManager .

Notice that our code here doesn't care which type of security we're usingit relies on the .NET Framework to handle those details. The thread's Principal object could be of type WindowsPrincipal or our own BusinessPrincipal , but we don't care which it is. Both implement the IPrincipal interface, which is what we're relying on in our code.

We do the same thing for the static deletion method, where we need to ensure that the user is either a ProjectManager or an Administrator .

 // delete object     public static void DeleteProject(Guid id)     {  if(!Thread.CurrentPrincipal.IsInRole("ProjectManager") &&         !Thread.CurrentPrincipal.IsInRole("Administrator"))         throw new System.Security.SecurityException("User not authorized to remove a project");  DataPortal.Delete(new Criteria(id));     } 

Controlling the edit process is a bit different. Presumably, all users can view a Project ; it's the update process that needs to be secured. We can check the update process in a couple of places, either by overriding the Save() method, or by putting our checks in the DataPortal_Update() method.

Overriding the Save() method is nice, because it means that the security check occurs before the DataPortal is invokedthe check will occur on the client, and be faster. We need to keep in mind, however, that Save() is called for adding, updating, and deleting the object. Add this override to the static Methods region:

  public override BusinessBase Save()     {       if(IsDeleted)       {         System.Security.Principal.IIdentity user =           Thread.CurrentPrincipal.Identity;         bool b = user.IsAuthenticated;         if(!Thread.CurrentPrincipal.IsInRole("ProjectManager") &&           !Thread.CurrentPrincipal.IsInRole("Administrator"))           throw new System.Security.SecurityException("User not authorized to remove a project");       }       else       {         // no deletion we're adding or updating         if(!Thread.CurrentPrincipal.IsInRole("ProjectManager"))           throw new System.Security.SecurityException("User not authorized to update a project");       }       return base.Save();     }  

We have to implement two different security checks: one if IsDeleted is true , and the other if it's false . This way, we can follow the use-case requirements regarding who can do what. After the security checks are completeand assuming the user met the criteriawe call base.Save() , which performs the normal behavior of the Save() method. By calling the Save() method of the base class, we ensure that the normal checks to ensure that the object is valid and is dirty occur before the update process starts.

This completes the code for the Project class, which now contains business properties and methods, along with their associated business rules. (In a more complex object, we'd have implemented additional business processing such as calculations or other data manipulations.) It also contains the static methods, Criteria class, and constructor method that are required to implement the "class-in-charge" model, and to work with the DataPortal . Finally, it includes the four DataPortal_xyz() methods that the DataPortal will call to create, retrieve, update, and delete our Project objects.

RoleList

Before we get into the creation of the child objects for a Project , we need to create a supporting object that they'll use: the RoleList . This object contains a read-only name-value list of the possible roles that a Resource can hold when assigned to a Project . Because we have a powerful NameValueList base class in the CSLA .NET Framework, this object is quite easy to implement.

Add a new class to the project, and name it RoleList . Then add the following code, based on the template that we discussed earlier.

  using System; using CSLA; namespace ProjectTracker.Library {   [Serializable()]   public class RoleList : NameValueList   {     #region Shared Methods     public static RoleList GetList()     {       return (RoleList)DataPortal.Fetch(new Criteria());     }     #endregion     #region Constructors     private RoleList()     {       // prevent direct creation     }     // this constructor overload is required because     // the base class (NameObjectCollectionBase) implements     // ISerializable     private RoleList(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("PTracker", "Roles", "id", "name");     }     #endregion   } }  

Since the names and values for this object are coming from a single table in our database ( Roles ), we can make use of the SimpleFetch() method in the NameValueList base class, providing it with the name of the database, the name of the table, and the two columns from which to read the data:

 SimpleFetch("PTracker", "Roles", "id", "name"); 

The base class takes care of the rest of the work, so we're all done.

Resource

Our other primary root object is Resource . Like Project , a Resource object can be directly created, retrieved, or updated. It also contains a list of child objects.

Since we've already walked through the creation of an editable root business object in detail, there's no need to do the same for the Resource class. Simply get hold of the Resource.cs code from the download, and add it to the project using the Add image from book Add Existing Item option. However, there are two aspects that we need to examine.

Object Creation

The first process we'll consider is the creation of a new Resource object. When we created the Project class, we called DataPortal.Create() to initialize the object with default values, illustrating how this technique would allow such values to be loaded from a database. For many other objects, however, the default values are simply the empty values of the underlying data typean empty string for a string variable, for instance. In other cases, we can simply hard-code the defaults into our code. In these last two scenarios, we don't need to invoke DataPortal.Create() , and we can use a more efficient technique.

In the static Methods region of the Resource class, we have a NewResource() method that doesn't call DataPortal.Create() , but rather creates an instance of the Resource class directly, as follows:

 // create new object     public static Resource NewResource(string id)     {       if(!Thread.CurrentPrincipal.IsInRole("Supervisor") &&         !Thread.CurrentPrincipal.IsInRole("ProjectManager"))         throw new System.Security.SecurityException("User not authorized to add a resource");       return new Resource(id);     } 

Notice that we're calling a constructor that accepts parameters, rather than the default constructor. This not only allows us to provide criteria data to the creation process, but also keeps the construction of a new object with default values separate from the default construction process used by DataPortal . The constructor sets the default values of the object as it's created.

 #region Constructors  private Resource(string id)     {       _id = id;     }  private Resource()     {       // prevent direct instantiation     }     #endregion 

These values can't come from the database like those in a DataPortal_Create() method, but we can provide any hard-coded or algorithmic default values here.

Data Access

The other thing we need to look at is data access. In the Project class, we used the [Transactional()] attribute for our data access, which provides for simple code that's capable of updating multiple databases within a transaction, but at a substantial performance cost. If we're only updating a single database, it's often preferable to use ADO.NET transactions instead. They provide much better performance, and require only slightly different coding.

Tip 

Details on the performance difference between manual ADO.NET transactions and Enterprise Services transactions can be found in the MSDN Library. [2]

Table 7-1 may help you to determine whether to use Enterprise Services (COM+) transactions or ADO.NET manual transactions.

Table 7-1: Decision Chart for ADO.NET vs. Enterprise Services Transactions

Criteria

Enterprise Services

ADO.NET

Updating two or more databases

Yes

No

Simpler code

Yes

No

Optimal performance

No

Yes

Requires Windows 2000 or higher

Yes

No

Since we've already implemented code using Enterprise Services and our [Transactional()] attribute in the Project class, for the Resource class we'll demonstrate the use of ADO.NET manual transactions.

To implement ADO.NET manual transactions, we need to call the BeginTransaction() method on our open-database connection object. This will return a SqlTransaction object representing the transaction. If we get through all the database updates without any exceptions, we need to call the Commit() method on the SqlTransaction object; otherwise we need to call the Rollback() method.

The BeginTransaction() method of the SqlConnection object optionally takes a parameter to indicate the level of isolation for our transaction, which we should specify appropriately for our application requirements. The possible IsolationLevel values are shown in Table 7-2.

Table 7-2: SqlConnection Isolation Level Options

Isolation Level

Description

Chaos

The pending changes from more highly isolated transactions cannot be overwritten.

ReadCommitted

Static locks are held while the data is being read to avoid dirty reads, but the data can be changed before the end of the transaction, resulting in nonrepeatable reads or phantom data.

ReadUncommitted

A dirty read is possible, meaning that no shared locks are issued, and no exclusive locks are honored.

RepeatableRead

Locks are placed on all data that's used in a query, thereby preventing other users from updating the data. Prevents nonrepeatable reads, but phantom rows are still possible.

Serializable

A range lock is placed on the data, thereby preventing other users from updating or inserting rows into the dataset until the transaction is complete.

Unspecified

A different isolation level than the one specified is being used, but the level cannot be determined.

Each option provides a different level of isolation, thereby ensuring that the data we read or update is more or less isolated from other concurrent users of the system. The more isolated our transaction, the more expensive it will be in terms of performance and database server resources.

The typical default isolation level is ReadCommitted , though if we're using the OdbcTransaction object, the default will depend on the underlying ODBC driver's default, and that can vary from driver to driver. The Serializable isolation level provides complete safety when updating data, because it ensures that no one can interact with data that we're updating until our transaction is complete. This is often desirable, though it can reduce performance and increase the chance of database deadlocks in higher volume applications.

DataPortal_Fetch Method

When we read data, we often don't think about using transactions. However, if we're concerned that another concurrent user might change the data we're reading while we're in the middle of doing the read operation, then we should use a transaction to protect our data retrieval. In many applications, this isn't a substantial concern, but we'll use such a transaction here to illustrate how it works.

The structure of DataPortal_Fetch() with manual transactions is very similar to the DataPortal_Fetch() that we implemented for Project .

 // called by DataPortal to load data from the database     protected override void DataPortal_Fetch(object criteria)     {       Criteria crit = (Criteria)criteria;       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         SqlTransaction tr =           cn.BeginTransaction(IsolationLevel.ReadCommitted);          try          {            using(SqlCommand cm = cn.CreateCommand())            {              cm.Transaction = tr;              cm.CommandType = CommandType.StoredProcedure;              cm.CommandText = "getResource";              cm.Parameters.Add("@ID", crit.ID);              using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))              {                dr.Read();                _id = dr.GetString(0);                _lastName = dr.GetString(1);                _firstName = dr.GetString(2);                // load child objects                dr.NextResult();                _assignments = ResourceAssignments.GetResourceAssignments(dr);              }            }            MarkOld();            tr.Commit();          }          catch          {            tr.Rollback();            throw;          }        }      } 

We declare a SqlTransaction variable, and then create a transaction after the database connection has been opened:

 tr = cn.BeginTransaction(IsolationLevel.ReadCommitted); 

In this case, we're opting for minimal transactional protection, but this will prevent any dirty readsthat is, where a row of data is in the middle of being changed while we read that same data. If we need to read some data that's currently being updated, our query will block until a clean read of the data is possible.

Each command object must be linked to the transaction object before it's executed, as follows:

 cm.Transaction = tr; 

Failure to do this will result in a runtime error, since all command objects associated with our database connection object must have an associated transaction, as a result of the BeginTransaction() call.

The rest of the code to set up and execute the command is very typical ADO.NET code. However, notice that we call the Commit() method of the transaction object if everything's going well, and we have a Catch block that calls Rollback() in the event of an exception.

 tr.Commit();         }         catch         {           tr.Rollback();           throw;         } 

The key here is that our call to the Fetch() method of our child collection is inside the try block, meaning that retrieval of the child objects is protected by the same transaction as the Resource data itself.

Also notice that the catch block not only rolls back the transaction, but also rethrows the exception. This allows client code to get the exception so that it knows what went wrong, and can take appropriate steps. It also provides the client code with exactly the same behavior as with a COM+ transactional implementationthe client code has no idea (and doesn't care) whether a COM+ transaction or manual transaction was used.

Our child object code, on the other hand, will need access to this same database connection and transaction object in order to ensure that the child-database updates are handled within the same transaction. We'll see how this works later when we implement the ResourceAssignment class.

DataPortal_Update Method

The DataPortal_Update() method of the Resource class works in very similar fashion. Again, we begin a transaction and ultimately commit or roll back that transaction depending on whether an exception occurs. The big difference is that we must pass the SqlTransaction object to the Update() method of our child collection, so that each child object can link its SqlCommand objects to this transaction. We'll see how that's done from the child's point of view when we implement the Assignment class shortly.

The salient bits of code in this method include the creation of the SqlTransaction object and link it to the SqlCommand object for the Resource update, as shown here:

 cn.Open();  SqlTransaction tr = cn.BeginTransaction(IsolationLevel.Serializable);  try         {           using(SqlCommand cm = cn.CreateCommand())           {  cm.Transaction = tr;  

In this case, we're using a higher level of isolation to protect our update process against collisions with other users more carefully .

After we update the core Resource data, we call the Update() method on the child collection, passing the SqlTransaction object as a parameter as follows:

 // update child objects           _Assignments.Update(tr, this); 

This code is inside the try block, so if any child object throws an exception, we can catch it and roll back the transaction.

The try catch block itself exists so that we call either the Commit() or the Rollback() method on the transaction, just like we did in DataPortal_Fetch() .

  tr.Commit();  }         catch         {  tr.Rollback();  throw;         } 

The end result is that with very little extra code, we've implemented a manual transaction scheme that will run nearly twice as fast as if we'd used COM+ transactions. It's only applicable if we're working with a single database, but it's an option that should be seriously considered due to the performance benefits it brings .

DataPortal_Delete Method

The same changes from the Project code apply to the simpler DataPortal_Delete() method. Again, we start the transaction and link our SqlCommand object to the SqlTransaction object as shown here:

 cn.Open();  SqlTransaction tr = cn.BeginTransaction(IsolationLevel.Serializable);  try         {           using(SqlCommand cm = cn.CreateCommand())           {  cm.Transaction = tr;  

We then either commit the transaction, or in the case of an exception we roll it back within a Catch block as follows:

  tr.Commit();       }       catch       {         tr.Rollback();         throw;       }  

Again, with very little extra code, we've used manual transactions instead of using COM+ transactions.

Assignment

The child object of a Project is a ProjectResource . The ProjectResource object contains read-only information about a Resource that's been assigned to the Project as well as basic information about the assignment itself.

The assignment information includes the date of assignment, and the role that the resource will play in the project. This information is common to both the ProjectResource and ResourceAssignment business objects, and as we discussed in Chapter 6 when we were creating our object model, this is an appropriate place to use inheritance. We can create an Assignment class that contains the common functionality, and we can then reuse this functionality in both ProjectResource and ResourceAssignment . Add a new class to the project, and name it Assignment .

This class is an implementation of an editable child object, so it will inherit from BusinessBase , but it will call MarkAsChild() during creation so that it will operate as a child object.

We'll go through most of this code relatively quickly, since the basic structure was discussed earlier in the chapter, and we've already walked through the creation of two business objects in some detail. First, let's import the data namespace, set up the class properly, and declare our variables.

  using System; using System.Data.SqlClient; using CSLA; namespace ProjectTracker.Library {   [Serializable()]   public abstract class Assignment : BusinessBase   {     protected SmartDate _assigned = new SmartDate(DateTime.Now);     protected int _role = 0;   } }  

The class is declared using the abstract keyword. Client code can never create an Assignment objectonly the derived ProjectResource and ResourceAssignment objects can be instantiated .

The variables are all assigned values as they are declared. The _assigned variable is a SmartDate , and is given the current date. Notice that the variables are declared as protected , which allows them to be accessed and modified by ProjectResource and ResourceAssignment objects at runtime.

Only the values that are common across both ProjectResource and ResourceAssignment are declared here. The other values that are specific to each type of child class will be declared in the respective child classes when we create them later.

Handling Roles

Each assignment of a Resource to a Project has an associated Role , and this value must be one of those from the RoleList object that we just created. In order to ensure that the Role value is in this list, our Assignment objects will need access to the RoleList .

However, we don't want to load the RoleList from the database every time we create a new Assignment object. Since the list of roles won't change very often, we should be able to use a caching mechanism to keep a single RoleList object on the client during the lifetime of our application.

Tip 

If our underlying data changes periodically, we may be unable to use this type of caching approach. Alternatively, we can use a Timer object to refresh the cache periodically.

One approach to this is to use the System.Web.Caching namespace, which provides powerful support for caching objects in .NET. This is the technology used by ASP.NET to implement page caching, and we can tap into it from our code as well. However, there's an easier solution available to us in this case: We can simply create a static variable in the Assignment class to store a RoleList object. This RoleList object will then be available to all Assignment objects in our application, and it will only be loaded once, when the application first accesses the RoleList object.

Although it's not as sophisticated as using the built-in .NET caching infrastructure, this approach is simple to code, and works fine for our purposes. We'll put this code in a special region to keep it separate from the rest of our work.

  #region Roles List     private static RoleList _roles;     static Assignment()     {       _roles = RoleList.GetList();     }     public static RoleList Roles     {       get       {         return _roles;       }     }     protected static string DefaultRole     {       get       {         // return the first role in the list         return Roles[0];       }     }     #endregion  

We're using a static constructor method to retrieve the RoleList object as shown here:

 static Assignment()       {         _roles = RoleList.GetList();       } 

The static constructor method is always called before any other method on the Assignment class (or an Assignment object). Any interaction with the Assignment class, including the creation of an Assignment object, will cause this method to be run first. This guarantees that we'll have a populated RoleList object before any other Assignment class code is run.

We then expose the RoleList object by implementing a Roles property. Since this property is static , it's available to any code in our application, and in any Assignment objects. We have basically created a globally shared cache containing the list of valid roles.

We also expose a DefaultRole() property that returns the name of the first role in the RoleList object. This will be useful as we create a user interface, since it provides a default value for any combo box (or other list control) in the UI.

Business Properties and Methods

Next, we can create the business functionality for an Assignment object.

  #region Business Properties and Methods     public string Assigned     {       get       {         return _assigned.Text;       }     }     public string Role     {       get       {         return Roles[_role.ToString()];       }       set       {         if(value == null) value = string.Empty;         if(Role != value)         {           _role = Convert.ToInt32(Roles.Key(value));            MarkDirty();         }       }     }     #endregion  

The only editable field here is the Role property. The Assigned property is set as the object is first created, leaving only the Role property to be changeable . In fact, Role is quite interesting, since it validates the user's entry against the RoleList object we're caching, as shown here:

 set       {         if(value == null) value = string.Empty;         if(Role != value)         {           _role = Convert.ToInt32(Roles.Key(value));           MarkDirty();         }       } 

The client code has no idea that the Role value is an integerall the client or UI code sees is that this is a text value. The UI can populate a combo box (or any other list control) with a list of the roles from the RoleList object, and as the user selects a text value from the list, it can be used to set the value of our property.

Inside the set block, we attempt to convert that text value to its corresponding key value by retrieving the Name from the RoleList object. If this succeeds, we'll have the key value; if it fails, it will raise an exception indicating that the client code provided us with an invalid text value. We get both validation and name-value translation with very little code.

Constructors

Since Assignment is a child object, it doesn't have public methods to allow creation or retrieval. In fact, this is a base class that we can't create an instance of directly at all. However, we'll implement a constructor method that marks this as a child object.

  #region Constructors       protected Assignment()       {         MarkAsChild();       }       #endregion  

This way, our ProjectResource and ResourceAssignment classes don't need to worry about this detail. Since this is the default constructor, it will automatically be invoked by the .NET runtime as the objects are created.

ProjectResource

With the base Assignment class built, we can move on to create the child class for the Project object: ProjectResource . A ProjectResource is an Assignment , plus some read-only values from the Resources table for display purposes. ProjectResource objects will be contained in a child collection object, which we'll create shortly.

We won't walk through the creation of this class, which you'll find in the code download as ProjectResource.cs . Add it as an existing item, and we'll then look at its most important features. First of all, since it inherits from Assignment (and therefore, indirectly, from BusinessBase ), ProjectResource starts out with all the data and behaviors we built into the Assignment class. All we need to do is add the data items specifically required for a child of a Project .

 using System; using System.Data; using System.Data.SqlClient; using CSLA; using CSLA.Data; namespace ProjectTracker.Library {   [Serializable()]   public class ProjectResource : Assignment   {  string _resourceID = string.Empty;     string _lastName = string.Empty;     string _firstName = string.Empty;  
Business Properties and Methods

We then implement business properties and methods that return these specific values, as shown here:

 #region Business Properties and Methods     public string ResourceID     {       get       {          return _resourceID;       }     }     public string LastName     {       get       {         return _lastName;       }     }     public string FirstName     {       get       {         return _firstName;       }     }     #endregion 

Remember that these values must be read-only in this object. The data values "belong" to the Resource object, not to this object. We're just " borrowing " them for display purposes.

Since the ProjectResource object provides Resource information, it's a good idea to have it provide an easy link to the actual Resource object. This can be done by including a GetResource() method in our Business Properties and Methods region, as follows:

 public Resource GetResource()     {       return Resource.GetResource(_resourceID);     } 

This method is easy to implement, since our ProjectResource object already has the ID value for the Resource object. All we need to do is call the static factory method on the Resource class to retrieve the appropriate Resource object.

Implementing this method simplifies life for the UI developer. This way, the UI developer doesn't need to grab the right ID value and manually retrieve the object. More importantly, this method directly and specifically indicates the nature of the uses relationship between a ProjectResource and its related Resource object. We have just implemented a form of self-documentation in our object model.

System.Object Overrides

Because this object represents a specific Resource that has been assigned to our Project , we can implement ToString() to return some useful informationin this case, the name of the Resource . Also, we should always overload the Equals() method to handle our specific class, as shown here:

 #region System.Object Overrides     public override string ToString()     {       return _lastName + ", " + _firstName;     }     public new static bool Equals(object objA, object objB)     {       if(objA is ProjectResource && objB is ProjectResource)         return ((ProjectResource)objA).Equals((ProjectResource)objB);       else         return false;     }     public override bool Equals(object projectResource)     {       if(projectResource is ProjectResource)         return Equals((ProjectResource)projectResource);       else         return false;     }     public bool Equals(ProjectResource assignment)     {       return _resourceID == assignment.ResourceID;     }     public override int GetHashCode()     {       return _resourceID.GetHashCode();     }     #endregion 

Because a ProjectResource object basically represents a specific Resource assigned to our project, the test of whether it's equal to another ProjectResource object is whether the objects have the same ResourceID value.

static Methods

The Assignment class was marked as Abstract , so there was no way to create an Assignment object directly. Our ProjectResource class can be used to create objects, however, so it must provide appropriate methods for this purpose.

We could directly implement internal constructor methods that can be called by the parent collection object. In the interests of consistency, however, it's better to use the static factory method scheme that's used in our other business objects, so that the creation of any business object is always handled the same way.

When a ProjectResource object is created, it's being created to reflect the fact that a Resource is being assigned to the current Project object to fill a specific role. This should be reflected in our factory methods.

We have a static factory method that exposes this functionality to the parent collection object, as shown here:

 internal static ProjectResource NewProjectResource(Resource resource, string role)     {       return new ProjectResource(resource, role);     } 

There are a couple of variations on this theme that might be useful as well.

 internal static ProjectResource NewProjectResource(string resourceID, string role)     {       return ProjectResource.NewProjectResource(Resource.GetResource(resourceID), role);     }     internal static ProjectResource NewProjectResource(string resourceID)     {       return ProjectResource.NewProjectResource(Resource.GetResource(resourceID), DefaultRole);     } 

These two implementations might be viewed as optional. After all, the parent collection object could include the code to retrieve the appropriate Resource object to pass in as a parameter. It's good practice, however, for our business objects to provide this type of flexible functionality. When we implement the ProjectResources collection object, we'll see how these methods are used, and why they're valuable .

We also implement a factory method that can be used by our parent object during the fetch process. In that case, our parent Project object will have created a data reader with our data that will be passed to the ProjectResources collection as a parameter. The ProjectResources collection loops through the data reader, creating a ProjectResource object for each row of data. We're passed the data reader, with its cursor pointing at the row of data that we should use to populate this object, as shown here:

 internal static ProjectResource GetProjectResource(SafeDataReader dr)     {       ProjectResource child = new ProjectResource();       child.Fetch(dr);       return child;     } 

We'll come to the Fetch() method implementation momentarilyit will be responsible for actually loading our object's variables with data from the data reader.

Constructors

Like all our constructors, these are scoped as private . The actual creation process occurs with the static factory methods, as follows:

 #region Constructors     private ProjectResource(Resource resource, string role)     {       _resourceID = resource.ID;       _lastName = resource.LastName;       _firstName = resource.FirstName;       _assigned.Date = DateTime.Now;       _role = Convert.ToInt32(Roles.Key(role));     }     private ProjectResource()     {        // prevent direct creation of this object     }     #endregion 

The first constructor accepts both a Resource object and a Role , which are then used to initialize the ProjectResource object with its data. These parameters directly reflect the business process that we're emulating, whereby a resource is being associated with our project.

In both cases, the constructors are declared as private to prevent our class from being instantiated directly.

Data Access

As with any editable business class, we must provide methods to load our data from the database, and update our data into the database.

In the case of ProjectResource , we retrieve not only the core assignment data, but also some fields from the Resources table. We get this data via a preloaded data-reader object that was created by the DataPortal_Fetch() method in the Project object. Our code simply pulls the values out of the current row from the data reader, and puts them into the object's variables, as follows:

 #region Data Access     private void Fetch(SafeDataReader dr)     {       _resourceID = dr.GetString(0);       _lastName = dr.GetString(1);       _firstName = dr.GetString(2);       _assigned = dr.GetSmartDate(3);       _role = dr.GetInt32(4);       MarkOld();     }     #endregion 

This method is scoped as private because it's called by our static factory method, rather than directly by the parent collection object.

There's no transaction handling, database connections, or anything complex here at all. We're provided with a data reader that points to a row of data, and we simply use that data to populate our object.

Since the object now reflects data in the database, it meets the criteria for being an "old" object, so we call the MarkOld() method that sets IsNew and IsDirty to false .

When a Project object is updated, its DataPortal_Update() method calls the Update() method on its child collection object. The child collection object's Update() method will loop through all the ProjectResource objects it contains, calling each object's Update() method in turn. In order to ensure that the child object has access to the parent object's ID value, the parent Project object is passed as a parameter to the Update() method.

The Update() method itself must open a connection to the database and call the appropriate stored procedure to delete, insert, or update the child object's data. We don't need to worry about transactions or other object updates, because those details are handled by Enterprise Services. All we need to do is ensure that any exceptions that occur aren't trappedthat way, they'll trigger an automatic rollback by COM+.

In general, this Update() method is structured like the DataPortal_Update() method we created in the Project object, as you can see here:

 internal void Update(Project project)      {        // if we're not dirty then don't update the database        if(!this.IsDirty)          return;       // do the update       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           if(this.IsDeleted)           {             if(!this.IsNew)             {               // we're not new, so delete               cm.CommandText = "deleteAssignment";               cm.Parameters.Add("@ProjectID", project.ID);               cm.Parameters.Add("@ResourceID", _resourceID);               cm.ExecuteNonQuery();               MarkNew();             }           }           else           {             // we're either adding or updating             if(this.IsNew)             {               // we're new, so insert               cm.CommandText = "addAssignment";             }             else             {               // we're not new, so update               cm.CommandText = "updateAssignment";             }             cm.Parameters.Add("@ProjectID", project.ID);             cm.Parameters.Add("@ResourceID", _resourceID);             cm.Parameters.Add("@Assigned", _assigned.DBValue);             cm.Parameters.Add("@Role", _role);             cm.ExecuteNonQuery();             MarkOld();           }         }        }      } 

Note that we don't use the [Transactional()] attribute herethat attribute only has meaning on a DataPortal_xyz() method of a root object. Our Update() method will automatically run in the transaction started by DataPortal_Update() in the Project object.

In this method, we first see if the object's data has changed. If it's not dirty, we don't need to update the database at all. If it is dirty, we open a connection to the database, and proceed to call the appropriate stored procedure.

If IsDeleted is true and IsNew is false , we call the deleteAssignment stored procedure to delete the Assignment data. Otherwise, we call either addAssignment or updateAssignment , depending on the value of IsNew .

In the end, we've deleted, added, or updated the data for this particular Assignment object.

ResourceAssignment

The ResourceAssignment class provides us with the child objects for our Resource object. These objects are the counterparts to the child objects of a Project , and they also inherit from the Assignment base class.

Tip 

The code for the ResourceAssignment class is supplied in the code download.

The architecture of the ResourceAssignment class is virtually identical to that of ProjectResource . The difference is that a ResourceAssignment class contains read-only data from the Projects table, in addition to the core assignment data.

Because it reflects a Project that's associated with the current Resource , it includes a GetProject() method that provides comparable functionality to the GetResource() method in the ProjectResource class.

 public Project GetProject()     {       return Project.GetProject(_projectID);     } 

The class also includes specific static factory methods for creating the child object. In this case, we're a child of a Resource object, and we need to provide the Project object and Role to which this Resource will be assigned. This is reflected in the constructor, as shown here:

 private ResourceAssignment(Project project, string role)     {       _projectID = project.ID;       _projectName = project.Name;       _assigned.Date = DateTime.Now;       _role = Convert.ToInt32(Roles.Key(role));     } 

This is a mirror image of the constructor in the ProjectResource class, and there are corresponding static factory methods as welljust like in ProjectResource .

Data Access

Also, we have a Fetch() method that accepts a data-reader object pointing to the row that contains our data. This data reader comes from the DataPortal_Fetch() method of the Resource object:

 #region Data Access     private void Fetch(SafeDataReader dr)     {       _projectID = dr.GetGuid(0);       _projectName = dr.GetString(1);       _assigned.Date = dr.GetDateTime(2);       _role = dr.GetInt32(3);       MarkOld();     }     #endregion 

Once again, this is basically the same as ProjectResource , but with slightly different data.

Predictably, we also have an Update() method, but let's look at this in a bit more detail (we're using manual ADO.NET transactions rather than COM+ transactions, which makes things just a bit different). The vast majority of the code is similar to that in ProjectResource , but we assume responsibility for managing the transaction, just like we did in the Resource class.

In this case, the Resource object's DataPortal_Update() opens the connection to the database and starts a transaction on that connection. This means that we have a SqlTransaction object that represents the transaction and connection. It's passed to the child collection's Update() method, which loops through all the child ResourceAssignment objects, passing the transaction object to each child object in turn.

A reference to the parent Resource object is also passed to the Update() method, so that we have access to the parent object's data values as needed. In our particular case, we need the ID value from the Resource object, but by passing the entire object as a parameter we ensure that neither Resource nor the ResourceAssignments collection knows what data we're using. This is good because it means our objects are more loosely coupled .

As we'll see when we implement the DataPortal_Update() method in the Resource class, the transaction is committed or rolled back based on whether an exception occurs during the update process. If no exception occurs, the transaction is committed; otherwise, it's rolled back. This means that the Update() methods in our child objects don't need to worry about committing or rolling back the transactionthey just need to do the update and allow any exceptions to flow back up to DataPortal_Update() to cause a rollback to occur.

This keeps the structure of the method very simple, and makes it virtually identical to the COM+ approach. The biggest difference is that we don't open or close the database connection in a child object, because we passed the transaction in. The state of the database is therefore handled by the root Resource object as shown here:

 internal void Update(SqlTransaction tr, Resource resource)     {       // if we're not dirty then don't update the database       if(!this.IsDirty)         return;       // do the update       using(SqlCommand cm = tr.Connection.CreateCommand())       {         cm.Transaction = tr;         cm.CommandType = CommandType.StoredProcedure;         if(this.IsDeleted)         {           if(!this.IsNew)           {             // we're not new, so delete             cm.CommandText = "deleteAssignment";             cm.Parameters.Add("@ProjectID", _projectID);             cm.Parameters.Add("@ResourceID", resource.ID);             cm.ExecuteNonQuery();             MarkNew();           }         }         else         {           // we're either adding or updating           if(this.IsNew)           {             // we're new, so insert             cm.CommandText = "addAssignment";           }           else           {             // we're not new, so update             cm.CommandText = "updateAssignment";           }           cm.Parameters.Add("@ProjectID", _projectID);           cm.Parameters.Add("@ResourceID", resource.ID);           cm.Parameters.Add("@Assigned", _assigned.DBValue);           cm.Parameters.Add("@Role", _role);           cm.ExecuteNonQuery();           MarkOld();         }       }     } 

The SqlTransaction object that we're passed provides us with the connection to use, and allows us to link our SqlCommand object with the transaction as shown here:

  using(SqlCommand cm = tr.Connection.CreateCommand())  {  cm.Transaction = tr;  cm.CommandType = CommandType.StoredProcedure; 

The code to call the deleteAssignment , addAssignment , or updateAssignment stored procedures is the same as in the previous implementation, but notice that we don't close the database connection. That must remain open so that the remaining child objects can use that same connection and transaction. The connection will be closed by the code in the root Resource object's DataPortal_Update() method, which also deals with committing or rolling back the transaction.

Again, in a real-world application, you'd pick either the COM+ or the manual transaction approach. In other words, you'd write only one of these implementations, rather than both of them.

ProjectResources

At this point, we have our Project class, and we have its child ProjectResource class. The only thing we're missing is the child collection class that manages the ProjectResource objects. We'll also have a similar collection class for the ResourceAssignment objects.

Because this is our first business collection class, we'll walk through it in some detail. ProjectResources starts out like any business class, importing the data namespace, being marked as [Serializable()] , and inheriting from the appropriate CSLA .NET base class as follows:

  using System; using CSLA; using CSLA.Data; namespace ProjectTracker.Library {   [Serializable()]   public class ProjectResources : BusinessCollectionBase   {   } }  

Since this is a collection object, we typically don't need to declare any instance variablesthat's all taken care of by BusinessCollectionBase .

Business Properties and Methods

What we do need to do is implement an indexer to retrieve items from the collection, and a Remove() method to remove items from the collection, as follows:

  #region Business Properties and Methods     public ProjectResource this [int index]     {       get       {         return (ProjectResource)List[index];       }     }     public ProjectResource this [string resourceID]     {       get       {         foreach(ProjectResource r in List)         {           if(r.ResourceID == resourceID)             return r;         }         return null;       }     }  

In fact, there are two indexer properties. The basic one is easily implemented by simply returning the item from our internal collection, List . The reason why this indexer has to be implemented here, rather than in the base class, is that it's strongly typedit returns an object of type ProjectResource .

The second indexer provides more usability in some ways, because it allows the UI developer to retrieve a child object based on that object's unique identification value, rather than a simple numeric index. In the case of a ProjectResource , we can always identify the child object by the ResourceID field.

We have two Remove() methods in order to provide flexibility to the UI developer.

  public void Remove(ProjectResource resource)     {       List.Remove(resource);     }     public void Remove(string resourceID)     {        Remove(this[resourceID]);     }     #endregion  

The easy case is where we're given a ProjectResource object to remove, because we can use the base-class functionality to do all the work. More complex is the case in which we're given the ResourceID of the ProjectResource object. Then, we have to scan through the list of child objects to find the matching entry; after that, we can have the base class remove it.

The other functionality we need to implement is the ability to add new ProjectResource child objects to the collection. We specifically designed our ProjectResource class so that the UI developer can't directly create instances of the child objectthey need to call methods on our collection to do that.

Above all what we're doing here is adding a resource to our project to fill a certain role. In the ProjectResource class, we implemented a number of static factory methods that make it easy to create a child object, given a Resource object or a ResourceID . The assignment can be made to a specific role, or we can use a default role value.

Because all the hard work of creating and initializing a new child object is already handled by the methods we wrote in the ProjectResource class, the only thing we need to worry about in our collection class is getting the new object into the collection properly. The trick is to ensure that we don't add duplicate objects to the collection, which we can handle with the following method, which you should place inside the Business Properties and Methods region as follows:

  private void DoAssignment(ProjectResource resource)     {       if(!Contains(resource))         List.Add(resource);       else         throw new Exception("Resource already assigned");     }  

This method checks the lists of active objects to see if any of them already represent this particular resource. If one does, we throw an exception; otherwise, the new child object is added to the active list.

Then all we need to do is create the set of methods that the UI code can use to add a new child object. Add these three methods to the same region as follows:

  public void Assign(Resource resource, string role)     {       DoAssignment(ProjectResource.NewProjectResource(resource, role));     }     public void Assign(string resourceID, string role)     {       DoAssignment(ProjectResource.NewProjectResource(resourceID, role));     }     public void Assign(string resourceID)     {       DoAssignment(ProjectResource.NewProjectResource(resourceID));     }  

These methods allow the UI developer to provide us with a Resource object and a role, a resource ID value, and an option in between. Notice how we're using the static factory methods from the ProjectResource class to create the child object, thereby keeping the code here in our collection class relatively simple.

Contains

As we noted when we built our collection templates, 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, as follows:

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

In our case, we'll also implement another variation on the Contains() and ContainsDeleted() methods in this region in order to accept ID values rather than objects. This will come in very handy when we implement our Web Forms UI, because it will allow us to determine quickly if the collection contains a child object, when all we know is the child object's ID value.

 public bool Contains(string resourceID)  {       foreach(ProjectResource r in List)         if(r.ResourceID == resourceID)           return true;       return false;     }     public bool ContainsDeleted(string resourceID)     {       foreach(ProjectResource r in deletedList)         if(r.ResourceID == resourceID)           return true;       return false;     }  

As we'll see in Chapter 9 when we build the Web Forms UI, we sometimes have cases in which the UI knows the ID value of a child object, but we haven't yet loaded the child object from the database. By allowing this check, we can find out if the child object can be added to the collection before going to the expense of loading the object from the database.

static Methods

In our static Methods region, we'll have two methods to create a new collection and to load the collection with data during the fetch operation.

  #region static Methods     internal static ProjectResources NewProjectResources()     {       return new ProjectResources();     }     internal static ProjectResources GetProjectResources(SafeDataReader dr)     {       ProjectResources col = new ProjectResources();       col.Fetch(dr);       return col;     }     #endregion  

With these functions, the Project object can now easily create an instance of the child collection using our standard static factory method approach.

Constructors

We also have the standard private constructor method that calls MarkAsChild() so that our collection object acts like a child, as shown here:

  #region Constructors     public ProjectResources()     {       MarkAsChild();     }     #endregion  
Data Access

Finally, we need to create the data-access methods. In a child object or child collection, we implement internal methods that are called by the parent object. We'll have a Fetch() method that's called by our factory method, which in turn is called by the Project object's DataPortal_Fetch() . The Fetch() method is passed a SafeDataReader object from the Project . All it needs to do is move through the rows in the former, adding a new ProjectResource object for each row, as shown here:

  #region Data Access     // called to load data from the database     private void Fetch(SafeDataReader dr)     {       while(dr.Read())         List.Add(ProjectResource.GetProjectResource(dr));     }  

In Project.DataPortal_Fetch() , we loaded the data reader with data, and got it pointed at the right result set from the getProject stored procedure. This means that our code here can simply use the data providedit doesn't need to worry about retrieving it. The ProjectResource class includes a special factory method GetProjectResource() that accepts a data-reader object as a parameter specifically to support this scenario.

We also need to implement an Update() method that's called from Project.DataPortal_Update() . Since the latter includes the [Transactional()] attribute, our data updates here will automatically be protected by a COM+ transaction. The interesting thing here is that we need to call Update() on the child objects in both the active and deleted lists.

  // called by Project to delete/add/update data into the database     internal void Update(Project project)     {       // update (thus deleting) any deleted child objects       foreach(ProjectResource obj in deletedList)         obj.Update(project);       // now that they are deleted, remove them from memory too       deletedList.Clear();       // add/update any current child objects       foreach(ProjectResource obj in List)         obj.Update(project);     }     #endregion  

Each child object is responsible for updating itself into the database. We pass a reference to the root Project object to each child so that it can retrieve any data it requires from that object. In our case, each child object needs to know the ID of the Project to use as a foreign key in the database.

Once the deleted objects have been updated into the database, we clear the deleted list. Since these objects are physically out of the database at this point, we don't need to retain them in memory either. Then we update the list of nondeleted child objects. The end result is that our collection and all its child objects are an exact copy of the data in the underlying database.

ResourceAssignments

As you'd expect, the ResourceAssignments class is very similar to ProjectResources . The biggest differences between the two come because we're illustrating the use of Enterprise Services transactions with the ProjectResources object, but manual ADO.NET transactions with ResourceAssignments . In this section, we'll just focus on the parts dealing with the transactional code so we'll add the existing class from the download.

In the Data Access region, we have the Fetch() method. By the time execution gets here, the Resource object's DataPortal_Fetch() method has retrieved our data from the getResource stored procedure within the context of a transaction. It passes the populated data reader to our collection so that we can read the data as we create each child object.

Our collection code doesn't need to worry about committing or rolling back the transaction, because DataPortal_Fetch() handles it. We're just responsible for reading the data.

 // called by Resource to load data from the database     private void Fetch(SafeDataReader dr)     {       while(dr.Read())         List.Add(ResourceAssignment.GetResourceAssignment(dr));     } 

This code is no different, regardless of whether we use COM+ or manual transactions. The story is a bit different with the Update() method, however. In this case, DataPortal_Update() in the Resource object opened a database connection and started a transaction on that connection. Our update code needs to use that same connection and transaction in order to be protected, too. This means that we get the SqlTransaction object passed as a parameter. This also gives us the underlying connection object, since that's a property of the SqlTransaction object.

The primary difference between the resulting Update() method and the one in our earlier ProjectResources class is that we pass the SqlTransaction object to each child object so that it can use the same connection and transaction to update its data.

 // called by Resource to delete/add/update data into the database     internal void Update(SqlTransaction tr, Resource resource)     {       // update (thus deleting) any deleted child objects       foreach(ResourceAssignment obj in deletedList)         obj.Update(tr, resource);       // now that they are deleted, remove them from memory too       deletedList.Clear();       // add/update any current child objects       foreach(ResourceAssignment obj in List)         obj.Update(tr, resource);     } 

The differences between using COM+ and manual transactions when updating a collection are very minor. Most of the changes occur in the child class ( Assignment ), which we discussed earlier in the chapter.

ProjectList

The final two classes that we need to create are the read-only lists containing project and resource data. As with most applications, we need to present the user with a list of items. From that list of items, the user will choose which one to view, edit, delete, or whatever.

We could simply retrieve a collection of Project objects and display their data, but that would mean retrieving a lot of data for Project objects that the user may never use. Instead, it's more efficient to retrieve a small set of read-only data for display purposes, and then retrieve an actual Project object once the user has chosen which one to use.

Tip 

In VB 6, we would have probably used a disconnected Recordset object for this purpose. In .NET, however, we'll often get better performance using this object-oriented implementation as opposed to using a DataSet , because our implementation will have less overhead in terms of metadata.

The CSLA .NET Framework includes the ReadOnlyCollectionBase class, which is designed specifically to support this type of read-only list. Add a new class called ProjectList , and make the following highlighted changes:

  using System; using System.Data; using System.Data.SqlClient; using CSLA; using CSLA.Data; namespace ProjectTracker.Library {   [Serializable()]   public class ProjectList : ReadOnlyCollectionBase   {   } }  

As with any other class, we'll implement business properties, shared methods, and data access, though the implementations will be comparatively simple.

Data Structure

The easiest way to implement this type of class is to define a struct containing the data fields we want to display:

Normally, we'd define a struct like this:

 [Serializable()] public struct ProjectInfo {   public Guid ID;   public string Name; } 

The catch here is that we want to support data binding. Windows Forms data binding will bind just fine to a struct that has public fields, like the one we showed earlier. Unfortunately, Web Forms data binding won't bind to fields, but only to properties . This means that we need to make our fields private and implement public properties instead:

 [Serializable()]   public class ProjectList : ReadOnlyCollectionBase   {  #region Data Structure      [Serializable()]       public struct ProjectInfo     {       // this has private members, public properties because       // ASP.NET can't databind to public members of a structure       private Guid _id;       private string _name;       public Guid ID       {         get         {           return _id;         }         set         {           _id = value;         }       }       public string Name       {         get         {           return _name;         }         set         {           _name = value;         }       }       public bool Equals(ProjectInfo info)       {         return _id.Equals(info.ID);       }     }     #endregion   }  

The end result is the same: We have a struct with ID and Name values, but by exposing the values via properties , we allow both Web Forms and Windows Forms to data bind to the struct . We also overload the Equals() method so that we can provide strongly typed comparisons.

Business Properties and Methods

Since this is a read-only collection, the only business property we need to expose is an indexer:

  #region Business Properties and Methods     public ProjectInfo this [int index]     {       get       {         return (ProjectInfo)List[index];       }     }     #endregion  

This is the same kind of code we use to implement any strongly typed indexer in a collection object.

Contains

As with our other collections, we overload the Contains() method as follows:

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

This ensures that the overload Equals() method on our child structures will be invoked.

static Methods

We then need to implement a static factory method that returns a populated ProjectList object.

  public static ProjectList GetProjectList()     {       return (ProjectList)DataPortal.Fetch(new Criteria());     }  

This method simply calls DataPortal.Fetch() , so that the DataPortal can create an instance of the ProjectList class and call its DataPortal_Fetch() method.

Criteria and Constructors

As with any business object that interacts with the DataPortal , we need a private constructor and a nested Criteria class. In this case, we'll always be retrieving all the project data, so there's no criteria data. We still need a Criteria class to drive the DataPortal , however.

  #region Criteria     [Serializable()]       public class Criteria     {       // no criteria, we retrieve all projects     }     #endregion     #region Constructors     private ProjectList()     {       // prevent direct creation     }     #endregion  

In many cases, we would use criteria to filter the result set, since it's not wise to return large amounts of data in a distributed application. To do this, we simply add fields to the Criteria class, as we have in the previous business classes in this chapter.

Data Access

Since this is a read-only object, the only method we can implement for data access is DataPortal_Fetch() . This method is relatively straightforwardit involves calling the getProjects stored procedure and then creating a ProjectInfo struct for each row of data returned, as shown here:

  #region Data Access     protected override void DataPortal_Fetch(object criteria)     {       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           cm.CommandText = "getProjects";           using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))           {             locked = false;             while(dr.Read())             {               ProjectInfo info = new ProjectInfo();               info.ID = dr.GetGuid(0);               info.Name = dr.GetString(1);               List.Add(info);             }             locked = true;           }         }       }     }     #endregion  

This method follows the same basic pattern as all the other DataPortal_Fetch() methods we've implemented so far. The end result is that the ProjectList collection contains one ProjectInfo entry for each project in our database.

ResourceList

The ResourceList class is very similar to ProjectList . The entire class is available with the code download for this book; the class has its own struct to represent the appropriate Resource data:

 #region Data Structure     [Serializable()]       public struct ResourceInfo     {       // this has private members, public properties because       // ASP.NET can't databind to public members of a structure       private string _id;       private string _name;       public string ID       {         get         {           return _id;         }         set         {           _id = value;         }       }       public string Name       {         get         {           return _name;         }         set         {           _name = value;         }       }       public bool Equals(ResourceInfo info)       {         return _id == info.ID;       }     }     #endregion 

And its DataPortal_Fetch() method calls the getResources stored procedure and populates the collection based on that data, as follows:

 #region Data Access     protected override void DataPortal_Fetch(object criteria)     {       using(SqlConnection cn = new SqlConnection(DB("PTracker")))       {         cn.Open();         using(SqlCommand cm = cn.CreateCommand())         {           cm.CommandType = CommandType.StoredProcedure;           cm.CommandText = "getResources";           using(SafeDataReader dr = new SafeDataReader(cm.ExecuteReader()))           {             locked = false;             while(dr.Read())             {               ResourceInfo info = new ResourceInfo();               info.ID = dr.GetString(0);               info.Name = dr.GetString(1) + ", " + dr.GetString(2);               List.Add(info);             }             locked = true;           }         }       }     }     #endregion 

Notice how the Name property in the struct is populated by concatenating two values from the database. This illustrates how business rules and formatting can be incorporated into the business object to provide the most useful set of return data for display.

At this stage, the ProjectTracker solution will compile without error, and we're ready to move on and write code that actually uses it.

[2] Priya Dhawan, "Performance Comparison: Transaction Control: Building Distributed Applications with Microsoft .NET," MSDN, February 2002. See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/bdadotnetarch13.asp.



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