CSLA

At this point, we've created the two base classes that safely declare our events for data binding. We're now ready to move on and implement the bulk of the business framework that we described in Chapter 2. This code will make use of what we've already done in this chapter, and it will be enhanced when we implement the DataPortal and data access code in Chapter 5.

Here, we'll implement the classes in order of dependence. Figure 4-5 illustrates the dependencies of the classes.

image from book
Figure 4-5: CLSA .NET class dependencies

You can see clearly that before we can implement the BusinessBase class, we must implement a number of other classes. Also, notice that we have a circular dependency between UndoableBase and BusinessBase , and another among UndoableBase , BusinessCollectionBase , and BusinessBase . This isn't a problem in any technical sense, since all three of these classes are in the same assembly. However, it does mean that as we create UndoableBase , we'll have to write some code that will give us compiler errors until after we've created BusinessBase and BusinessCollectionBase .

Our general plan of attack, then, will be to focus on getting the dependent classes done first, and then we'll wrap up by implementing the ReadOnlyBase and ReadOnlyCollectionBase classes. As an initial step, add a new Class Library project to our solution by right-clicking the solution in Solution Explorer and choosing Add image from book New Project. Name it CSLA , and make sure that it's in the CSLA directory, as shown in Figure 4-6.

image from book
Figure 4-6: Adding the CSLA project to the solution

Right-click Class1.cs and delete it from the project. This time, we'll be adding our own classes as and when we need them.

Much of the code we'll be writing will rely on the classes we created in the CSLA.Core.BindableBase assembly. Use the Add Reference dialog window to add a reference to this project, as shown in Figure 4-7.

image from book
Figure 4-7: Adding a reference to CSLA.Core.BindableBase

NotUndoableAttribute

As you know, we'll be supporting n-Level undo capabilities for our business objects and collections. Sometimes, however, we may have values in our objects that we don't want to be included in the snapshot that's taken before an object is edited. (These may be read-only values, or recalculated values, or valueslarge images, perhaps that are simply so big that we choose not to support undo for them.) In this section, we'll create the custom attribute that will allow business object developers to mark any variables that they don't want to be part of the undo process.

In our implementation of the UndoableBase class, which will provide our BusinessBase class with support for n-Level undo operations, we'll detect whether this attribute has been placed on any variables. If so, we'll simply ignore that variable within the undo process, neither taking a snapshot of its value nor restoring it in the case of a cancel operation. To create the attribute, add a class to the project named NotUndoableAttribute , with the following code:

  using System; namespace CSLA {   [AttributeUsage(AttributeTargets.Field)]   public class NotUndoableAttribute : Attribute   {} }  

