Business Object Life Cycle

Before we get into the code structure for our business objects, it's worth spending some time to understand the life cycle of those objects. By life cycle, I mean the sequence of methods and events that occur as the object is created and used. Although we can't predict the business properties and methods that might exist on an object, there's a set of steps that occur during the lifetime of every business object.

Typically, an object is created by UI code, whether that's Windows Forms, Web Forms, or a web service. Sometimes, an object may be created by another object, which will happen when we have a using relationship between objects, for instance.

Object Creation

Whether editable or read-only, all root objects go through the same basic creation process. (Root objects are those that can be directly retrieved from the database, while child objects are retrieved within the context of a root object, though never directly.)

As we discussed in Chapter 5, it's up to the root object to invoke methods on its child objects and child collections so that they can load their own data from the database. Usually, the root object actually calls the database and gets all the data back, and then provides that data to the child objects and collections so that they can populate themselves . From a pure object-oriented perspective, it might be ideal to have each object encapsulate the logic to get its own data from the database, but in reality it's not practical to have each object independently contact the database to retrieve one row of data.

Root Object Creation

Root objects are created by calling a factory method , which is a method that's called in order to create an object. In our case these will be static methods on the class. The static method will either create the object directly, or use the DataPortal to load the object with default values.

If we don't need to retrieve default values from the database, we can use a simple, fast approach, as follows :

  1. The static factory method is called.

  2. The static method creates the object using the new keyword, possibly passing parameter values.

  3. The business object does any initialization in the constructor.

  4. The business object is returned.

  5. From the business object's perspective, only one method is called, as follows: new (any constructor).

This is illustrated in Figure 7-1.

image from book
Figure 7-1: Root object creation process

On the other hand, if we do need to populate the new object with default values, then we'll use the DataPortal 's Create() method as follows:

  1. The static factory method is called.

  2. The static method calls DataPortal.Create() to get the business object.

  3. DataPortal.Create() creates the object using reflection.

  4. The business object can do basic initialization in the constructor.

  5. The DataPortal_Create() method is called, and this is where the business object implements data-access code to load its default values.

  6. The business object is returned.

  7. From the business object's perspective, two methods are called, as follows:

    • new (default constructor)

    • DataPortal_Create()

This is illustrated in Figure 7-2.

image from book
Figure 7-2: Process to create a business object

To the UI code, of course, there's no difference: That code just calls the static factory method and gets an object back. From the business object's perspective, the primary difference lies in whether DataPortal_Create() is called in order to initialize the object.

Child Object Creation

Child objects are usually created when the UI code calls an Add() method on the collection object that contains the child object. Ideally, the child class and the collection class will be in the same assembly, so the static factory methods on a child object can be scoped as internal , rather than public . This way, the UI can't directly create the object, but the collection object can create the child when the UI calls the collection's Add() method.

The framework doesn't actually dictate this approach. Rather, it's a design choice on my part because I feel that it makes the use of the business objects more intuitive from the UI developer's perspective. It's quite possible to allow the UI code to create child objects directly, by making the child factory methods public ; the collection's Add() method would then accept a prebuilt child object as a parameter. I think that's less intuitive, but it's perfectly valid, and you could implement your objects that way if you chose.

As we did with the root objects, we may or may not need to load default values from the database when we create a child object.

Tip 

If we don't need to retrieve default values from the database, we could have the collection object create the child object directly, using the new keyword. For consistency, however, it's better to stick with the static factory method approach, so that all objects are created the same way.

The sequence of steps to create a child object that doesn't need default values from the database is

  1. The static factory method ( internal scope) is called.

  2. The static method creates the object locally by using the new keyword and possibly passing parameter values.

  3. The child object does any initialization in the constructor method.

  4. The child object is returned.

  5. From the child object's perspective, only one method is called as follows: new (any constructor).

This is illustrated in Figure 7-3.

image from book
Figure 7-3: Child object creation process

Once the child object has been created and added to the parent, the UI code can access the child via the parent's interface. Typically, the parent will provide an indexer that allows the UI to access child objects directly.

