Desktop Developer
Manage the Business Object Lifecycle
Using business objects effectively requires understanding the nuances of their life cycles.
Technology Toolbox: Visual Basic
It's worth spending some time to understand the lifecycle of business objects. By lifecycle, I mean the sequence of methods and events that occur as the object is created and used.
Although it isn't always possible to 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. I'll walk you through what these steps are, and how you can take advantage of them to make more robust and efficient business objects.
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 there's a using relationship between objects, for instance.
Object Creation
All root objects go through the same basic creation process, whether they are editable or read-only. (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.)
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 calls the database and gets all the data back itself, and then provides that data to the child objects and collections so that they can populate themselves. From a purely 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 objects are created by calling a factory method, which is a method that's called in order to create an object. These will be Shared methods on the class. The Shared method will use the data portal to load the object with default values (see Figure 1).
If the object doesn't need to retrieve default values from the database, the <RunLocal()> attribute can be used to short-circuit the data portal, so the object initialization occurs locally.
There's no difference from the perspective of the UI code—that code calls the factory method and gets an object back:
Dim root As Root = Root.NewRoot()
From the business object's perspective, most of the work occurs in the DataPortal_Create() method, where the object's values are initialized.
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 Shared factory methods on a child object can be scoped as Friend, 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.
Use Objects More Intuitively
This approach is 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 can implement your objects that way if you choose.
Note that child objects can optionally be created through databinding, in which case the addition is handled by overriding the AddNewCore() method in the collection class.
As with the root objects, you might need to load default values from the database when creating a child object.
If you don't need to retrieve default values from the database, you could have the collection object create the child object directly using the new keyword. For consistency, however, it's better to stick with the factory method approach so that all objects are created the same way.
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 a default property that allows the UI to access child objects directly.
Though the factory method is called by the parent object rather than the UI code, this is the same process that's used to create a root object. The same is true if the object needs to load itself with default values from the database.
Begin by calling the factory method (Friend scope). The factory method calls DataPortal.Create() to get the child business object. The data portal then creates a new instance of the business object, and the child object can do basic initialization in the constructor method. The DataPortal_Create() method is called. This is where the child object implements data access code to load its default values. The child object is returned at this point. Again, the 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. From the child object's perspective, two methods are called: the default constructor and DataPortal_Create().
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 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.
Root 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.
To retrieve a root object, the UI code calls the Shared factory method on the class, providing the parameters that identify the object to be retrieved. The factory method calls DataPortal.Fetch(), which in turn creates the object and calls DataPortal_Fetch(). 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.
A typical practice is to implement stored procedures to return the root object's data, as well as 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 also gets 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 drill down one level deeper and discuss how child objects load their data. The retrieval of a child object is quite different from the retrieval of a root object, because the data portal 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 for loading the data for all child objects, as well. It then calls methods on the child objects, passing the preloaded data as parameters, so the child objects can load their fields with data.
For read-only objects, retrieval is the only data access concept required. Editable business objects and editable collections (those deriving from BusinessBase and BusinessListBase) support update, insert, and delete operations, as well. 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 okay or Save button, thereby triggering the process of updating the object into the database.
The Save() method is implemented in BusinessBase and BusinessListBase, and typically requires no change or customization. Remember that the framework's Save() method includes checks to ensure that objects can only be saved if IsValid and IsDirty are True. This helps to optimize data access by preventing the update of invalid or unchanged data. If you don't like this behavior, your 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_Insert() or DataPortal_Update(). Also, 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 the DataPortal_XYZ methods are responsible not only for saving the object's data, but also for starting the process of saving all the child object data. Calling the data portal does not save child objects; they are saved because their root parent object directly calls Friend-scoped Insert(), Update(), or DeleteSelf() 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 implement a Friend method named Update(). Child objects within a collection implement Friend methods—named Insert(), Update(), and DeleteSelf()—that can be called by the collection during the update process. It is helpful for related root, child, and child collection classes to be placed in the same project (assembly), so that they can use Friend scope in this manner.
The Insert() and Update() methods often accept parameters. Typically, the root object's primary key value is a required piece of data when saving a child object (because it would be a foreign key in the table), and so a reference to the root object is typically passed as a parameter to the collection's Update() method, and then to each child object's Insert() or 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 process—that information is encapsulated within the child class.
Also, when implementing transactions manually using ADO.NET, rather than System.Transactions or Enterprise Services, 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.
In most cases, the use of System.Transactions will provide the best trade-off between performance and simplicity of data access code.
While child objects are deleted within the context of the root object that's being updated, deletion of root objects is a bit different. Recall that the data portal was implemented to support both immediate and deferred deletion of a root object.
Immediate deletion occurs when the UI code calls a Shared delete method on the business class, providing parameters that define the object to be deleted: typically, the same criteria that would be used to retrieve the object.
Most applications will use immediate deletion for root objects. The delete process must also remove any data for child objects. This can be done through ADO.NET data access code, through a stored procedure, or by the database (if cascading deletes are set up on the relationships). In the example application, child data is deleted by stored procedures.
Deferred deletion occurs when the business object is loaded into memory and the UI calls a method on the object to mark it for deletion. When the Save() method is called, the object is deleted rather than being inserted or updated.
It is up to the business object author to decide which deletion model to support by implementing either a Shared or Instance delete method on the object.
Implement Dispose and Finalize Objects
Most business objects contain moderate amounts of data in their fields. For these, the default .NET default garbage collection behavior is fine. With that behavior, you don't know exactly when an object will be destroyed and its memory reclaimed. But that's almost always okay, because this is exactly what garbage collection is designed to do.
However, the default garbage collection behavior may be insufficient when objects hold onto "expensive" or unmanaged resources until they're destroyed. These resources include things like open database connections, open files on disk, synchronization objects, handles, and any other objects that already implement IDisposable. These are things that need to be released as soon as possible in order to prevent the application from wasting memory or blocking other users who might need to access a file or reuse a database connection.
If business objects are written properly, most of these concerns should go away. Data access code should keep a database connection open for the shortest amount of time possible, and the same is true for any files the object might open on disk. However, there are cases in which business objects can legitimately contain an expensive resource—something like a multi-megabyte image in a field, perhaps.
In such cases, the business object should implement the IDisposable interface, which will allow the UI code to tell the business object to release its resources. This interface requires that the object implement a Dispose() method to actually release those resources:
<Serializable()> _
Public Class MyBusinessClass
Inherits BusinessBase(Of MyBusinessClass)
Implements IDisposable
Private mDisposedValue As Boolean
Protected Sub Dispose(ByVal disposing As Boolean)
If Not mDisposedValue Then
If disposing Then
? free unmanaged resources
End If
End If
? free shared unmanaged resources
mDisposedValue = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
End Sub
End Class
The UI code can now call the object's Dispose() method (or employ a Using statement) when it has finished using the object, at which point the object will release its expensive resources.
Note that if a business object is retrieved using a remote data portal configuration, the business object would be created and loaded on the server. It's then returned to the client. However, this means there remains a copy left in memory on the server.
Because of this, there's no way to call the business object's Dispose() method on the server. To avoid this scenario, any time that the data portal can be configured to run outside of the client process, the business object designs must avoid any requirement for a Dispose() method. Happily, this is almost never an issue with a properly designed business object, because all database connections or open files should be closed in the same method from which they were opened.
This article is excerpted from Chapter 7 of the book by Rockford Lhotka, Expert VB 2005 Business Objects (Apress). [ISBN: 1-59059-631-5].