The [AttributeUsage()] attribute allows us to specify that this attribute can be applied only to fields, or variables, within our code. Beyond that, our attribute is merely a marker to indicate that certain actions should (or shouldn't) be taken by our UndoableBase code, so there's no real code here at all.

Core.UndoableBase

The UndoableBase class is where all the work to handle n-Level undo for an object will take place. This is pretty complex code that makes heavy use of reflection to find all the instance variables in our business object, take snapshots of their values, and then ( potentially ) restore their values later in the case of an undo operation.

Tip 

Typically, a snapshot of a business object's variables is taken before the user or an application is allowed to interact with the object. That way, we can always undo back to that original state. When we're done, the BusinessBase class will include a BeginEdit() method that will trigger the snapshot process, a CancelEdit() method to restore the object's state to the last snapshot, and an ApplyEdit() method to commit any changes since the last snapshot.

The reason this snapshot process is so complex is that we need to copy the values of all variables in our object, and our business object is essentially composed of several classes all merged together through inheritance and aggregation. As we'll see, this causes problems when classes have fields with the same names as fields in the classes they inherit from, and it causes particular problems if a class inherits from another class in a different assembly. For now, though, add a new class named UndoableBase to the project, and use some namespaces that will make the process easier:

  using System; using System.Collections; using System.Reflection; using System.IO; using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary;  namespace CSLA {   public class UndoableBase 

This class should also be in the CSLA.Core namespace. We can change the namespace statement to place the class in a subnamespace named Core :

  namespace CSLA.Core  {   public class UndoableBase 

Since this will be a base class from which our business objects will ultimately derive, it must be marked as [Serializable()] . It should also be declared with abstract , so that no one can create an instance of this class directlyshe must create business object classes instead. Finally, we know that we want all our business objects to support the IsDirtyChanged event as implemented by our BindableBase class, so we'll inherit from that too:

 namespace CSLA.Core {   [Serializable()]   abstract public class UndoableBase : CSLA.Core.BindableBase   {   } } 

With that base laid down, we can start to discuss how we're actually going to implement the undo functionality. There are three operations involved: taking a snapshot of the object state, restoring the object state in case of an undo, and discarding stored object state in case of an accept operation.

Additionally, if this object has child objects of type BusinessBase or BusinessCollectionBase , those child objects must also perform the store, restore, and accept operations. To achieve this, anytime we encounter a variable that's derived from either of these types, we'll cascade the operation to that object so it can take appropriate action.

The three operations will be implemented by a set of three methods :

  • CopyState()

  • UndoChanges()

  • AcceptChanges()

Helper Functions

To implement the three principal methods, we'll need a helper method that will simplify our use of reflection. More specifically , it simplifies the process of determining whether a variable has the [NotUndoable()] attribute attached. Add the following code to the UndoableBase class:

  #region Helper Functions     private bool NotUndoableField(FieldInfo field)     {       return Attribute.IsDefined(field, typeof(NotUndoableAttribute));     }     #endregion  
NotUndoableField Function

The NotUndoableField() function in the previous listing returns a bool to indicate whether the specified field has the [NotUndoable()] attribute. If the attribute has been applied to the field, the function returns true . We'll use this method as we walk through the variables in our object to take a snapshot of their values, and we'll only take a snapshot of the values of variables without this attribute.

CopyState

The CopyState() method will take a snapshot of our object's current data and store it in a System.Collections.Stack object.

Stacking the Data

Since we're supporting n-Level undo capability, each of our objects could end up storing a number of snapshots. As each undo or accept operation occurs, we'll get rid of the most recent snapshot we've stored; this is the classic behavior of a "stack" data structure. Fortunately, the .NET Framework includes a prebuilt Stack class that we can use. Let's declare that now:

 [Serializable()]   abstract public class UndoableBase : CSLA.Core.BindableBase   {  // keep a stack of object state values     [NotUndoable()]     Stack _stateStack = new Stack();  

This variable is marked as [NotUndoable()] because we certainly don't want to take a snapshot of our snapshots! We just want the variables that contain actual business data. Once we've taken a snapshot of our object's data, we'll serialize the data into a single byte stream, and put that byte stream on the stack. From there, we can retrieve it to perform an undo operation if needed.

Taking a Snapshot of the Data

The process of taking a snapshot of each variable value in our object is a bit tricky. First, we need to use reflection to walk through all the variables in our object. Then, we need to check each variable to see if it has the [NotUndoable()] attribute. If so, we need to ignore it. Finally, we need to be aware that variable names may not be unique within our object.

In fact, it's the last part that complicates matters the most. To see what I mean, consider the following two classes:

  public class BaseClass {   int _id; } public class SubClass : BaseClass {   int _id; }  

Here, each class has its own variable named _id , and in most circumstances that's not a problem. However, if we use reflection to walk through all the variables in a SubClass object, we'll find two _id variables: one for each of the classes in the inheritance hierarchy.

To get an accurate snapshot of our object's data, we need to accommodate this scenario. In practice, this means that we need to prefix each variable name with the name of the class to which it belongs. Instead of two _id variables, we have BaseClass._id and SubClass._id . The use of a period for a separator is arbitrary, but we do need some character to separate the class name from the variable name.

As if this weren't complex enough, reflection works differently with classes that are subclassed from other classes in the same assembly, from when a class is subclassed from a class in a different assembly. If our example BaseClass and SubClass are in the same assembly, we can use one technique, but if they're in different assemblies, we need to use a different technique. Of course, our code should deal with both scenarios, so the business developer doesn't have to worry about these details.

The following CopyState() method deals with all of the preceding issues. We'll walk through how it works after the listing.

  protected internal void CopyState()     {       Type currentType = this.GetType();       Hashtable state = new Hashtable();       FieldInfo[] fields;       string fieldName;     do     {       // get the list of fields in this type       fields = currentType.GetFields(         BindingFlags.NonPublic          BindingFlags.Instance          BindingFlags.Public);       foreach(FieldInfo field in fields)       {         // make sure we process only our variables         if(field.DeclaringType == currentType)   {         // see if this field is marked as not undoable         if(!NotUndoableField(field))         {           // the field is undoable, so it needs to be processed           Object value = field.GetValue(this);           if(field.FieldType.IsSubClassOf(CollectionType))           {             // make sure the variable has a value             if(!(value == null))             {               // this is a child collection, cascade the call               BusinessCollectionBase tmp = (BusinessCollectionBase)value;               tmp.CopyState();             }           }           else           {             if(field.FieldType.IsSubClassOf(BusinessType))             {               // make sure the variable has a value               if(!(value == null))               {                 // this is a child object, cascade the call                 BusinessBase tmp = (BusinessBase)value;                 tmp.CopyState();               }             }             else             {               // this is a normal field, simply trap the value               fieldName = field.DeclaringType.Name + "." + field.Name;               state.Add(fieldName, value);             }           }         }       }     }     currentType = currentType.BaseType;   } while(currentType != UndoableType);   // serialize the state and stack it   MemoryStream buffer = new MemoryStream();   BinaryFormatter formatter = new BinaryFormatter();   formatter.Serialize(buffer, state);   _stateStack.Push(buffer.ToArray()); }  

This method is scoped as protected internal , which is a bit unusual. The method needs protected scope because BusinessBase will subclass UndoableBase , and its BeginEdit() method will need to call CopyState() . That part is fairly straightforward.

The method also needs internal scope, however, because child business objects will be contained within business collections. When a collection needs to take a snapshot of its data, what that really means is that the objects within the collection need to take snapshots of their data. BusinessCollectionBase will include code that goes through all the business objects it contains, telling each business object to take a snapshot of its state. This will be done via the CopyState() method, which means that BusinessCollectionBase needs the ability to call this method too. Since it's in the same project, we can accomplish this with internal scope.

To take a snapshot of data, we need somewhere to store the various field values before we push them onto the stack. For this purpose, we're using a Hashtable , which allows us to store name-value pairs. It also provides high-speed access to values based on their name, which will be important for our undo implementation. Finally, the Hashtable object supports .NET serialization, which means that we can serialize the Hashtable and pass it by value across the network as part of our overall business object.

The routine we've written here is essentially a big loop that starts with the outermost class in our inheritance hierarchy and walks back up through the chain of classes until it gets to UndoableBase . At that point, we can stopwe know that we have a snapshot of all our business data.

Getting a List of Fields

It's inside the loop that we do all the real work. First, we get a list of all the fields corresponding to the current class:

 // get the list of fields in this type         fields = currentType.GetFields(           BindingFlags.NonPublic            BindingFlags.Instance            BindingFlags.Public); 

We don't care whether the fields are public we want all of them regardless. What's more important is that we want only instance variables, not those declared as static . The result of this call is an array of FieldInfo objects, each of which corresponds to a variable in our object.

Avoiding Double-Processing of Fields

As we discussed earlier, our array could include variables from the base classes of the current class. Due to the way the Just-in-Time (JIT) compiler optimizes code within the same assembly, if some of our base classes are in the same assembly as our actual business class, we may find the same variable listed in multiple classes! As we walk up the inheritance hierarchy, we could end up processing those variables twice, so as we loop through the array, we look only at the fields that directly belong to the class we're currently processing:

 foreach(FieldInfo field in fields)         {           // make sure we process only our variables           if(field.DeclaringType == currentType) 
Skipping [NotUndoable()] Fields

At this point in the proceedings , we know that we're dealing with a field (variable) within our object that's part of the current class in the inheritance hierarchy. However, we want to take a snapshot of the variable only if it doesn't have the [NotUndoable()] attribute, so we need to check for that:

 // see if this field is marked as not undoable             if(!NotUndoableField(field)) 

Having reached this point, we know that we have a variable that needs to be part of our snapshot, so there are three possibilities: we may have a regular variable, we may have a reference to a child object, or we may have a reference to a collection of child objects.

Cascading the Call to Child Objects or Collections

If we have a reference to a child object (or a collection of child objects), we need to cascade our CopyState() call to that object, so that it can take its own snapshot:

 // the field is undoable, so it needs to be processed               object value = field.GetValue(this);               if(field.FieldType.IsSubClassOf(CollectionType))               {                 // make sure the variable has a value                 if(!(value == null))                 {                   // this is a child collection, cascade the call                   BusinessCollectionBase tmp = (BusinessCollectionBase)value;                   tmp.CopyState();                 }               }               else               {                 if(field.FieldType.IsSubClassOf(BusinessType))                 {                   // make sure the variable has a value                   if(!(value == null))                   {                     // this is a child object, cascade the call                     BusinessBase tmp = (BusinessBase)value;                     tmp.CopyState();                   }                 } 

For our object to "reach into" these objects and manipulate their state would break encapsulation. Instead, therefore, we call the (collection) object's own CopyState() method (which in turn loops through all its child objects calling their CopyState() methods).

Tip 

Of course, the GetValue() method returns everything as type object , so we need to cast the object to Business ( Collection ) Base , so we can call the method.

Later on, when we implement methods to undo or accept any changes, they'll work the same waythat is, they'll cascade the calls through the collection object to all the child objects. This way, all the objects handle undo the same way, without breaking encapsulation.

Taking a Snapshot of a Regular Variable

If we have a regular variable, we can simply store its value into our Hashtable object, associating that value with the combined class name and variable name:

 // this is a normal field, simply trap the value                   fieldName = field.DeclaringType.Name + "." + field.Name;                   state.Add(fieldName, value); 

Note that these "regular" variables might actually be complex types in and of themselves . All we know is that the variable doesn't contain an object derived from BusinessCollectionBase or BusinessBase . It could be a simple value such as an int or string , or it could be a complex object (as long as that object is marked as [Serializable()] ).

After we've gone through every variable for every class in our inheritance hierarchy, our Hashtable will contain a complete snapshot of all the data in our business object.

Tip 

Note that this snapshot will include some variables that we'll be putting into the BusinessBase class to keep track of the object's status (such as whether it's new, dirty, deleted, etc.). The snapshot will also include the collection of broken rules that we'll implement later. An undo operation will restore the object to its previous state in every way.

Serializing and Stacking the Hashtable

At this point, we have our snapshot, but it's in a complex data type: a Hashtable . To further complicate matters, some of the elements contained in the Hashtable might be references to more complex objects. In that case, the Hashtable just has a reference to the existing object, not a copy or a snapshot at all.

Fortunately, there's an easy answer to both issues. We can use .NET serialization to convert the Hashtable to a byte stream, reducing it from a complex data type into a very simple one for storage. Better yet, the very process of serializing the Hashtable will automatically serialize any objects to which it has references.

This does require that all objects referenced by our business objects must be marked as [Serializable()] , so that they can be included in the byte stream. If we don't do that, the serialization attempt will result in a runtime error. Alternatively, we can mark any objects that can't be serialized as [NotUndoable()] , so that the undo process simply ignores them.

The code to do the serialization is fairly straightforward:

 // serialize the state and stack it       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, state); 

The MemoryStream object is our byte stream in memory. The BinaryFormatter object does the actual serialization, converting the Hashtable (and any objects to which it refers) into a stream of bytes. Once we have the byte stream, we can simply convert it to a byte array and put that on the stack:

 _stateStack.Push(buffer.ToArray()); 

Converting a MemoryStream to a byte array is a nonissue, since the MemoryStream already contains a byte array with its data. It just provides us with a reference to that existing array, so no data is copied .

The act of conversion to a byte array is important, however, because a byte array is serializable, while a MemoryStream object is not. If we need to pass our object across the network by value while it is being edited , we need to ensure that the snapshots in our stack can be serialized by .NET.

Tip 

We don't anticipate passing our objects across the network while in the middle of being edited, but since our business object is [Serializable()] , we can't prevent the business developer from doing just that. If we were to reference a MemoryStream , the business application would get a runtime error as the serialization failed, and that's not acceptable. By converting the data to a byte array, we avoid accidentally crashing the application on the off chance that the business developer does decide to pass our object across the network as it's being edited.

At this point, we're a third of the way through. We can create a stack of snapshots of our object's data, so now we need to move on and implement the undo and accept operations.

UndoChanges

The UndoChanges() method is the reverse of CopyState() . It takes a snapshot of data off the stack, deserializes it back into a Hashtable , and then takes each value from the Hashtable and restores it into the appropriate object variable.

We've already solved the hard issues of walking through the types in our inheritance hierarchy and retrieving all the fields in the objectwe had to deal with those when we implemented CopyState() . The structure of UndoChanges() will therefore be virtually identical, except that we'll be restoring variable values rather than taking a snapshot.

EditLevel Property

Before we get under way, the one thing we do need to keep in mind is that we need to program defensively. The business developer might accidentally write code to call our UndoChanges() method at times when we have no state to restore! In that case, we obviously can't do any work, so to detect it we can implement an EditLevel property to indicate how many snapshot elements are on our Stack object:

  protected int EditLevel     {       get       {         return _stateStack.Count;       }     }  

The property is declared as protected because we'll also use it in the BusinessBase class, later in the chapter.

UndoChanges Method

Here's the code for the UndoChanges() method itself:

  protected internal void UndoChanges()     {       // if we are a child object we might be asked to       // undo below the level where we stacked states,       // so just do nothing in that case       if(EditLevel > 0)       {         MemoryStream buffer = new MemoryStream((byte[])_stateStack.Pop());         buffer.Position = 0;         BinaryFormatter formatter = new BinaryFormatter();         Hashtable state = (Hashtable)(formatter.Deserialize(buffer));         Type currentType = this.GetType();         FieldInfo[] fields;         string fieldName;   do         {           // get the list of fields in this type           fields = currentType.GetFields(             BindingFlags.NonPublic              BindingFlags.Instance              BindingFlags.Public);           foreach(FieldInfo field in fields)           {             if(field.DeclaringType == currentType)             {               // see if the field is undoable or not               if(!NotUndoableField(field))               {                 // the field is undoable, so restore its value                 object value = field.GetValue(this);                 if(field.FieldType.IsSubClassOf(CollectionType))                 {                   // make sure the variable has a value                   if(!(value == null))                   {                     // this is a child collection, cascade the call                     BusinessCollectionBase tmp = (BusinessCollectionBase)value;                     tmp.UndoChanges();                   }                 }                 else                 {                   if(field.FieldType.IsSubClassOf(BusinessType))                   {                     // make sure the variable has a value                     if(!(value == null))                     {                       // this is a child object, cascade the call                       BusinessBase tmp = (BusinessBase)value;                       tmp.UndoChanges();                     }                   }                   else                   {                     // this is a regular field, restore its value                     fieldName = field.DeclaringType.Name + "." + field.Name;                     field.SetValue(this, state[fieldName]);                   }                 }               }             }           }   currentType = currentType.BaseType;         } while(currentType != UndoableType);       }     }  
Re-creating the Hashtable Object

First, we check to make sure that there's data on the stack for us to restore. Once that's done, we pop the most recently added snapshot off the stack and deserialize it to re-create the Hashtable object containing the detailed values:

 MemoryStream buffer = new MemoryStream((byte[])_stateStack.Pop());         buffer.Position = 0;         BinaryFormatter formatter = new BinaryFormatter();         Hashtable state = (Hashtable)(formatter.Deserialize(buffer)); 

This is the reverse of the process that we used to put the Hashtable onto the stack in the first place. We take the byte array off the stack and use it to create a new MemoryStream object. We then make sure that the MemoryStream 's internal cursor is set to the start of the data, at which point we can use a BinaryFormatter object to deserialize the data. The result of that process is a Hashtable , which is what we started with in the first place.

Restoring the Object's State Data

Now that we have the Hashtable that contains the original object values, we can implement the same kind of loop as we did for CopyState() to walk through the variables in our object. If we encounter child business objects or business object collections, we cascade the UndoChanges() call to those objects so that they can do their own restore operation. Again, this is done to preserve encapsulationonly the code within a given object should manipulate that object's data.

When we encounter a "normal" variable, we restore its value from the Hashtable :

 // this is a regular field, restore its value                   fieldName = field.DeclaringType.Name + "." + field.Name;                   field.SetValue(this, state[fieldName]); 

At the end of this process, we'll have totally reversed any changes made to the object since the most recent snapshot was taken. All we have to do now is implement a method to accept changes, rather than to undo them.

AcceptChanges

AcceptChanges() is actually the simplest of the three methods. If we're accepting changes, it means that the current values in the object are the ones we want to keep and that the most recent snapshot is now meaningless.

In concept, this means that all we need to do is discard the most recent snapshot. However, we also need to remember that our object may have child objects, or collections of child objects, and they need to know to accept changes as well. This means that we need to loop through our object's variables to find any such children and cascade the method call to them too. Here's the code for the method:

  protected internal void AcceptChanges()     {       if(EditLevel > 0)       {         _stateStack.Pop();         Type currentType = this.GetType();         FieldInfo[] fields;     do     {       // get the list of fields in this type       fields = currentType.GetFields(         BindingFlags.NonPublic          BindingFlags.Instance          BindingFlags.Public);         foreach(FieldInfo field in fields)         {           if(field.DeclaringType == currentType)           {             // see if the field is undoable or not             if(!NotUndoableField(field))             {               object value = field.GetValue(this);               // the field is undoable so see if it is a collection               if(field.FieldType.IsSubClassOf(CollectionType))               {                 // make sure the variable has a value                 if(!(value == null))                 {                   // it is a collection so cascade the call                   BusinessCollectionBase tmp = (BusinessCollectionBase)value;                   tmp.AcceptChanges();                 }               }               else               {                 if(field.FieldType.IsSubClassOf(BusinessType))                 {                   // make sure the variable has a value                   if(!(value == null))                   {                     // it is a child object so cascade the call                     BusinessBase tmp = (BusinessBase)value;                     tmp.AcceptChanges();                   }   }                 }               }             }           }           currentType = currentType.BaseType;         }         while(currentType != UndoableType);       }     }  

First, we ensure that there is data on the stack. If there is, we then remove and discard the most recent snapshot:

 _stateStack.Pop(); 

Then all that remains is to implement the same basic looping structure as before, so that we can scan through all the variables in our object to find any child objects or child collections. If we find them, the AcceptChanges() call is cascaded to that object so it can accept its changes as well.

BusinessBase

With UndoableBase complete, we can implement almost all of the BusinessBase class. (The only bits we'll have to defer are those to do with tracking business rules via the BrokenRules class, which we'll discuss next .)

Tip 

Of course, there will also be a fair amount of code in BusinessBase to support data accessa topic we'll discuss in Chapter 5. In this chapter, we're focusing on the behaviors that support the creation of the user interface and the implementation of non- data-access business logic.

To get under way, add a new class to the CSLA project and name it BusinessBase . We know that it needs to inherit from UndoableBase (and therefore also from BindableBase ), so we can start with this code:

  [Serializable()]   abstract public class BusinessBase : Core.UndoableBase   {   }  

Notice that the class is marked as abstract . It's meaningless to directly create a BusinessBase object. Instead, this class will be used as a base from which business classes and objects can be created.

Tracking Basic Object Status

All business objects have some common behaviors that we can implement in BusinessBase . At the very least, we need to keep track of whether the object has just been created, whether its data has been changed, or whether it has been marked for deletion. We'll also want to keep track of whether it's valid, but we'll implement that later on, when we build the BrokenRules class.

By tracking whether an object is new, we enable the business developer to implement fields that can be changed as an object is being created, but not after it's been saved to the database. For instance, a property on the object that's a primary field in the database can be changed by the user up until the data is actually stored in the database. After that point, it must become read-only. While the business developer could keep track of whether the object is new, it is simpler for our framework to deal with that detail on his behalf .

We'll also use the knowledge of whether the object is new to decide whether to do an insert or an update operation in the data access code. If the object is new, then we're inserting the data; otherwise , we're updating existing data.

As we discussed in Chapter 2, it's also important to keep track of whether the object's data has been changedwe can use this knowledge to optimize our data access. When the user interface (UI) code asks the object to save its data to the database, we can choose to do the save operation only if the object's data has been changed. If it hasn't been changed, there's no reason to update the database with the values it already contains.

Also in Chapter 2, we discussed the two ways of deleting object data from the database. One way is to pass criteria data that identifies the object to the server, so that the server can simply delete the data. The other way is to retrieve the data from the database to construct the object on the client. The UI code can then mark the object as deleted and save it to the server. In this case, our data access code will detect that the object was marked for deletion and remove its data from the database.

These different approaches are valid in different business scenarios, and we're supporting both in our framework so that the business developer can choose between them as appropriate. By tracking whether an object is marked as deleted, we enable the second scenario.

To track these three pieces of information, we'll need some variables. To help keep everything organized, we'll put them, along with their related code, in a new region:

 [Serializable()]   abstract public class BusinessBase : Core.UndoableBase   {  #region IsNew, IsDeleted, IsDirty     // keep track of whether we are new, deleted or dirty     bool _isNew = true;     bool _isDeleted = false;     bool _isDirty = true;     #endregion  

Notice that these variables are not marked as [NotUndoable()] , so they will be stored and restored in the course of any CopyState() , UndoChanges() , or AcceptChanges() method calls. This is important, as these are all elements of our object's state that need to be restored in the case of an undo operation.

Also notice that as a business object is created, we default it to being "new." If we then load it with data from the database, we'll set the _isNew variable to false , but to start with we assume that it's new. Given this, it's also reasonable to assume that a new object isn't marked for deletion. It can be marked for deletion later on, but to start with it's assumed that we don't want to delete this new object.

The object also starts out marked as "dirty." This may sound a bit surprising, but the data in a new object doesn't match anything in the database, and in our terms that means it's dirty. This is important, because we want to make sure that any new object will be saved to the database when requested by the business or UI code. Later on, we'll ensure that we don't try to update an object unless it's dirty, so here we're simply making sure that any new object will get saved appropriately.

IsDirty

Now we can implement the code that uses these variables, the most straightforward piece of which just keeps track of whether any data in our object has changed. There's no way to detect automatically when a variable within our object has been changed, so we must rely on the business developer to tell us when an object has become dirty.

However, there's no way that code from outside the business object should be able to alter this valuewhether the object is dirty should be controlled entirely from within that object. To this end, we'll implement a protected method that allows the business logic within the object to mark the object as dirty.

  protected void MarkDirty()     {       _isDirty = true;       OnIsDirtyChanged();     }  #endregion 

The call to OnIsDirtyChanged() refers back to the code we created in BindableBase . It causes the IsDirtyChanged event to be raised. Since the MarkDirty() method should be called by our business logic anytime that the object's internal state is changed, this means that the IsDirtyChanged event will be raised anytime the object's data changes.

The next thing to realize is that there's never reason for the business logic in an object to mark the object as being "not dirty." If the user requests an undo operation, the object's state will be restored to its previous values, including the _isDirty variable. This means that _isDirty will be restored to false if the object is completely restored to its original state. However, we will need to mark the object as "clean" from within BusinessBase , when the data is updated into the database. We'll create a private method to support this functionality:

  private void MarkClean()     {       _isDirty = false;       OnIsDirtyChanged();     }  #endregion 

Again, we not only set the variable's value, but also call OnIsDirtyChanged() to cause the IsDirtyChanged event to be raised.

Finally, we need to expose a property so that business and UI code can get the value of the flag:

  virtual public bool IsDirty     {       get       {         return _isDirty;       }     }  #endregion 

Notice that this method is marked as virtual . This is important, because sometimes our business objects aren't simply dirty because their own data has changed. For instance, consider a business object that contains a collection of child objects. Even if the business object's data hasn't changed, it will be dirty if any of its child objects has changed. In that case, the business developer will need to override the IsDirty property to provide a more sophisticated implementation. We'll see an example of this in Chapter 6, when we implement our example business objects.

IsDeleted

As we've said a couple of times already, deletion of an object can be done in a couple of ways: deferred or immediate. The deferred approach allows us to load the object into memory, view its data, and manipulate it. At that point, the user may decide to delete the object, in which case we'll mark it for deletion. When the object is then saved to the database, instead of an UPDATE operation, we'll perform a DELETE operation. This approach is particularly useful for child objects, where we may be adding and updating some child objects at the same time as deleting others.

The immediate approach is entirely different. In that case, we don't load the object into memory at all; we simply pass criteria data to the DataPortal , which invokes our business logic to delete the data from the database directly. Immediate deletion is commonly employed where the UI presents the user with a large list of information, such as a list of customers. The user can select one or more customers from the list and then click a Delete button. In that case, there's no reason to retrieve the complete customer objectswe can simply remove them using criteria information.

Support for the immediate approach is provided by the DataPortal , which we'll be discussing in the next chapter. The deferred approach, on the other hand, is something that we'll implement here by using the _isDeleted variable. First, let's create a property to expose the value:

  public bool IsDeleted     {       get       {         return _isDeleted;       }     }  #endregion 

Unlike the IsDirty property, this one isn't virtual , because there's no reason for the business object to change this behavior.

Then, as with _isDirty , we'll expose a protected method so that our business logic can mark the object as deleted when necessary:

  protected void MarkDeleted()     {       _isDeleted = true;       MarkDirty();     }  #endregion 

Of course, marking the object as deleted is another way of changing its data, so we also call the MarkDirty() method to indicate that the object's state has been changed. Later on, we'll implement a public method named Delete() that can be called by the UI code. That method will call the MarkDeleted() method to actually mark the object for deletion.

As with _isDirty , there's no reason for our business logic ever to mark an object as "not deleted." If that behavior is desired, an undo operation can be invoked that will restore our object to its previous state, including resetting the _isDeleted variable to its original false value.

IsNew

The third of our basic object status-tracking properties keeps track of whether the object is new. By "new," we simply mean that the object exists in memory, but not in the database or other persistent store. If the object's data resides in the database, then the object is classed as "old."

Having this piece of information around will allow us to write our data access code very easily later on. It encapsulates the decision surrounding whether to perform an INSERT or an UPDATE operation on the database. Sometimes, it can be useful to the UI developer as well, since some UI behaviors may be different for a new object as compared to an existing object. The ability to edit the object's primary key data is a good examplethis is often editable only up to the point that the data has been stored in the database. When the object becomes "old," the primary key is fixed.

To provide access to this information, we can implement a property:

  public bool IsNew     {       get       {         return _isNew;       }     }  #endregion 

We also need to allow the business developer to indicate whether the object is new or old. However, neither the UI code nor any other code outside our business object should be able to change the value, so we'll create a couple of protected methods:

  protected void MarkNew()     {       _isNew = true;       _isDeleted = false;       MarkDirty();     }     protected void MarkOld()     {       _isNew = false;       MarkClean();     }  #endregion 

After an object has been saved to the database, for instance, the MarkOld() method would be called, since at that point the object's data resides in the database and not just in memory. Notice, however, that we aren't only altering the _isNew variable.

The scenario for marking an object as being new is right after we've completed a delete operation, thereby removing the object's data from the database. This means that not only is the object now new (where it was old before), but also the object in memory is no longer marked for deletionit has, in fact, already been deleted and is now like a brand-new object.

Also, whether we're marking the object as new or old, we need to indicate that its data has been changed. If an object was old and is now marked as new, the object has changed. By definition, the data in the object no longer matches any data in a database, so the MarkNew() method calls MarkDirty() . On the other hand, if we're marking an object as old, it means that we've just synchronized the database and the object, typically because we've just performed an INSERT operation. In this case, the object is now both "old" and "clean"all our data matches the data in the database. To this end, the MarkOld() method calls MarkClean() to mark the object as unchanged.

Object Editing

Moving on from basic state-handling, in UndoableBase we implemented the basic functionality to take snapshots of our object's data and then perform undo or accept operations using those snapshots. Those methods were implemented as protected methods, so they're not available for use by code in the UI. In BusinessBase , we can implement three standard methods for use by the UI code, as described in Table 4-1.

Table 4-1: Object Editing Methods in BusinessBase

Method

Description

BeginEdit()

Initiate editing of the object. Triggers a call to CopyState() .

CancelEdit()

Indicates that the user wants to undo her recent changes. Triggers a call to UndoChanges() .

ApplyEdit()

Indicates that the user wants to keep her recent changes. Triggers a call to AcceptChanges() .

We'll also implement the System.ComponentModel.IEditableObject interface to support data binding. The IEditableObject interface is used by Windows Forms data binding to control the editing of objects. We'll implement this interface later in the chapter.

BeginEdit, CancelEdit, and ApplyEdit Methods

First, let's implement the basic edit methods. Once again, we'll put them in a region to keep them organized:

  #region Begin/Cancel/ApplyEdit     public void BeginEdit()     {       CopyState();     }     public void CancelEdit()     {       _bindingEdit = false;       UndoChanges();     }     public void ApplyEdit()     {       _bindingEdit = false;       _neverCommitted = false;       AcceptChanges();     }     #endregion  

These methods can be called by our UI code as needed. Before allowing the user to edit the object, the UI should call BeginEdit() . If the user then clicks a Cancel button, the CancelEdit() method can be called. If the user clicks a Save or Accept button, then ApplyEdit() can be called.

Calling BeginEdit() multiple times will cause stacking of states. This allows complex hierarchical interfaces to be created, in which each form has its own Cancel button that triggers a call to CancelEdit() .

Nothing requires the use of BeginEdit() , CancelEdit() , or ApplyEdit() . In many web scenarios, as we'll see in Chapter 9, there's no need to use these methods at all. A flat UI with no Cancel button has no requirement for undo functionality, so there's no reason to incur the overhead of taking a snapshot of our object's data. On the other hand, if we're creating a complex Windows Forms UI that involves modal dialog windows to allow editing of child objects (or even grandchild objects), we may choose to call these methods manually to support OK and Cancel buttons on each of the dialog windows.

Object Cloning

Since all business objects created with our framework must be marked as [Serializable()] , and they can reference only other serializable objects, we can easily implement a Clone() method that will completely copy any of our business objects. This will use the same technology that remoting uses to pass objects across the network by value: .NET serialization.

Tip 

The primary reason I'm including this cloning implementation is to reinforce the concept that our objects and any objects they reference must be [Serializable()] . Having implemented a Clone() method as part of our framework, we make it very easy to create a test harness that attempts to clone each of our business objects, clearly establishing that they are, in fact, totally serializable.

The .NET Framework formally supports the concept of cloning through the ICloneable interface, so we'll implement that here:

 [Serializable()]  abstract public class BusinessBase : Core.UndoableBase, ICloneable  

This will require that we implement a Clone() method to make a copy of our object. To use serialization more easily, we need to use a couple of namespaces:

  using System.IO; using System.Runtime.Serialization.Formatters.Binary;  

Then we can write a generic Clone() method that will make an exact copy of our business object (and any objects it references):

  #region ICloneable     public Object Clone()     {       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, this);       buffer.Position = 0;       return formatter.Deserialize(buffer);     }     #endregion  

By implementing the ICloneable interface, we not only provide for easy testing to ensure that our object is serializable, but also potentially allow other parts of the .NET Framework to make copies of our object as needed.

Root and Child Objects

As we discussed in Chapter 2, a business object can be a root object, which means (for our purposes) that it can be retrieved from or stored in a database. Alternatively, it might be a child object, which relies on its parent object to initiate any database retrieval or storage. An example of a root object is an Invoice , while a child object would be a LineItem object within that Invoice .

Child objects are related to root objects via a containment relationship, as illustrated by the UML diagram in Figure 4-8.

image from book
Figure 4-8: Class diagram showing how root, child, and grandchild objects are related

As we hinted at when describing BusinessBase in Chapter 2, there are some objects that act as a root object for some of the time and some that act as a child object for some of the time. Consider, for instance, a Posting object that represents a quantity to be posted to a customer's account. Sometimes we may have a single Posting object that's created and saved. Other times we may have a broader Adjustment object that contains many child Posting objects.

We'll support all three scenariosroot, child, and combinationin BusinessBase by allowing the business programmer to make the choice through code. The key will lie in a MarkAsChild() method. If this method is not called, the object will be a root object. If it is called, the object will be a child object. The business developer can choose when and if she should call MarkAsChild() , thus giving her complete control over this feature.

Sometimes, we'll have objects that are loaded directly from the database as root objects on some occasions, but loaded as part of another object on others. This is entirely driven by the business requirements. For instance, consider an OpenOrder object. There are times when a user will need to be able to create and interact with an OpenOrder object directly, in which case it would be a root object. At other times, the user will be working with a Customer object that contains a collection of that customer's OpenOrder objects, each of which would then be a child object. This is a "combination" objectone that can be set to root or child, depending on how it is created.

If an object is configured as a root object, we'll allow certain behaviors that apply to root objects and disable some that apply only to child objects. Conversely, if the object is configured as a child object, we'll disable root-level features such as data access, while enabling child-only behaviors. We'll control whether an object is a root or a child with a simple flag, property, and protected method, much as we did with _isDirty earlier on:

  #region IsChild     [NotUndoable()]     bool _isChild = false;     internal bool IsChild     {       get       {         return _isChild;       }     }     protected void MarkAsChild()     {       _isChild = true;     }     #endregion  

By default, objects are assumed to be root objects. Only if the business developer calls the MarkAsChild() method while the object is being created does it become a child object. We'll see how this works when we implement some business objects in Chapter 7.

The reason we're implementing this as a method rather than as a read-write property is because this is a one-time thing. As the object is created, either we mark it as a child or we don'tthe value can't be changed later in the object's life.

Note that the _isChild variable is marked with the [NotUndoable()] attribute. This is because it can't be changed after the object is created (that is, the code object, rather than the object in the database), so there's no reason to take a snapshot of its value. Since the value can't change, we'd be copying the value just so that we could undo it to the same value later, which is a waste of memory.

The IsChild property will be used within other BusinessBase code and may be useful to the business developer, so it's declared as protected .

Deleting Objects

Earlier, we implemented an IsDeleted property to support the idea of deferred deletion that is, when an object is marked as deleted but isn't actually deleted until it's "stored" to the database. The MarkDeleted() method we implemented was protected in scope, for use by the business logic in the object to mark the object as being deleted.

We also need to provide a public method named Delete() for the UI to call on our root business object to mark it for deletion. The root object will need to also mark any child objects as deleted, so we'll provide a DeleteChild() method with internal scope for that purpose. The process is illustrated by the sequence diagram in Figure 4-9.

image from book
Figure 4-9: Process of deleting a root object and all its children

We can see that the UI code calls the root object's Delete() method, thus marking it for deletion. This also causes the root object to call a DeleteChild() method on its collection of child objects. The collection then loops through all the child objects, calling each of their DeleteChild() methods so that they can mark themselves for deletion as well.

For root objects, we provide a Delete() method that's for use by the UI code:

  #region Delete     public void Delete()     {       if(this.IsChild)         throw new NotSupportedException(           "Cannot directly mark a child object for deletion " +           "- use its parent collection");       MarkDeleted();     }     #endregion  

First, we check to ensure that this is not a child object. If it is a child object, we throw an exception to indicate that this method isn't valid in that scenario. If it's a root object, then we simply call the MarkDeleted() method to mark the object for deletion. It's then up to our data access code to notice that _isDeleted is true and actually perform the delete.

Child objects can be marked for deletion as well, but they can't be marked for deletion directly . Instead, they must be deleted by their parent object. Typically, the parent is a collection object that inherits from BusinessCollectionBase , and we're not really "deleting" the child object as much as we're "removing it from the collection." The deletion is just a side effect of the object being removed from the collection.

To support this, we'll also implement a DeleteChild() method:

  // allow the parent object to delete us     // (internal scope)     internal void DeleteChild()     {       if(!this.IsChild)         throw new NotSupportedException("Invalid for root objects " +           "- use Delete instead");       MarkDeleted();     }  #endregion 

This method does basically the same thing as Delete() , but it's only accessible if the object is a child object. Also, it's declared as internal , so it will be available only to other code within the CSLA assembly. We specifically want it to be available to BusinessCollectionBase , as the latter will call this method in the implementation that we'll create later in this chapter.

Edit Level Tracking for Child Objects

When we implement BusinessCollectionBase , our code will make certain demands of each child business object. We've already seen something like this in action with the DeleteChild() method, but we need to support one other feature, too.

As we'll see in the implementation of BusinessCollectionBase , n-Level undo of collections of child objects is pretty complex. The biggest of several problems arises when a new child object is added to the collection, and then the collection's parent object is " canceled ." In that case, the child object must be removed from the collection as though it were never therethe collection must be reset to its original state. To support this, child objects must keep track of the edit level at which they were added.

Earlier, as we implemented UndoableBase , we implemented a very short EditLevel property that returned a number corresponding to the number of times the object's state had been copied for later undo. From a UI programmer's perspective, the edit level is the number of times BeginEdit() has been called, minus the number of times CancelEdit() or ApplyEdit() has been called.

An example might help. Suppose that we have an Invoice object with a collection of LineItem objects. If we call BeginEdit() on the Invoice , then its edit level is 1. Since it cascades that call down to the child collection, the collection and all child objects are also at edit level 1.

If we then add a new child object, it would be added at edit level 1, but if we then cancel the Invoice , we expect its state to be restored to what it was originallyeffectively, back to the level 0 state. Of course, this includes the child collection, which means that the collection somehow needs to realize that our new child object should be discarded. To do this, the BusinessCollectionBase code will loop through its child objects looking for any that were added at an edit level that's higher than the current edit level.

In our example, when Invoice is canceled, its edit level immediately goes to 0. It cascades that call to the child collection, which then also has an edit level of 0. The collection scans its child objects looking for any that were added at an edit level greater than 0 and finds our new child object that was added at edit level 1. It then knows that this child object can be removed.

This implies that our business objectsif they're child objectsmust keep track of the edit level at which they were added. To do this, we'll add a simple variable, with a internal property to retrieve its value. We'll also add an internal property that will be used by BusinessCollectionBase to set the child object's initial edit level:

  #region Edit Level Tracking (child only)     // we need to keep track of the edit     // level when we were added so if the user     // cancels below that level we can be destroyed     int _editLevelAdded;   // allow the collection object to use the     // edit level as needed (internal scope)     internal int EditLevelAdded     {       get       {         return _editLevelAdded;       }       set       {         _editLevelAdded = value;       }     }     #endregion  

The purpose and use for this functionality will become much clearer as we implement the BusinessCollectionBase class later in this chapter, but at this point the BusinessBase class is completeexcept for one major feature. We also want to automate the tracking of simple business rules to make it easy for the business developer to determine whether the object is valid at any given time.

IEditableObject Interface

Earlier in the chapter we implemented BeginEdit() , CancelEdit() , and ApplyEdit() methods so our business objects could support undo functionality. Related to this is the IEditableObject interface. This interface is used by the Windows Forms data-binding infrastructure to control undo operations in two cases:

  • First, if our object is a child of a collection and is being edited in a DataGrid control, the IEditableObject interface will be used so that the user can start editing a row of the grid (that is, one of our objects) and then press Esc to undo any edits he's made on the row.

  • Second, if we bind controls from a Windows Form to our object's properties, the IEditableObject interface will be used to tell us that editing has started . It will not be used to tell us when editing is complete or if the user requests an undo. It's up to our UI code to handle these cases.

If we are using data binding to bind our object to a form, we can allow the data- binding infrastructure to tell us that editing has started. I typically don't rely on that feature, preferring to call BeginEdit() myself . Since I have to call CancelEdit() and ApplyEdit() manually anyway, I prefer simply to control the entire process.

IEditableObject is most important when our object is being edited within a DataGrid control. In that case, this interface is the only way to get the editing behavior that's expected by users. The IEditableObject interface comes from the System.ComponentModel namespace, so we'll use that first:

  using System.ComponentModel;  

Then we can indicate that we want to implement the interface:

 [Serializable()]  abstract public class BusinessBase : Core.UndoableBase,                                           IEditableObject, ICloneable  

Clearly, implementing the interface requires that we understand how it works. The interface defines three methods, as described in Table 4-2.

Table 4-2: IEditableObject Interface Methods

Method

Description

BeginEdit()

This is called to indicate the start of an edit process. However, it may be called by the Windows Forms data-binding infrastructure many times during the same edit process, and only the first call should be honored.

CancelEdit()

This is called to indicate that any changes since the first BeginEdit() call should be undone. However, it may be called by the Windows Forms data-binding infrastructure many times during the same edit process, and only the first call should be honored.

EndEdit()

This is called to indicate that the edit process is complete, and that any changes should be kept intact. However, it may be called by the Windows Forms data-binding infrastructure many times during the same edit process, and only the first call should be honored.

Note 

The official Microsoft documentation on these methods is somewhat inconsistent with their actual behavior. In the documentation, only BeginEdit() is noted for being called multiple times, but experience has shown that any of these methods may be called multiple times.

While these methods are certainly similar to our existing edit methods, there are some key differences in the way these new methods work. Consider BeginEdit() , for example. Every call to our existing BeginEdit() method will result in a new snapshot of our object's state, while only the first call to IEditableObject.BeginEdit() should be honored. Any subsequent calls (and they do happen during data binding) should be ignored. The same is true for the other two methods.

Tip 

When we bind an object's properties to controls on a Windows Form, we'll get many calls to the IEditableObject methods from the Windows Forms data-binding infrastructure. In the .NET Framework documentation, it's made quite clear that only the first such call should be honored, so that's what we'll implement here.

So that we can implement the behavior of the IEditableObject methods properly, we need to keep track of whether the edit process has been started and when it ends. At the same time, though, we want to preserve our existing BeginEdit() functionality. To this end, we'll implement separate methods to implement IEditableObject , and then we'll have them call our existing methods when appropriate.

There is one other issue we need to deal with as well. When a collection of objects is bound to a Windows Forms DataGrid control (or similar grid controls), the user can dynamically add and remove child objects in the collection by using the grid control. When an object is removed in this manner, the grid control does not notify the collection object. Instead, it notifies the child object, and it's up to the child object to remove itself from the collection.

This is quite different from the approach we've taken thus far, where the collection is in control of the process. This is because Windows Forms data binding uses only single-level undo, while we're supporting n-Level undo. We need to bridge this gap to have the IEditableObject interface work as expected. The easiest way to do this is to have the child object contact its collection to have itself removed. For this to happen, the child object needs a reference to the collection.

We'll add code in BusinessBase and later in BusinessCollectionBase to manage this parent reference.

Put this code in a new region:

  #region IEditableObject     [NotUndoable()]     BusinessCollectionBase _parent;     [NotUndoable()]     bool _bindingEdit = false;     bool _neverCommitted = true;     internal void SetParent(BusinessCollectionBase parent)     {       if(!IsChild)         throw new Exception("Parent value can only be set for child objects");       _parent = parent;     }     void IEditableObject.BeginEdit()     {       if(!_bindingEdit)         BeginEdit();     }   void IEditableObject.CancelEdit()     {       if(_bindingEdit)       {         CancelEdit();         if(IsNew && _neverCommitted && EditLevel <= EditLevelAdded)         {           // we're new and no EndEdit or ApplyEdit has ever been           // called on us, and now we've been canceled back to           // where we were added so we should have ourselves           // removed from the parent collection           if(!(_parent == null))             _parent.RemoveChild(this);         }       }     }     void IEditableObject.EndEdit()     {       if(_bindingEdit)         ApplyEdit();       }     #endregion  

Notice that all three methods call the corresponding edit method we've already implemented. We use the _bindingEdit variable to determine whether the BeginEdit() method has been called already so we know whether to honor subsequent method calls.

There's a _neverCommitted variable that tracks whether the ApplyEdit() method has ever been called. If it hasn't ever been called and data binding tells us to cancel the object, we can detect that the object should be automatically removed from the parent collection.

Also notice the _parent variable. It is marked as [NotUndoable()] because the parent value won't change as we edit or undo the object's state.

Note 

It's important to mark the _parent variable as [NotUndoable()] because otherwise we'll introduce a circular reference that the CopyState() method in UndoableBase won't be able to handle.

The _parent variable is set through the SetParent() method, which will be called by the parent collection object. When we implement BusinessCollectionBase we'll make use of this method. Also note that our code calls a RemoveChild() method, which we'll have to implement in BusinessCollectionBase . Right now this code will show as being in error, since we haven't implemented the method yet.

To complete our functionality, we need to enhance our existing BeginEdit() , CancelEdit() , and ApplyEdit() methods to properly manipulate the _bindingEdit value.

 #region Begin/Cancel/ApplyEdit     public void BeginEdit()     {  _bindingEdit = true;  CopyState();     }     public void CancelEdit()     {  _bindingEdit = false;  UndoChanges();     }     public void ApplyEdit()     {  _bindingEdit = false;       _neverCommitted = false;  AcceptChanges();     }     #endregion 

The _bindingEdit variable is set to true when an edit process is started and to false when either CancelEdit() or ApplyEdit() is called. The _neverCommitted variable starts out true and is set to false if ApplyEdit() is called. With this mechanism in place, the implementation of IEditableObject.BeginEdit() calls only the real BeginEdit() method if no edit session is currently under way.

Notice that _bindingEdit is declared with the [NotUndoable()] attribute. This variable controls interaction with the UI, not internal object state, and because of this there's no reason to make it part of the object's snapshot data, as that would just waste memory.

With the implementation of the edit methods and IEditableObject , we now provide full control over our object's editing and undo capabilities, both to the UI developer and to Windows Forms data binding.

BrokenRules

As we discussed in Chapter 2, most business objects will be validating data based on various business rules. In many cases, if any of those rules are broken, the business object is in an invalid state. Typically, this means that the object shouldn't be saved to a database until the data has been changed so that the rule is no longer broken.

Obviously, our framework can't implement the actual business rules and validation codethat will vary from application to applicationbut we can simplify the business developer's life by keeping track of whether there are any rules currently broken. We can keep a list of the rules that are broken, and we can prevent the object from being saved to the database via our data portal mechanism if that list has any entries. Also, if we have a list of broken rules, we can expose a read-only version of that list to the UI, so the UI can display the descriptions of the broken rules to the user.

Tip 

Displaying the list of broken rules to the user was a common enhancement to the architecture from my Visual Basic 5 and 6 Business Objects books. So many readers wrote in that they'd done this that I've included it directly into the framework in this book.

It's important to realize that the BrokenRules collection doesn't enforce any rules. It merely allows the business logic to maintain a list of the business rules that are broken. It is entirely up to the business developer to implement those rules and to tell the BrokenRules collection when each rule is broken or unbroken. This will be done by writing logic within a business object that goes something like this:

 BrokenRules.Assert("NameReq",         "Name is a required field", value.Length == 0); 

If the length of the string variable called value is zero, then the rule will be marked as broken and it will be added to the list. Otherwise, it will be considered unbroken, and if it is in the list it will be removed.

The BrokenRules object is a container for a collection of elements. Each element corresponds to a business rule that is currently broken, and includes a key name and a description for that rule. At first, this seems like a pretty simple concept: just a collection of elements. However, we have some special requirements, in that the collection should be read-write for our business object, but read-only for the UI code.

Tip 

There's great value in exposing a read-only list to the UI code, because this makes it very easy for the UI developer to display descriptions of all broken rules to the user. In fact, since this is a collection, we can even use data binding to display the information in a control.

The challenge is that there's no easy way to make a regular collection object that's read-only in one case and read-write in another. While we could pass a copy of the data to the client, then we'd lose the live updates that we'll get by providing direct access to the data. By providing live data, data binding will automatically pick up changes to the list of broken rules so the user's display can be updated.

To solve this problem, we'll create a special RulesCollection class that implements this behavior. We'll also define a Rule data type that defines the data we want to keep about each broken rule. Finally, the BrokenRules class itself will provide an easy interface for use by our business logic to mark rules as broken and unbroken.

To keep our CSLA namespace from becoming cluttered, we'll nest the Rule data type and the RulesCollection class within the BrokenRules class. This means that they'll be addressed as follows :

 CSLA.BrokenRules.Rule CSLA.BrokenRules.RulesCollection 

Add a new class to the CSLA project and name it BrokenRules . Mark it as being [Serializable()] . BusinessBase will be referring to an object of this type, and since we'll be serializing our business objects, any other objects to which they refer must also be serializable:

  [Serializable()]  public class BrokenRules   {   } 

This means that our business objects can be passed across the network by value, bringing their list of broken rules along for the ride. It also means that the list of broken rules is considered to be part of our object's state, so it will be serialized when BeginEdit() is called and restored in the case of CancelEdit() .

Rule Data Type

We'll start the real work by creating the Rule data type. This could be a simple structure, but for one problem: Web Forms data binding can't bind to a regular structure. The data binding in Web Forms only binds to public read-write properties, not to public fields. Windows Forms doesn't have this limitation, but we want to support data binding in either environment.

To put this issue into code, we can't simply create the data type like this:

 #region Rule structure   public struct Rule   {     public string Rule;     public string Description;   }   #endregion 

Instead, we need to declare the fields as private and implement properties to expose them for use. In the end, this works out well, since we want more control over the process anyway. If we simply expose the fields, then the UI code could change them. By implementing properties, we ensure that the UI can't change the data values.

We can also implement an internal constructor, which achieves two things. First, since it's of internal scope, it prevents the UI code (or our business object code) from creating a Rule directlythe only way to do that from "outside" will be through our BrokenRules class. Second, implementing this constructor will simplify the code that we write in BrokenRules , allowing us to create and insert a new rule in a single line.

Here's the code for the Rule data type. Place it inside the BrokenRules class.

 public class BrokenRules {  #region Rule structure     [Serializable()]       public struct Rule     {       string _name;       string _description;       internal Rule(string name, string description)       {         _name = name;         _description = description;       }       public string Name       {         get         {           return _name;         }         set         {           // the property must be read-write for Web Forms data binding           // to work, but we really don't want to allow the value to be           // changed dynamically so we ignore any attempt to set it         }       }       public string Description       {         get         {           return _description;         }         set         {           // the property must be read-write for Web Forms data binding           // to work, but we really don't want to allow the value to be           // changed dynamically so we ignore any attempt to set it         }       }     }     #endregion  } 

Note that the structure is marked as [Serializable()] , which is important for the same reasons that BrokenRules itself is marked as such. Also note that the two properties are read-write, but they don't implement the set portion of the method:

 set         {           // the property must be read-write for Web Forms data binding           // to work, but we really don't want to allow the value to be           // changed dynamically so we ignore any attempt to set it         } 

This is important . Data binding requires that these properties should be read-write, but at the same time we don't want to allow the UI code (or data binding) to alter the values of our variables. The workaround we've used meets both criteria: it allows data binding to work, while totally ignoring any attempt by the UI or data-binding code to alter the values.

RulesCollection

The .NET Framework makes it relatively easy to create a strongly typed collection object. All we need to do is subclass CollectionBase ; implement our own indexer, Add() , and Remove() methods; and we're on our way. When we subclass CollectionBase , we gain access to a protected variable called List , which gives us access to the underlying collection of values that's managed by the .NET Framework base class.

In our case, we also want to support data binding, so we'll subclass from our own BindableCollectionBase instead, but the process is the same.

Unfortunately, our requirements have posed something of a problem on this occasion. The collection we need here is very different: it needs to be read-write when called by our internal BrokenRules code, but read-only when called by the UI. This requires some extra coding on our part. To start with, let's create the basic collection functionality by inserting the following code inside the BrokenRules class:

  #region RulesCollection     [Serializable()]     public class RulesCollection : CSLA.Core.BindableCollectionBase     {       internal RulesCollection()     {       AllowEdit = false;       AllowRemove = false;       AllowNew = false;     }   public Rule this [int index]     {       get       {         return (Rule)List[index];       }     }     internal void Add(string name, string description)     {       Remove(name);       List.Add(new Rule(name, description));     }     internal void Remove(string name)     {       // we loop through using a numeric counter because       // the base class Remove requires a numeric index       for(int index = 0; index < List.Count; index++)         if(((Rule)List[index]).Name == name)         {           List.Remove(List[index]);           break;         }       }       internal bool Contains(string name)       {         for(int index = 0; index < List.Count; index++)           if(((Rule)List[index]).Name == name)             return true;         return false;       }     }     #endregion  

The New() , Add() , Remove() , and Contains() methods are all declared here with internal scope. This means that they won't be available to our business object code or UI code, but they will be available to the BrokenRules code that we'll implement shortly.

The indexer property is read-only and public , so it provides read-only access to the list of Rule objects to both our business object code and the UI code. Also notice that the constructor sets the three values we defined in BindableCollectionBase , so data binding will know that it can't add, remove, or edit items in this collection.

The trouble is that none of these steps will stop the UI developer from calling Clear() or RemoveAt() on our collection. Unfortunately, these two methods are exposed by the CollectionBase class, and they're available at all times. We need some way to prevent these methods from working and yet still allow BrokenRules to use our Add() and Remove() methods.

To solve the problem, we'll override some special methods on the base class that allow us to intercept any attempt to remove items or clear the collection. Of course, we want to block those activities only when they're attempted by the UI code, not when our code is working with the collection. This means we need to be able to lock and unlock the collection, so that we can unlock it when we want to use it and lock it the rest of the time.

To lock the collection, we'll add a variable to indicate whether adding, removing, or changing collection values is legal. Essentially, it will be legal if the request is made through our Add() or Remove() methods; otherwise, it will be illegal.

 [Serializable()]     public class RulesCollection : CSLA.Core.BindableCollectionBase     {  bool _validToEdit = false;  internal RulesCollection()       {         AllowEdit = false;         AllowRemove = false;         AllowNew = false;       }       public Rule this [int index]       {         get         {           return (Rule)List[index];         }       }       internal void Add(string name, string description)       {         Remove(name);  _validToEdit = true;  List.Add(new Rule(name, description));  _validToEdit = false;  }       internal void Remove(string name)       {         // we loop through using a numeric counter because         // the base class Remove requires a numeric index  _validToEdit = true;  for(int index = 0; index < List.Count; index++)           if(((Rule)List[index]).Name == name)           {             List.Remove(List[index]);             break;           }  _validToEdit = false;  }.       internal bool Contains(string name)       {         for(int index = 0; index < List.Count; index++)           if(((Rule)List[index]).Name == name)             return true;         return false;       } 

This allows us to solve the problem. The CollectionBase class will call a method before any insert, remove, clear, or change operation. We can override these methods, raising an error if the operation is invalid:

  protected override void OnClear()       {         if(!_validToEdit)           throw new NotSupportedException("Clear is an invalid operation");       }       protected override void OnInsert(int index, Object val)       {         if(!_validToEdit)           throw new NotSupportedException("Insert is an invalid operation");       }       protected override void OnRemove(int index, Object val)       {         if(!_validToEdit)           throw new NotSupportedException("Remove is an invalid operation");       }       protected override void OnSet(int index, Object oldValue, Object newValue)       {         if(!_validToEdit)           throw new NotSupportedException(                                   "Changing an element is an invalid operation");       }  }     #endregion 

If our collection is locked and an attempt to change it is made, we throw a NotSupportedException to indicate that the operation isn't supported by our object.

This scheme means that BrokenRules can call our Add() and Remove() methods, and yet we can still provide the UI with a reference to our RulesCollection object without the risk of UI code changing the collection's contents.

Since we're subclassing BindableCollectionBase , the UI can employ data binding to display the rule descriptions, or it can include code to loop manually through the collection to process or display the data.

BrokenRules

We now have everything we need to keep a collection of broken rules. Rather than having our business logic work directly with the collection, however, we'll implement a more abstract BrokenRules class that provides an easy-to-use interface through which rules can simply be marked as broken or unbroken.

The methods of BrokenRules will only be available to code in our business objects, so our goal here is to create a simple interface that business developers can use to keep track of their business rule status. First off, let's declare the collection object and create a method to allow the marking of a rule as broken or unbroken. Add the following to the BrokenRules class:

  RulesCollection _rules = new RulesCollection();     public void Assert(string name, string description, bool isBroken)     {       if(isBroken)         _rules.Add(name, description);       else         _rules.Remove(name);     }  

This method will be used by our business logic, so we can write code in our business objects that looks something like this:

 public string Name   {     get     {       return _name;     }     set     {       BrokenRules.Assert("NameReq",                "Name is a required field", value.Length == 0);       _name = value;       MarkDirty();     }   } 

Obviously, the key line here is the call to Assert() , which will set the rule as broken if the new value is of zero length or as unbroken if a longer value is supplied. This makes the implementation of many business rule checks very simple for the business developer.

We should also allow the business object to determine quickly whether any rules are broken. If not, the object is assumed to be valid. To do this, we'll add an IsValid property to the BrokenRules class:

  public bool IsValid     {       get       {         return (_rules.Count == 0);       }     }  

We'll also allow the business logic to ask if a specific rule is currently broken:

  public bool IsBroken(string name)     {       return _rules.Contains(name);     }  

Both the business logic and the UI logic should have access to the collection. We went to great lengths to make the RulesCollection object read-write for BrokenRules , but read-only for everyone else. This means that we can simply provide a reference to the RulesCollection object, knowing that it will be read-only:

  public RulesCollection BrokenRulesCollection     {       get       {         return _rules;       }     }  

Finally, in case the UI developer wants a simple text representation of the list of broken rules, let's override the ToString() method with our own implementation that returns the list of broken rule descriptions as delimited text that can be displayed in a message box or a multiline text display control:

  public override string ToString()     {       System.Text.StringBuilder obj = new System.Text.StringBuilder();       bool first = true;       foreach(Rule item in _rules)       {         if(first)           first = false;         else           obj.Append(Environment.NewLine);         obj.Append(item.Description);       }       return obj.ToString();     }  

At this point, we have a BrokenRules object that will enable BusinessBase and the business object author to manipulate the list of broken rules, and allow a business object to expose the collection of broken rules to the UI, if so desired.

Exposing BrokenRules from BusinessBase

We can now return to the BusinessBase class and enhance it to make use of BrokenRules . By incorporating BrokenRules support directly into BusinessBase , we're making this functionality available to all business objects created using our framework.

Every business object will have an associated BrokenRules object to keep track of that object's rules. BusinessBase will include the code that declares and configures this object, so add the following to BusinessBase :

  #region BrokenRules, IsValid     // keep a list of broken rules     BrokenRules _brokenRules = new BrokenRules();     #endregion  

We'll expose the RulesCollection collection from the BrokenRules object to the UI directly, as a public property. We can do this because we've taken precautions to ensure that the UI code can't change the contents of the collection. We'll also need to implement an easy way for the business developer to access the BrokenRules object, so he can mark rules as broken or unbroken.

To do this, we'll expose a set of public and protected methods in BusinessBase that expose the appropriate functionality. Figure 4-10 illustrates which objects will be exposed.

image from book
Figure 4-10: Exposing objects through methods in BusinessBase

First, let's expose BrokenRules to the business object itself by adding this code within our new region:

  protected BrokenRules BrokenRules     {       get       {         return _brokenRules;       }     }  

All business objects should expose an IsValid property that can be used to determine whether the object is currently valid. By default, this will simply rely on BrokenRules , but we'll make it virtual so that the business object author can enhance this behavior if required by her specific application:

  virtual public bool IsValid     {       get       {         return _brokenRules.IsValid;       }     }  

Finally, we'll expose read-only data for use by the business logic or UI code. We can expose both the RulesCollection itself and the text version of the rules from BrokenRules.ToString() :

  public BrokenRules.RulesCollection RulesCollection     {       get       {         return _brokenRules.BrokenRulesCollection;       }     }   public string BrokenRulesString     {       get       {         return _brokenRules.ToString();       }     }  

Data binding to the collection can be used to populate list or grid controls with a list of broken rules, while the BrokenRulesString() method will return a simpler text representation of all the broken rule descriptions. Again, all business objects will support this functionality.

At this point, we're done with the BusinessBase class. We can use it as a base to create business objects that support basic state management, n-Level undo, cloning, parent/child functionality, and tracking of broken rules. In Chapter 5, we'll further enhance BusinessBase to understand object persistence and data access.

BusinessCollectionBase

While BusinessBase is the primary class in our framework for building business objects, we also need to support collections of business objects. As we built both the UndoableBase and BusinessBase classes, we made accommodations for collections of type BusinessCollectionBase , which is what we'll build now.

BusinessCollectionBase needs to support many of the same features we've implemented for business objects. These include n-Level undo functionality, IsDirty , IsValid , and acting as either a root or a child object. Of course, the implementation of each of these is quite different for a collection of objects from what it is for a single object. In some ways, this class will be more complex than BusinessBase .

For a start, the n-Level undo functionality must integrate with what we've written in UndoableBase . This means that the class must implement CopyState() , UndoChanges() , and AcceptChanges() methods that store and restore the collection's state as appropriate. Because a collection can also be a root object, it needs to implement BeginEdit() , CancelEdit() , and ApplyEdit() methods, just as we did in BusinessBase . In either scenario, the process of taking a snapshot of the collection's state is really a matter of having all the child objects take a snapshot of their individual states.

The undo operation for a collection is where things start to get more complicated. Undoing all the child objects isn't too hard, since we can cascade the request to each object, and it can reset its state. At the collection level, however, an undo means restoring any objects that were deleted and removing any objects that were added, so the collection's list of objects ends up the same as it was in the first place.

The IsDirty and IsValid concepts are relatively easy to implement. A collection is "dirty" if it contains child objects that are dirty, added, or removed. A collection's "validity" can be determined by finding out if all its child objects are valid. An invalid child object means that the entire collection is in an invalid state.

The idea that a collection can be a root object or a child object is particularly important. It's fairly obvious that a collection can be a child objectan Invoice root object will have a LineItems collection that contains LineItem objects, so the LineItems collection is itself a child object. However, collection objects can also be root objects.

We may have a root object called Categories , which contains a list of Category objects. It's quite possible that there's no root object to act as a parent for Categories it may simply be an editable list of objects. To support this concept, our BusinessCollectionBase , like BusinessBase itself, must support these two modes of operation. In root mode, some operations are legal while others are not; in child mode, the reverse is true.

The code to implement all these features is somewhat intertwined, so be aware that at some points in the writing of the code, we'll end up relying on methods that haven't yet been implemented.

Creating the Class

Add a new class to the CSLA project and name it BusinessCollectionBase . Let's start with the basics:

  [Serializable()]   abstract public class BusinessCollectionBase : CSLA.Core.BindableCollectionBase   {   }  

As with all our base classes, this one must be [Serializable()] . It also inherits from BindableCollectionBase , so it gains the implementation of IBindingList and the ListChanged event. This automatically grants all our business collection objects full support for Windows Forms data binding.

Note that our business collection objects don't inherit from UndoableBase , because they don't need to. UndoableBase enables an object to take a snapshot of its own state, but the "state" of a collection is quite different from the state of a regular object. A collection has no intrinsic state of its own; rather, its state is the state of all the objects it contains. This means that UndoableBase does us no good, and we'll need to implement our own state management to deal with the unique nature of collections.

Clone Method

Because the class is marked as [Serializable()] , we can implement a Clone() method just like we did in BusinessBase . First, indicate we're implementing the interface:

 [Serializable()]   abstract public class BusinessCollectionBase : CSLA.Core.BindableCollectionBase,  ICloneable  

Next, use a couple of useful namespaces:

  using System.IO; using System.Runtime.Serialization.Formatters.Binary;  

Then, implement the method:

  #region ICloneable     public Object Clone()     {       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, this);       buffer.Position = 0;       return formatter.Deserialize(buffer);     }     #endregion  

This method can be used to create a clone of the collection and all its child objects in a single step.

Contains Method

We can also easily implement a Contains() method, so that business or UI code can see if the collection contains a specific object. This is a typical function provided by collection objects, and while it's not strictly necessary, it's a good feature to support anytime we implement a collection:

  #region Contains     public bool Contains(BusinessBase item)     {       return List.Contains(item);     }     #endregion  

Root or Child

Next, as we did in BusinessBase , we need to allow our collection object to know whether it's a root or a child object:

  #region IsChild     bool _IsChild = false;     protected bool IsChild     {       get       {         return _IsChild;       }     }     protected void MarkAsChild()     {       _IsChild = true;     }     #endregion  

This functionality is the same as we implemented in BusinessBase , and it allows the business developer to mark the object as a child object when it's first created. We can now use the IsChild property in our code to adjust the behavior of our object (such as exercising control over deletion) accordingly .

Edit-Level Tracking

The hardest part of implementing n-Level undo functionality is that we can't just add or delete a child object. We always need to be able to "undelete" or "unadd" those child objects in case of an undo operation.

In BusinessBase and UndoableBase , we implemented the concept of an edit level . The edit level allows us to keep track of how many BeginEdit() calls have been made to take a snapshot of our state without corresponding CancelEdit() or ApplyEdit() calls. More specifically, it tells us how many states we've stacked up for undo operations.

In BusinessCollectionBase , we need the same edit-level tracking as in BusinessBase . However, a collection won't actually stack its states. Rather, it cascades the call to each of its child objects, so that they can stack their own states. Because of this, we'll use a simple numeric counter to keep track of how many unpaired BeginEdit() calls have been made:

  #region Edit level tracking     // keep track of how many edit levels we have     int _editLevel;     #endregion  

As we implement CopyState() , UndoChanges() , and AcceptChanges() , we'll alter this value accordingly.

Reacting to Insert, Remove, or Clear Operations

Collection base classes don't implement Add() or Remove() methods directly, since those are to be implemented by the collection object's author. However, we do need to perform certain operations anytime that an insert, remove, or clear operation occurs. To accommodate this, the CollectionBase class invokes certain virtual methods when these events occur. We can override these methods in our code.

We also need to provide our child objects with the ability to have themselves removed. Earlier in the chapter we implemented the IEditableObject interface in the BusinessBase class. At that time we added code to keep a reference to the collection object, and to call a RemoveChild() method. We'll implement this method here as well:

  #region Insert, Remove, Clear     internal void RemoveChild(BusinessBase child)     {       List.Remove(child);     }     protected override void OnInsert(int index, Object val)     {       // when an object is inserted we assume it is       // a new object and so the edit level when it was       // added must be set       ((BusinessBase)val).EditLevelAdded = _editLevel;       ((BusinessBase)val).SetParent(this);       base.OnInsert(index, val);     }     protected override void OnRemove(int index, Object val)     {       // when an object is 'removed' it is really       // being deleted, so do the deletion work       DeleteChild((BusinessBase)val);       base.OnRemove(index, val);     }     protected override void OnClear()     {       // when the list is cleared, we need to       // remove all the elements       while(List.Count > 0)         List.RemoveAt(List.Count - 1);       base.OnClear();     }     #endregion  

The RemoveChild method is called by a child object contained within the collection. This is called when a Windows Forms DataGrid control requests that the child remove itself from the collection.

The OnInsert() method is called when an item is being added to the collection. We assume that it's a new child object, and tell the object at what edit level it's being added by changing the EditLevelAdded property. As we implemented it in BusinessBase , this merely records the value so that it can be checked later, during undo operations. We'll use this value when we implement the UndoChanges() and AcceptChanges() methods later on.

Notice that when a child object is inserted into the collection we call the child object's SetParent method to make sure its parent reference is correct. This way, if needed, it can call our RemoveChild method to remove itself from the collection.

The OnRemove() method is called when an item is being removed from the collection. Since we need to support the concept of undo, we can't actually allow the object to be removedwe might need to restore it later. To resolve this, we'll call a DeleteChild() method, passing the object being removed as a parameter. We'll implement this method shortly. For now, it's enough to know that it keeps track of the object in case we need to restore it later.

Similarly, the OnClear() method is called when the entire collection is being cleared. Since the OnRemove() method already handles the details, this is just a matter of removing all the items in the list to trigger the OnRemove() behavior.

Deleted Object Collection

To ensure that we can properly "undelete" objects in case of an undo operation, we need to keep a list of the objects that have been "removed" from the collection. The first step in accomplishing this goal is to maintain an internal list of deleted objects.

Along with implementing this private collection object, we'll also provide a ContainsDeleted() method so that the business or UI logic can find out whether the collection contains a specific deleted object.

As with the Contains() method earlier, there's no specific requirement for such a method on a collection. However, it's often very useful to be able to ask a collection if it contains a specific item. Since our collection is unusual in that it contains two lists of objects, it's appropriate that we allow client code to ask whether an object is contained in our deleted list, as well as in our nondeleted list:

  #region DeletedCollection     protected DeletedCollection deletedList = new DeletedCollection();     [Serializable()]     protected class DeletedCollection : CollectionBase     {       public void Add(BusinessBase child)       {         List.Add(child);       }   public void Remove(BusinessBase child)       {         List.Remove(child);       }       public BusinessBase this [int index]       {         get         {           return (BusinessBase)List[index];         }       }     }     #endregion  #region Contains     // ...  public bool ContainsDeleted(BusinessBase item)     {       foreach(BusinessBase element in deletedList)         if(element.Equals(item))           return true;       return false;     }  #endregion 

The collection of deleted objects is protected , to match the collection of regular objects. The root CollectionBase class exposes a List variable, which is a protected collection of the values the list contains. We're just following this precedent by exposing our internal deletedList variable as well.

Notice that DeletedCollection is a nested class that implements a simple, strongly typed collection of BusinessBase objects. Technically, we could just have used a native .NET collection type, but it's preferable to build a strongly typed collection for clarity of code and ease of debugging.

Deleting and Undeleting Child Objects

Now that we have a collection for storing deleted child objects, we can implement methods to delete and undelete objects as needed.

Deleting a child object is really a matter of marking the object as deleted, and moving it from List (the active list of child objects) to deletedList . Undeleting occurs when a child object has restored its state so that it's no longer marked as deleted. In that case, we must move it from deletedList back to List , and it becomes an active child object once again.

The permutations here are vast. The ways in which combinations of calls to BeginEdit() , Add() , Remove() , CancelEdit() , and ApplyEdit() can be called are probably infinite. Let's look at some relatively common scenarios, though, so we can see what happens as we delete and undelete child objects.

First, let's consider a case where we've loaded the collection object from a database, and the database included one child object: A . Then, we called BeginEdit() on the collection and added a new object to the collection: B . Figure 4-11 shows what happens if we remove those two objects and then call CancelEdit() on the collection.

image from book
Figure 4-11: Edit process where objects are removed and CancelEdit() is called
Tip 

In Figure 4-11, EL is the value of _editLevel in the collection, ELA is the _editLevelAdded value in each child object, and DEL is the IsDeleted value in each child object.

We can see that after both objects have been removed from the collection, they're marked for deletion and moved to the deletedList collection. This way they appear to be gone from the collection, but we still have access to them if needed.

After we call CancelEdit() on the collection, its edit level goes back to 0. Since child A came from the database, it was "added" at edit level 0, so it sticks around. Child B , on the other hand, was added at edit level 1, so it goes away. Also, child A has its state reset as part of the CancelEdit() call (remember that CancelEdit() causes a cascade effect, so each child object restores its snapshot values). The result is that because of the undo operation, child A is no longer marked for deletion.

Another common scenario is where we do the same thing, but call ApplyEdit() at the end, as shown in Figure 4-12.

image from book
Figure 4-12: Edit process where objects are removed and ApplyEdit() is called

The first two steps are identical of course, but after we call ApplyEdit() , things are quite different. Since we accepted the changes to the collection rather than rejecting them, the changes became permanent. Child A remains marked for deletion, and if the collection is saved back to the database, the data for child A will be removed. Child B is actually gone at this point. It was a new object added and deleted at edit level 1, and we've now accepted all changes made at edit level 1. Since we know that B was never in the database (because it was added at edit level 1), we can simply discard the object entirely from memory.

Let's look at one last scenario. Just to illustrate how rough this gets, this will be more complex. We'll have nested BeginEdit() , CancelEdit() , and ApplyEdit() calls on the collection. This can easily happen if the collection contains child or grandchild objects, and we've implemented a Windows Forms UI that uses dialog windows to edit each level (parent, child, grandchild, etc.).

Again, we'll have child A loaded from the database, we'll have child B added at edit level 1, and we'll have child C added at edit level 2. Then we'll remove all three child objects, as shown in Figure 4-13.

image from book
Figure 4-13: A more complex example with nested edit method calls

Suppose that we then call ApplyEdit() on the collection. This will apply all edits made at edit level 2, putting us back to edit level 1. Since child C was added at edit level 2, it simply goes away, but child B sticks around because it was added at edit level 1, which is where we are now, as shown in Figure 4-14.

image from book
Figure 4-14: Result after calling ApplyEdit()

Both objects remain marked for deletion because we applied the changes made at edit level 2. Were we now to call CancelEdit() , we'd return to the point we were when the first BeginEdit() was called, meaning that all that would be left is child A , not marked for deletion. Alternatively, we could call ApplyEdit() , which would commit all changes made at edit level 1: child A would continue to be marked for deletion, and child B would be totally discarded since it was added and deleted at edit level 1. Both of these possible outcomes are illustrated in Figure 4-15.

image from book
Figure 4-15: Result after calling either CancelEdit() or ApplyEdit()

Having gone through all that, let's take a look at the code that will implement these behaviors. We'll create the DeleteChild() and UnDeleteChild() methods to deal with marking the child objects as deleted and moving them between the List and deletedList collections:

 #region Delete and Undelete child  private void DeleteChild(BusinessBase child)     {       // mark the object as deleted       child.DeleteChild();       // and add it to the deleted collection for storage       deletedList.Add(child);     }     private void UnDeleteChild(BusinessBase child)     {   // we are inserting an _existing_ object so     // we need to preserve the object's editleveladded value     // because it will be changed by the normal add process     int saveLevel = child.EditLevelAdded;     List.Add(child);  child.EditLevelAdded = saveLevel;  // since the object is no longer deleted, remove it from     // the deleted collection     deletedList.Remove(child);     }     #endregion  

On the surface, this doesn't seem too complicated, but look at the code that deals with the child's EditLevelAdded property in the UnDeleteChild() method. Earlier, we implemented the OnInsert() method, in which we assumed that any child being added to our collection was a new object, and therefore that its edit level value should be set with the collection's current value. However, the OnInsert() method will be run as we insert this preexisting object as well, altering its edit level, which isn't what we want.

Here, we're not dealing with a new object; we're just restoring an old object. To solve this, we first store the object's edit level value, then we re-add the object to the collection, and then we restore the edit level value, effectively leaving it unchanged.

CopyState

Now we're at the point where we can implement the n-Level undo functionality. This means implementing the CopyState() , UndoChanges() , and AcceptChanges() methods, making use of the plumbing that we've put in place so far.

The CopyState() method needs to take a snapshot of our collection's current state. It is invoked when the BeginEdit() method is called on our root object. At that time, the root object takes a snapshot of its own state and calls CopyState() on any child objects or collections so they can take snapshots of their state as well.

  #region N-level undo  internal void CopyState()  {       // we are going a level deeper in editing       _editLevel += 1;       // cascade the call to all child objects       foreach(BusinessBase child in List)         child.CopyState();       // cascade the call to all deleted child objects       foreach(BusinessBase child in deletedList)         child.CopyState();     }  

Each time we take a snapshot of our collection's state, we increase the edit level by one. In UndoableBase , we relied on the Stack object to track this for us, but here we're just using a simple numeric counter. Remember, a collection has no state of its own, so there's nothing to add to a stack of states. Instead, a collection is only responsible for ensuring that all the objects it contains take snapshots of their states. All we need to do is keep track of how many times we've asked our child objects to store their state, so we can properly implement the adding and removing of child objects that we described earlier.

Notice that we also stack the states of the objects in deletedList . This is important, because those objects might, at some point, get restored as active objects in our collection. Even though they're not active at the moment (because they're marked for deletion), we need to treat them the same as we treat regular objects.

Overall, this process is fairly straightforward: we're just cascading the method call down to the child objects. The same can't be said for UndoChanges() or AcceptChanges() .

UndoChanges

The UndoChanges() method is more complex than the CopyState() method. It too cascades the call down to the child objects, deleted or not, but it also needs to find any objects that were added since the latest snapshot. Those objects must be removed from the collection and discarded, since an undo operation means that it must be as though they were never added. Furthermore, it needs to find any objects that were deleted since the latest snapshot. Those objects must be re-added to the collection.

Here's the complete method:

  internal void UndoChanges()     {       BusinessBase child;       // we are coming up one edit level       _editLevel -= 1;       if(_editLevel < 0)         _editLevel = 0;     // Cancel edit on all current items     for(int index = List.Count - 1; index > 0; index--)     {       child = (BusinessBase)List[index];       child.UndoChanges();       // if item is below its point of addition, remove       if(child.EditLevelAdded > _editLevel)         List.Remove(child);     }   // cancel edit on all deleted items       for(int index = deletedList.Count - 1; index > 0; index--)       {         child = deletedList[index];         child.UndoChanges();         // if item is below its point of addition, remove         if(child.EditLevelAdded > _editLevel)           deletedList.Remove(child);         // if item is no longer deleted move back to main list         if(!child.IsDeleted)           UnDeleteChild(child);       }     }  

First of all, we decrement _editLevel to indicate that we're countering one call to CopyState() . Then, we loop through the list of active child objects, calling UndoChanges() on each of them so that they can restore their individual states.

Notice that we're looping through the List and deletedList collections backward, from bottom to top, using a numeric index value. This is important, because it allows us to remove items from the collection safely as we go through each list. If we used foreach , or even a forward-moving numeric index, we would be unable to remove items from the collection as we went without causing a runtime error.

After a child object has been restored, we can check the edit level when it was added to the collection. If we're undoing the collection to a point prior to that edit level, then we know that this is a new child object that now must be discarded.

 // if item is below its point of addition, remove         if(child.EditLevelAdded > _editLevel)           List.Remove(child); 

The same process occurs for the objects in deletedList again, we call UndoChanges() on each of them, and also see if the child object was a newly added object that can now be discarded:

 // if item is below its point of addition, remove         if(child.EditLevelAdded > _editLevel)           deletedList.Remove(child); 

With the deleted child objects, we also need to see if they should still be deleted. Remember that the IsDeleted flag can be reset by UndoChanges() , so if we've now restored a deleted object to a state where it was no longer marked for deletion, then we need to put it back into the active list:

 // if item is no longer deleted move back to main list         if(!child.IsDeleted)           UnDeleteChild(child); 

At the end of the process, our collection object and all its child objects will be in the state they were when CopyState() was last called. We'll have undone any changes, additions, or deletions.

AcceptChanges

The AcceptChanges() method isn't nearly as complicated as UndoChanges() . It also decrements the _editLevel variable to indicate that we're moving down our list of stacked state data. It then cascades the AcceptChanges() method call to each child object, so that child object can accept its own changes. The only complex bit of code is that we also need to alter the "edit level added" value of each child:

  internal void AcceptChanges()     {       // we are coming up one edit level       _editLevel -= 1;       if(_editLevel < 0)         _editLevel = 0;       // cascade the call to all child objects       foreach(BusinessBase child in List)       {         child.AcceptChanges();         // if item is below its point of addition, lower point of addition         if(child.EditLevelAdded > _editLevel)           child.EditLevelAdded = _editLevel;       }       // cascade the call to all deleted child objects       foreach(BusinessBase child in deletedList)       {         child.AcceptChanges();         // if item is below its point of addition, lower point of addition         if(child.EditLevelAdded > _editLevel)           child.EditLevelAdded = _editLevel;       }     }     #endregion  

Here, we're making sure that no child object maintains an EditLevelAdded value that's higher than our collection's current edit level.

Think back to our LineItem example, and suppose that we were at edit level 1 and we accepted the changes. In that case, we're saying that the newly added LineItem object is to be keptit's valid. Because of this, its edit level added value needs to be the same as the collection object, so it needs to be set to 0 as well.

This is important, because there's nothing to stop the user from starting a new edit session and raising the collection's edit level to 1 again. If the user then cancels the operation, we don't want to remove that new Category object accidentally. It was already accepted once, and it should stay accepted.

Notice that here we're looping through the List and deletedList collections using foreach loops. Since we won't be removing any items from either collection as we accept our changes, we can use this simpler looping structure rather than the bottom- to-top numeric looping structure we had to use in the UndoChanges() method.

At this point, we've implemented all the functionality we need to support n-Level undo, so our BusinessCollectionBase is at pretty much at the same level as our UndoableBase class.

BeginEdit, CancelEdit, and ApplyEdit

Now we can implement the methods that the UI will need in order to control the edit process on our collection. Remember, though, that this control is only valid if the collection is a root object. If it's a child object, then its edit process should be controlled by its parent object. To that end, we'll check to ensure that the object isn't a child before allowing these methods to operate :

  #region Begin/Cancel/ApplyEdit     public void BeginEdit()     {       if(this.IsChild)         throw new NotSupportedException(                                 "BeginEdit is not valid on a child object");       CopyState();     }     public void CancelEdit()     {       if(this.IsChild)         throw new NotSupportedException(                                 "CancelEdit is not valid on a child object");       UndoChanges();     }     public void ApplyEdit()     {       if(this.IsChild)         throw new NotSupportedException(                                 "ApplyEdit is not valid on a child object");       AcceptChanges();     }     #endregion  

These methods allow us to create a UI that starts editing a collection with BeginEdit() , lets the user interact with the collection, and then either cancels or accepts the changes with CancelEdit() or ApplyEdit() , respectively.

IsDirty and IsValid

Finally, we can implement the IsDirty and IsValid properties. To determine if our collection has changed (is dirty), all we need to do is determine whether we've added or removed any objects, or if any of our child objects themselves are dirty. To determine if the collection is valid, we merely need to see if any of our child objects are invalid:

  #region IsDirty, IsValid     public bool IsDirty     {       get       {         // any deletions make us dirty         if(deletedList.Count > 0)           return true;         // run through all the child objects         // and if any are dirty then the         // collection is dirty         foreach(BusinessBase child in List)           if(child.IsDirty)             return true;         return false;       }     }     public bool IsValid     {       get       {         // run through all the child objects         // and if any are invalid then the         // collection is invalid         foreach(BusinessBase child in List)           if(!child.IsValid)             return false;         return true;       }     }     #endregion  

In both cases, we're relying on our child objects to keep track of whether they've been changed or are valid, and then just summarizing to get the state of the collection itself.

At this point, the BusinessCollectionBase class is complete, insofar as we've implemented all the functionality that's demanded by UndoableBase and BusinessBase . You should be able to build the assembly at this point without errors.

Tip 

In Chapter 5, we'll finish BusinessCollectionBase by enhancing it with support for data access, but for now it's complete.

ReadOnlyBase

With BusinessBase and BusinessCollectionBase finished (at least for the time being), we're providing the tools that a business developer needs to build editable objects and collections. However, most applications also include a number of read-only objects and collections. We might have a read-only object that contains system configuration data or a read-only collection of ProductType objects that are used just for lookup purposes.

The ReadOnlyBase class will provide a base on which business developers can build a read-only object. Once we're done with this, we'll implement a ReadOnlyCollectionBase to support read-only collections of data.

By definition, a read-only object is quite simple: it's just a container for data, possibly with some security or formatting logic to control how that data is accessed. It doesn't support editing of the data, so there's no need for n-Level undo, change events, or much of the other complexity we built into UndoableBase and BusinessBase . In fact, other than data access logic, the only thing our base class can implement is a Clone() method.

Add a new class to the CSLA project and name it ReadOnlyBase . Then, add the following code:

  using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Configuration; namespace CSLA {   abstract public class ReadOnlyBase : ICloneable   {     #region ICloneable     public Object Clone()     {       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, this);       buffer.Position = 0;       return formatter.Deserialize(buffer);     }     #endregion   } }  

As with all our business base classes, ReadOnlyBase is marked as [Serializable()] and abstract . The former enables the use of serialization as we construct the standard Clone() method, while the latter ensures that to use this class, the business developer will create his own specific business object.

Presumably, any business object based on this class would consist entirely of read- only properties or methods that just return values. In Chapter 5, we'll implement data access functionality for this class, supporting only the reading of data from the database, with no update possible.

ReadOnlyCollectionBase

Like the ReadOnlyBase class, our ReadOnlyCollectionBase is quite simple to create. However, it does need a little extra code to ensure that the contents of the collection can't be altered , except by our data access logic as we load the collection with data from the database.

Add a new class to the CSLA project and name it ReadOnlyCollectionBase , with the following code:

  using System; namespace CSLA {   [Serializable()]   abstract public class ReadOnlyCollectionBase : CSLA.Core.BindableCollectionBase   {     public ReadOnlyCollectionBase()     {       AllowEdit = false;       AllowNew = false;       AllowRemove = false;     }   } }  

Since this class inherits from BindableCollectionBase , it fully supports data binding in Windows Forms. This also means that our constructor can set the values controlling data binding, so that data binding knows the collection can't be altered.

Clone Method

As with our other business base classes, we support the concept of cloning the object, so indicate we're implementing the interface:

 [Serializable()]   abstract public class ReadOnlyCollectionBase :  CSLA.Core.BindableCollectionBase, ICloneable  

Use the required namespaces:

  using System.IO; using System.Runtime.Serialization.Formatters.Binary;  

And then write the (now familiar) Clone() method:

  #region ICloneable     public Object Clone()     {       MemoryStream buffer = new MemoryStream();       BinaryFormatter formatter = new BinaryFormatter();       formatter.Serialize(buffer, this);       buffer.Position = 0;       return formatter.Deserialize(buffer);     }     #endregion  

Preventing Changes

Finally, we need to override the OnInsert() , OnRemove() , OnClear() , and OnSet() methods from CollectionBase to prevent any chance of the contents of the collection being altered. Remember that all collection objects have RemoveAt() and Clear() methods, even if we don't implement them, and we need to make sure that they're useless.

At the same time, we do need to allow items to be added to the collection in the first place. To resolve this, we'll include a protected variable named locked that defaults to true . When our business logic needs to alter the contents of the collection, it can set locked to false , make the changes, and then set locked back to true to prevent any other code from changing the contents of the collection:

  #region Remove, Clear, Set     protected bool locked = true;     protected override void OnInsert(int index, Object val)     {       if(locked)         throw new NotSupportedException(               "Insert is invalid for a read-only collection");     }   protected override void OnRemove(int index, Object val)     {       if(locked)         throw new NotSupportedException(               "Remove is invalid for a read-only collection");     }     protected override void OnClear()     {       if(locked)         throw new NotSupportedException(               "Clear is invalid for a read-only collection");     }     protected override void OnSet(int index, Object oldValue, Object newValue)     {       if(locked)         throw new NotSupportedException(               "Items cannot be changed in a read-only collection");     } #End Region  

Other than the data access code that we'll implement in Chapter 5, this completes the read-only collection base class for our framework.

SmartDate

The final class that we'll implement in this chapter is SmartDate . Date handling is often quite challenging because the standard DateTime data type doesn't have any comprehension of an "empty" or "blank" date.

In many applications, we have date fields that are optional, or where an empty date is not only valid but also has special meaning. For instance, we might have a StartDate field on an object to indicate the date on which something started. If it hasn't yet started, the field should be empty. For the purposes of manipulation, this "empty" date should probably be considered to represent the largest possible date value. In other cases, an empty date might represent the smallest possible date value.

We could force the user to enter some arbitrary date far in the future to indicate an "empty" date, and indeed many systems have done this in the past, but it's a poor solution from both a programmatic and an end-user perspective. Ideally , we'd have a situation in which the user could just leave the field blank, and the program could deal with the concept of an "empty" date and what that means.

Tip 

In the early 1990s, I worked at a company where all "far-future" dates were entered as 12/31/99. Guess how much trouble the company had around Y2K, when all of its never-to-be-delivered orders started coming due!

The DateTime data type is marked sealed , meaning that we can't inherit from it to create our own, smarter data type. However, we can use containment and delegation to "wrap" a DateTime value with extra functionality. That's exactly what we'll do here as we create a SmartDate data type that understands empty dates.

Not only will we make SmartDate understand empty dates, but also we'll give it extra capabilities to support a rich UI, including easy conversion of date values from Date to String , and vice versa. When we implement our data access code in Chapter 5, we'll enhance it to tie right into the data access mechanism so that we can easily store and retrieve both regular and empty date values.

Add a new class to the CSLA project and name it SmartDate . Like all our classes, it will be [Serializable()] so that it can be passed across the network by value:

  using System; namespace CSLA {   [Serializable()]   sealed public class SmartDate   {   } }  

Since each SmartDate object will contain a regular DateTime value, we'll declare a variable for that purpose. We'll also declare a variable to control whether an empty value represents the largest or smallest possible date:

 [Serializable()]   sealed public class SmartDate   {  DateTime _date;     bool _emptyIsMin;  } 

Now we can implement some constructors to allow easy creation of a SmartDate object. We just want to be able to create an empty SmartDate with no extra work, so we have a default constructor. At the same time, we want to be able to control whether an empty date is the largest or smallest date possible, so we overload the constructor with a parameter that allows us to set that value:

  #region Constructors     public SmartDate()     {       _emptyIsMin = false;       _date = DateTime.MaxValue;     }     public SmartDate(bool emptyIsMin)     {       _emptyIsMin = emptyIsMin;       if(_emptyIsMin)         _date = DateTime.MinValue;       else         _date = DateTime.MaxValue;     }  

We also want to be able to initialize the SmartDate object with an existing date, either in text or DateTime format:

  public SmartDate(DateTime date)     {       _emptyIsMin = false;       _date = date;     }     public SmartDate(string date)     {       _emptyIsMin = false;       Text = date;     }     public SmartDate(DateTime date, bool emptyIsMin)     {       _emptyIsMin = emptyIsMin;       _date = date;     }     public SmartDate(string date, bool emptyIsMin)     {       _emptyIsMin = emptyIsMin;       Text = date;     }     #endregion  

In both cases, we optionally allow control over how to treat an empty date.

If we set the value of SmartDate with a string value, our code turns around and sets Text , which we'll implement later. Text intelligently converts the text to a DateTime value, honoring our concept of empty dates.

Supporting Empty Dates

We've already defined a variable to control whether an empty date represents the largest or smallest possible date. We can expose that variable as a property, so that other code can determine how we handle dates:

  #region Empty Dates     public bool EmptyIsMin     {       get       {         return _emptyIsMin;       }     }     #endregion  

We should also implement an IsEmpty property, so that code can ask if the SmartDate object represents an empty date. Add this to the same region:

  public bool IsEmpty     {       get       {         if(_emptyIsMin)           return _date.Equals(DateTime.MinValue);         else           return _date.Equals(DateTime.MaxValue);       }     }  

Notice how we use the _emptyIsMin flag to determine whether an empty date is to be considered the largest or smallest possible date for comparison purposes. If it is the smallest date, then it is empty if our date value equals Date.MinValue , while if it's the largest date, it is empty if our value equals Date.MaxValue .

Conversion Functions

Given this understanding of empty dates, we can create a couple of functions to convert dates to text (or text to dates) intelligently. We'll implement these as static methods so that they can be used even without creating an instance of our SmartDate class. Using these methods, we could write business logic such as this:

 DateTime userDate = SmartDate.StringToDate(userDateString); 

Table 4-3 shows the results of this function, based on various user text inputs.

Table 4-3: Results of the StringToDate Method Based on Various Inputs

User Text Input

EmptyIsMin

Result of StringToDate()

string.Empty

true (default)

DateTime.MinValue

string.Empty

False

DateTime.MaxValue

Any text that can be parsed as a date

true or false (ignored)

A date value

StringToDate() converts a string value containing a date into a DateTime value. It understands that an empty string should be converted to either the smallest or the largest date, based on an optional parameter:

  #region Conversion Functions     static public DateTime StringToDate(string date)     {       return StringToDate(date, true);     }     static public DateTime StringToDate(string date, bool emptyIsMin)     {       if(date.Length == 0)       {         if(emptyIsMin)           return DateTime.MinValue;         else           return DateTime.MaxValue;       }       else         return DateTime.Parse(date);     }     #endregion  

Given a string of nonzero length, this function attempts to convert it directly to a DateTime variable. If that fails, the DateTime.Parse() function will throw an exception; otherwise, a valid date is returned.

We can also go the other way, converting a DateTime variable into a string , while understanding the concept of an empty date. Again, an optional parameter controls whether an empty date represents the smallest or the largest possible date. Another parameter controls the format of the date as it's converted to a string . Table 4-4 illustrates the results for various inputs.

Table 4-4: Results of the DateToString Method Based on Various Inputs

User Date Input

EmptyIsMin

Result of DateToString()

DateTime.MinValue

true (default)

string.Empty

DateTime.MinValue

false

DateTime.MinValue

DateTime .MaxValue

true (default)

DateTime.MaxValu e

DateTime.MaxValue

fals e

string.Empty

Any other valid date

true or false (ignored)

String representing the date value

Add the following code to the same region:

  static public string DateToString(DateTime date, string formatString)     {       return DateToString(date, formatString, true);     }     static public string DateToString(DateTime date, string formatString,                                       bool emptyIsMin)     {       if(emptyIsMin && (date == DateTime.MinValue))         return string.Empty;       else         if(!emptyIsMin && (date == DateTime.MaxValue))         return string.Empty;       else         return string.Format(formatString, date);     }  

This functions as a mirror to our first method. If we start with an empty string , convert it to a DateTime , and then convert that DateTime back to a string , we'll end up with an empty string .

Text Functions

We can now move on to implement functions in our class that support both text and DateTime access to our underlying DateTime value. When business code wants to expose a date value to the UI, it will often want to expose it as a string . (Exposing it as a DateTime precludes the possibility of the user entering a blank value for an empty date, and while that's great if the date is required, it isn't good for optional date values.)

Exposing a date as text requires that we have the ability to format the date properly. To make this manageable, we'll include a variable in our class to control the format used for outputting a date. We'll also implement a property so the business developer can alter this value if she doesn't like the default:

  #region Text Support     string _format = "{0:d}";     public string FormatString     {       get       {         return _format;       }       set       {         _format = value;       }     }     #endregion  

Given this information, we can use the StringToDate() and DateToString() methods to implement a Text property. This property can be used to retrieve or set our value using string representations of dates, where an empty string is appropriately handled. Add this to the same region:

  public string Text     {       get       {         return DateToString(_date, _format, _emptyIsMin);       }       set       {         if(value == null)           _date = StringToDate(string.Empty, _emptyIsMin);         else           _date = StringToDate(value, _emptyIsMin);       }     }  

This property was used in one of our constructors as well, meaning that the same rules for dealing with an empty date apply during object initialization, as when setting its value via the Text property.

There's one other text-oriented method that we should implement: ToString() . All objects in .NET have a ToString() method, and ideally it returns a useful text representation of the object's contents. In our case, it should return the formatted date value:

  public override string ToString()     {       return Text;     }  

Since we've already implemented all the code necessary to render our DateTime value as text, this is easy to implement.

Date Functions

We need to be able to treat a SmartDate like a regular DateTime as much as possible, anyway. Since we can't inherit from DateTime , there's no way for it to be treated just like a regular DateTime , so the best we can do is implement a Date property that returns our internal value:

  #region Date Support     public DateTime Date     {       get       {         return _date;       }       set       {         _date = value;       }     }     #endregion  

Beyond that, this property simply allows the business code to set or retrieve the date value arbitrarily. Of course, our text-based functionality will continue to treat either DateTime.MinValue or DateTime.MaxValue as an empty date, based on our configuration.

Overloading Operators

Since we're making SmartDate similar to DateTime , we should overload the operators that are overloaded by DateTime , including equality, comparison, addition, and subtraction:

  #region Operators     static public bool operator > (SmartDate date1, SmartDate date2)     {       return date1.Date > date2.Date;     }     static public bool operator < (SmartDate date1, SmartDate date2)     {       return date1.Date < date2.Date;     }   static public bool operator == (SmartDate date1, SmartDate date2)     {       return date1.Equals(date2);     }     static public bool operator != (SmartDate date1, SmartDate date2)     {       return !date1.Equals(date2);     }     static public bool operator + (SmartDate d, TimeSpan t)     {       return d + t;     }     static public bool operator - (SmartDate d, TimeSpan t)     {       return d - t;     }     public override bool Equals(object o)     {       return _date.Equals(((SmartDate)o).Date);     }     public override int GetHashCode()     {       return _date.GetHashCode ();     }     #endregion  

The Equals() and GetHashCode() methods must be overridden to overload the equality operators.

Date Manipulation

We should also provide arithmetic manipulation of the date value. Since we're trying to emulate a regular DateTime data type, we should provide at least CompareTo() , Add() , and Subtract() methods:

  #region Manipulation Functions     public int CompareTo(SmartDate date)     {       if(this.IsEmpty && date.IsEmpty)         return 0;       else         return _date.CompareTo(date.Date);     }   public void Add(TimeSpan span)     {       _date.Add(span);     }     public void Subtract(TimeSpan span)     {       _date.Subtract(span);     }     #endregion  

These are easily implemented by delegating each call down to the actual DateTime value it contains. It already understands how to compare, add, and subtract date values, so we don't have to do any real work at all.

Database Format

The final thing we'll do is implement a method that allows our date value to be converted to a format suitable for writing to the database. Though we have functions to convert a date to text, and text to a date, we don't have any good way of getting a date formatted properly to write to a database. Specifically, we need a way to either write a valid date or write a null value if the date is empty.

In ADO.NET, a null value is expressed as DBNull.Value , so we can easily implement a function that returns either a valid DateTime object or DBNull.Value . If our SmartDate object contains an empty date value, we'll return null:

  #region DBValue     public Object DBValue     {       get       {         if(this.IsEmpty)           return DBNull.Value;         else           return _date;       }     }     #endregion  

Since we've already implemented an IsEmpty() property, our code here is pretty straightforward. If it is empty, it returns DBNull.Value , which can be used to put a null value into a database via ADO.NET. Otherwise, it contains a valid date value, so it returns that instead.

Tip 

This routine could be altered to provide a different value from DBNull to represent an empty date in the database. Some people arbitrarily choose a far-future date such as 12/31/9999 to represent empty dates in the database, and in that case they'd alter this method to return that value instead of DBNull . I typically prefer to use a null value to represent an empty date in the database to avoid any chance of confusing it for a real date, and so that's the implementation I'm showing here.

At this point, we have a SmartDate class that business developers can use (if desired) to handle dates that must be represented as text, and where we need to support the concept of an empty date. In Chapter 5, we'll add a little data access functionality, so that a SmartDate can be easily saved and restored from a database.



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