Though the static factory method is called by the collection object rather than the UI code, this is the same process that's used to create a root object. The same is true if we need to retrieve default values from the database.

  1. The static factory method ( internal scope) is called.

  2. The static method calls DataPortal.Create() to get the business object.

  3. DataPortal.Create() creates the object using reflection.

  4. The child object can do basic initialization in the constructor method.

  5. The DataPortal_Create() method is called, and this is where the child object implements data-access code to load its default values.

  6. The child object is returned. Again, the static factory method is called by the collection object rather than the UI, but the rest of the process is the same as with a root object.

  7. From the child object's perspective, two methods are called, as follows:

    • new (default constructor)

    • DataPortal_Create()

This is illustrated in Figure 7-4.

image from book
Figure 7-4: Process to create a child object
Note 

Note that this requires the child object to have a nested Criteria class. Normally, child objects don't contain a Criteria class, because they can't be retrieved directly from the database. In this case, however, they need the Criteria class so that the DataPortal can properly call the DataPortal_Create() method.

Note that in either of these cases, the UI code is the same: It calls the Add() method on the parent object, and then interacts with the parent's interface to get access to the newly added child object. The UI is entirely unaware of how the child object is created (and possibly loaded with default values).

Also note that the parent object is unaware of the details. All it does is call the static factory method on the child class, and receive a new child object in return. All the details about how the child object got loaded with default values are encapsulated within the child class.

Object Retrieval

Retrieving an existing object from the database is similar to the process of creating an object that requires default values from the database. Only a root object can be retrieved from the database directly by code in the user interface. Child objects are retrieved along with their parent root object, not independently.

Root Object Retrieval

To retrieve a root object, our UI code simply calls the static factory method on the class, providing the parameters that identify the object to be retrieved. The static factory method calls DataPortal.Fetch() , which in turn creates the object and calls DataPortal_Fetch() , as follows:

  1. The static factory method is called.

  2. The static method calls DataPortal.Fetch() to get the business object.

  3. DataPortal.Fetch() creates the business object using reflection.

  4. The business object can do basic initialization in the constructor method.

  5. The DataPortal_Fetch() method is called, and this is where the business object implements data-access code to retrieve the object's data from the database.

  6. The business object is returned.

  7. From the business object's perspective, two methods were called, as follows:

    • The default constructor

    • DataPortal_Fetch()

Figure 7-5 illustrates the process.

image from book
Figure 7-5: Process to load an object with data from the database

It's important to note that the root object's DataPortal_Fetch() is responsible not only for loading the business object's data, but also for starting the process of loading the data for its child objects.

In Chapter 6, we implemented our stored procedures so that they return the root object's data and also all the child-object data two result sets from a single stored procedure. This means that when the root object calls the stored procedure to retrieve its data, it will also get the data for its child objects, so it must cause those to be created as well.

The key thing to remember is that the data for the entire object, including its child objects, is retrieved when DataPortal_Fetch() is called. This avoids having to go back across the network to retrieve each child object's data individually. Though the root object gets the data , it's up to each child object to populate itself based on that data. Let's dive one level deeper and discuss how child objects load their data.

Child Object Retrieval

The retrieval of a child object is quite different from the retrieval of a root object, because the DataPortal isn't directly involved. Instead, as stated earlier, the root object's DataPortal_Fetch() method is responsible for loading not only the root object's data, but also the data for all child objects. It then calls methods on the child objects, passing the preloaded data as parameters so the child objects can load their variables with data.

The sequence of events goes like this:

  1. The root object's DataPortal_Fetch() creates the child collection using a static factory method on the collection class, and it passes a data-reader object as a parameter.

  2. The child collection implements a private Fetch() method to load its data. This method uses the data reader provided as a parameter.

  3. The child collection's Fetch() method loops through the records in the data reader, performing the following steps for each record.

  4. Create a child object by calling a static factory method, passing the data reader as a parameter.

  5. The child object calls its own private Fetch() method to load itself with data from the data reader.

  6. The collection object adds the child object to its collection.

  7. At the end of the data reader, the child collection and all child objects are fully populated .

  8. From the child collection object's perspective, only one method is called, as follows:

    • Any constructor

  9. From each child object's perspective, only one method is called, as follows:

    • Any constructor

Figure 7-6 is a sequence diagram that illustrates how this works. Note that this diagram occurs during the process of loading the root object's data . This means that this diagram is really an expansion of the previous sequence diagram for retrieving a root object!

image from book
Figure 7-6: Process of loading child-object data from the database

Updating Editable Objects

For read-only objects, all we need to worry about is retrieval, but for our editable business objects and editable collections (those deriving from BusinessBase and BusinessCollectionBase ) we also need to think through the process of updating the objects into the database. The process is the same for adding or editing an object, and for deleting child objects. As we'll see, deleting a root object is a bit different.

Adding and Editing Root Objects

After an object is created or retrieved, the user will work with the object, changing its values by interacting with the user interface. At some point, the user may click the OK or Save button, thereby triggering the process of updating the object into the database. The sequence of events at that point is as follows:

  1. The UI calls the Save() method on our business object.

  2. The Save() method calls DataPortal.Update() .

  3. DataPortal.Update() calls the DataPortal_Update() method on the business object, which contains the data-access code needed to insert or update the data into the database.

  4. During the insert or update process, the business object's data may change.

  5. The updated business object is returned as a result of the Save() method.

  6. From the business object's perspective, two methods are called:

    • Save()

    • DataPortal_Update()

Figure 7-7 illustrates this process.

image from book
Figure 7-7: How business objects are added or updated

The Save() method is implemented in BusinessBase and BusinessCollectionBase , and typically requires no change or customization. Remember that the framework's Save() method includes checks so that we can only save an object if it has no broken rules in the BrokenRules collection, and that the object will only be saved if IsDirty is true . This helps to optimize our data access by preventing the update of invalid or unchanged data.

Tip 

If we don't like this behavior, our business class can override the framework's Save() method and replace that logic with other logic.

All the data-access code that handles the saving of the object is located in DataPortal_Update() .

Note 

It's important to recall that when the server-side DataPortal is remote, the updated root object returned to the UI is a new object. The UI must update its references to use this new object in lieu of the original root object.

Note that DataPortal_Update() is responsible not only for saving the object's data, but also for starting the process of saving all the child-object data. Calling the DataPortal does not save Child objects; they are saved because their root parent object directly calls internal -scoped Update() methods on each child collection or object, thereby causing them to save their data.

Adding, Editing, and Deleting Child Objects

Child objects are inserted, updated, or deleted as part of the process of updating a root parent object. To support this concept, child collections and child objects implement an internal method named Update() , which can be called by the root object during the update process. As we've discussed, it's helpful for related root, child, and child collection classes to be placed in the same project (assembly) so that they can use internal scope in this manner.

The sequence of events to add, edit, or delete a child object is as follows:

  1. The root object's DataPortal_Update() method calls the child collection's Update() method, passing itself as a parameter so that child objects can use root-object property values as needed (such as for foreign key values).

  2. The child collection's Update() method loops through all its active child objects, calling each child object's Update() method. (The child object's Update() method includes the data-access code to insert or update the object's data into the database.)

  3. The child collection's Update() method then loops through all its deleted child objects, calling each deleted object's Update() method. (The child object's Update() method includes the data-access code to delete the object's data from the database.)

  4. At this point, all the child object data has been inserted, updated, or deleted as required.

  5. From the perspective ofthe child collection object, just one method is called, as follows:

    • Update()

  6. From the perspective of each child object, just one method is called, as follows:

    • Update()

Figure 7-8 illustrates this process. Remember that this diagram is connected with the previous diagram showing the update of a root object. The events depicted in this diagram occur as a result of the root object's DataPortal_Update() being called, as shown earlier in Figure 7-7.

image from book
Figure 7-8: Process of adding or updating child objects

The Update() methods often accept parameters. Typically, the root object's primary key value is a required piece of data when we save child objects (since it would be a foreign key in the table), and so we'll typically pass a reference to the root object as a parameter to the Update() method. Passing a reference to the root object is better than passing any specific property value, because it helps to decouple the root object from the child object. Using a reference means that the root object doesn't know or care what actual data is required by the child object during the update processthat information is encapsulated within the child class.

Also, if we're implementing transactions manually using ADO.NET, rather than using Enterprise Services, then the ADO.NET transaction object will also need to be passed as a parameter, so that each child object can update its data within the same transaction as the root object.

Tip 

As we implement our sample application, we'll create examples using both Enterprise Services and ADO.NET transactions to illustrate how each is coded.

Deleting Root Objects

Although child objects are deleted within the context of the root object that's being updated, we implemented the DataPortal so that it supports the idea of immediate deletion of a root object, based on criteria. This means that the UI code can call a static delete method on the business class, thereby providing it with parameters that define the object to be deletedtypically, the same criteria that would be used to retrieve the object.

The sequence of events flows like this:

  1. The static delete method is called.

  2. The static delete method calls DataPortal.Delete() .

  3. DataPortal.Delete() creates the business object using reflection.

  4. DataPortal.Delete() calls the DataPortal_Delete() method on the business object, which contains the code needed to delete the object's data (and any related child data, and so on).

  5. From the business object's perspective, two methods are called, as follows:

    • The default constructor

    • DataPortal_Delete()

Figure 7-9 illustrates the process of immediate deletion.

image from book
Figure 7-9: Process of immediate deletion of objects

Since this causes the deletion of a root object, the delete process must also remove any data for child objects. This can be done through our ADO.NET data-access code, through a stored procedure, or by the database (if cascading deletes are set up on the relationships). In our example application, we'll be deleting the child data within our stored procedures. We created these procedures in Chapter 6.

Disposing and Finalizing Objects

Most business objects contain moderate amounts of data in their variables. For these, .NET's default garbage-collection behavior, in which we don't know exactly when an object will be destroyed, is finewe don't really care if the object is destroyed as soon as we release our reference to it, or later on. The time we do care is when we create objects that hold onto "expensive" resources until they're destroyed.

"Expensive" resources include things like open-database connections, open files on disk, and so forth. These are things that need to be released as soon as possible in order to prevent our application from wasting memory or blocking other users who might need to access a file or reuse a database connection. If we write our objects properly, most of these concerns should go away: Our data-access code should never keep a database connection open for any length of time, and the same is true for any files we might open on disk. However, there are cases in which our business object can legitimately contain an expensive resourcesomething like a multi-megabyte image in a variable, perhaps.

Implementing IDisposable

In such a case, we should implement the IDisposable interface in our business class, which will allow the UI code to tell our business object to release its resources. It's then up to us to write the Dispose() method to actually release those resources:

  [Serializable()] public class MyBusinessClass : BusinessBase, IDisposable {   public void Dispose()   {     // Release expensive resources here   } }  

The UI code can now call the Dispose() method on our class when it has finished using our object, at which point our object will release its expensive resourcessuch as our hypothetical image.

Note however that were we to retrieve a business object using a remote DataPortal configuration, the business object would be created and loaded on the server. It's then returned to the client by remoting, thereby leaving a copy in memory on the server. The server-side DataPortal code is finished when we execute the return statement that returns the object, as follows:

 return obj; 

Because of this, there's no way to call the Dispose() method on the server. To avoid this scenario, anytime that the DataPortal may be configured to run outside of the client process (via remoting), our object designs must avoid any requirement for a Dispose() method. Happily, this is almost never an issue with a properly designed business object, since all database connections or open files should be closed in the same method from which they were opened.

Note 

If you're calling the DataPortal via remoting, you must avoid object designs that require IDisposable .

Implementing a Finalize Method

If, for some reason, our business object holds an expensive resource (such as a database connection or a file) that must be closed when the object is destroyed, it's a best practice to implement a Finalize() method. However, this is generally a poor solution, for at least two reasons.

The Finalize() method is called by the garbage collector immediately before our object is destroyed. If our object implements a Finalize() method, it will take longer for the garbage collector to destroy our object, because objects with Finalize() methods are put into a separate list and processed later than simpler objects.

Also, if our process is shut down, the garbage collector won't call Finalize() . It assumes that there's no need because we're shutting down, but this can leave our resources in an indeterminate state after the application is gone, since we didn't explicitly close or release them.

Note 

Implementing a Finalize() method in a business object to release expensive resources is a last-ditch attempt to solve a problem that would have been better solved through a different design.

This scenario shouldn't occur in objects created for the CSLA .NET Framework. Waiting until Finalize() runs doesn't guarantee the release of the resource, and it may happen minutes or seconds after the object was de-referenced. The only situation in which a Finalize() method makes sense is in conjunction with implementing IDisposable . As we discussed in the previous section, CSLA .NET isn't very friendly to the idea of implementing IDisposable , and it won't work at all if we're running the DataPortal on a server via remoting.

When designing business objects for CSLA .NET, all expensive resources such as database connections and open files should be closed and released in the same method from which they were opened. Typically, this is in the DataPortal_xyz() methods where we're retrieving or saving object data.



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