Chapter 13. N-Level Undo

There are scenarios where an application requires the ability to undo changes made to an object. Data binding is one example because implementing the IEditableObject interface requires that an object be able to take a snapshot of its state, be edited, and then be able to return its state to that snapshot later. Another example is where the UI has a Cancel button that doesn't close the form or page, in which case the user expects that clicking Cancel will revert the form's data (and thus the business object) to a previous state.

Implementing an undo feature is challenging, especially when you consider parent-child object relationships. When undoing changes to an Invoice object, for example, it is necessary to remove all newly added line items, re-add all removed line items, and undo all edited line items—all that in addition to undoing changes to the Invoice object itself. It is important to remember that all child objects are part of the object's state.

Of course, it is also important to follow good object-oriented programming (OOP) practices, and a key tenet of OOP is to preserve encapsulation. This means one object can't directly manipulate the state (fields) of another object. So the Invoice object can't directly manipulate the state of its LineItemList collection or the LineItem objects it contains. Instead, it must ask each of those objects to manage its own state individually.

The undo functionality provided by CSLA .NET is n-level undo. This means that you can cause the object to take multiple snapshots of its state and then cancel or accept each level of changes:

_customer.BeginEdit();  // take a snapshot
  _customer.Name = "ABC Corp";
  _customer.BeginEdit();  // take a snapshot
  _customer.Name = "RDL Corp";
  _customer.BeginEdit();  // take a snapshot
  _customer.Name = "XYZ Corp";
  _customer.CancelEdit();  // undo to previous snapshot
  _customer.CancelEdit();  // undo to second snapshot
  _customer.ApplyEdit();  // keep first set of property changes

The end result is that the Name property has the value of ABC Corp because the second two sets of property changes were discarded by calls to CancelEdit().

Not all applications use n levels of undo. In fact, most web applications don't use undo at all, and the implementation in CSLA .NET is designed so no overhead is incurred if the feature isn't used.

Most WPF and Windows Forms applications use at least one level of undo because data binding uses the IEditableObject interface, which does take a snapshot of the object's state. If your WPF or Windows Forms interface also includes a Cancel button that doesn't close the form, you'll almost certainly use two levels of undo: one for IEditableObject and another for the form-level Cancel button.

Some WPF and Windows Forms UI designs may use modal windows to allow editing of child objects. If you don't use in-place editing in a grid control, it is quite common to pop up a modal window so the user can edit the details of each row in a grid. If those modal windows have a Cancel button, you'll almost certainly use n-level undo to implement the UI.

While the n-level undo functionality described in this chapter can't handle every type of UI, it does enable a wide range of UI styles, including the most widely used styles. If you don't use undo, no overhead is incurred. If you do use undo, it is designed to be relatively easy and as transparent as possible.

Using Undo

The undo functionality in CSLA .NET is designed to support two primary scenarios: data binding and manual invocation.

  • Data binding uses the undo feature through the IEditableObject interface from the System.ComponentModel namespace. I discuss this interface in Chapter 10, but in this chapter I focus specifically on how it is supported through the undo functionality.

  • Manual invocation of the undo functionality allows the developer to create various types of user experiences, including forms with Cancel buttons and nested modal forms for editing child or grandchild objects.

The behavior of the undo functionality is different for each of these scenarios. This is because data binding expects any IEditableObject implementation to work exactly the way it is implemented by the ADO.NET DataSet and DataTable objects. Those objects implement undo with some limitations:

  • There is only one level of undo per object or row.

  • Master-detail (parent-child) objects are independent.

  • Only the first call to BeginEdit() is honored; subsequent calls are ignored.

The rules for manual invocation are the exact opposite of data binding:

  • Any object can have any level of undo operations (n-level undo).

  • Calling BeginEdit() on a parent also takes a snapshot of child object states.

  • Each call to BeginEdit() takes another snapshot.

The implementation of undo provided by CSLA .NET supports both models and even allows them to be combined (within limits). For example, if you want to implement a form-level Cancel button, you can manually call BeginEdit() on an editable root object to take a snapshot of the entire object graph. Then you can bind the objects to the UI and allow data binding to interact with the objects following its rules. After unbinding the objects from the UI, you can then manually call CancelEdit() or ApplyEdit() to reject or accept all the changes done to any objects while they are bound to the UI.

The rest of the chapter focuses on the implementation of the undo functionality. Keep in mind that it supports both the data binding and manual invocation models.

Implementing Undo

The undo functionality provided by CSLA .NET preserves encapsulation, while providing powerful capabilities for objects and object graphs in a parent-child relationship. The UndoableBase, BusinessBase, and BusinessListBase classes work together to provide this functionality. The undo behaviors are exposed both through the implementation of IEditableObject and directly through BeginEdit(), CancelEdit(), and ApplyEdit() methods.

Figure 13-1 illustrates the relationship between the types that are used to implement the n-level undo functionality.

Types used to implement undo functionality

Figure 13.1. Types used to implement undo functionality

Most of the work occurs in UndoableBase, but BusinessBase and BusinessListBase also include important code for undo functionality.

Table 13-1 lists the key types involved in the process.

Table 13.1. Key Types Required by N-Level Undo

Type

Description

ISupportUndo

Provides public and polymorphic access to the n-level undo functionality; for use by UI developers and other framework authors

IUndoableObject

Allows UndoableBase to polymorphically interact with objects that support the undo functionality; not for use by code outside CSLA .NET

NotUndoableAttribute

Allows a business developer to specify that a field should be ignored by n-level undo

UndoableBase

Implements most undo functionality

UndoException

Is thrown when an undo-related exception occurs

In the rest of the chapter, I walk through the implementation of several of these types and the primary functionality they provide.

ISupportUndo Interface

When you need to manually invoke n-level undo methods, you'll often want to do so polymorphically, without worrying about the specific type of the business object. This is quite common when building reusable UI code or UI controls and enabling this scenario is the purpose behind the ISupportUndo interface. For example, the CslaDataProvider in the Csla.Wpf namespace uses ISupportUndo to call the n-level undo methods on any object that implements the interface.

The ISupportUndo interface defines the three n-level undo methods listed in Table 13-2.

Table 13.2. N-Level Undo Methods Defined by ISupportUndo

Method

Description

BeginEdit()

Takes a snapshot of the business object's state

CancelEdit()

Rolls the object's state back to the most recent snapshot taken by BeginEdit()

ApplyEdit()

Discards the most recent snapshot taken by BeginEdit(), leaving the object's state alone

These three methods encapsulate the functionality provided by n-level undo. The ISupportUndo interface is implemented by all editable objects, both single objects and collections.

NotUndoableAttribute Class

As discussed in Chapter 2, editable business objects and collections support n-level undo functionality. Sometimes, however, objects may have values that shouldn't be included in the snapshot that's taken before an object is edited. (These may be read-only values, or recalculated values, or values—large images, perhaps—that are simply so big you choose not to support undo for them.)

The custom attribute NotUndoable is used to allow a business developer to indicate that a field shouldn't be included in the undo operation.

The UndoableBase class, which implements the n-level undo operations, detects whether this attribute has been placed on any fields. If so, it will simply ignore that field within the undo process, neither taking a snapshot of its value nor restoring it in the case of a cancel operation.

Note

Since the NotUndoable attribute is used by business developers as they write normal business code, it is in the Csla namespace along with all the other types intended for use by business developers.

The NotUndoableAttribute class contains the following code:

namespace Csla
{
  [AttributeUsage(AttributeTargets.Field)]
  public sealed class NotUndoableAttribute : Attribute
  {

  }
}

AttributeUsage specifies that this attribute can be applied only to fields. Beyond that, the NotUndoable attribute is merely a marker to indicate that certain actions should (or shouldn't) be taken by the n-level undo implementation, so there's no real code here at all.

UndoableBase Class

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

Remember, nothing requires the use of n-level undo. In many web scenarios 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 the object's data. On the other hand, when creating a complex WPF or Windows Forms UI that involves modal dialog windows to allow editing of child objects (or even grandchild objects), it is often best to call these methods to provide support for OK and Cancel buttons on each of the dialog windows.

Tip

Typically, a snapshot of a business object's fields is taken before the user or an application is allowed to interact with the object. That way, you can always undo back to that original state. The BusinessBase and BusinessListBase classes include a BeginEdit() method that triggers 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 the values of all fields in each object must be copied, and each business object is essentially composed of several classes all merged together through inheritance and aggregation. 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.

Since UndoableBase is a base class from which BusinessBase will ultimately derive, it must be marked as Serializable. It is also declared as abstract, so that no one can create an instance of this class directly. All business objects need to utilize the INotifyPropertyChanged interface implemented in BindableBase so they inherit from that, too. Finally, the n-level undo functionality relies on the IUndoableObject interface from the Csla.Core namespace, so that is implemented in this class (and in BusinessListBase, discussed later in its own section):

[Serializable]
public abstract class UndoableBase : Csla.Core.BindableBase,
  Csla.Core.IUndoableObject
{
}

With that base laid down, I can start to discuss how 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 the stored object state in case of an accept operation.

Additionally, if this object has child objects that implement IUndoableObject, those child objects must also perform the store, restore, and accept operations. To achieve this, any time the algorithm encounters a field that's derived from either of these types, it cascades the operation to that object so it can take appropriate action.

The three operations are implemented by a set of three methods:

  • CopyState()

  • UndoChanges()

  • AcceptChanges()

CopyState

The CopyState() method takes a snapshot of the object's current data and stores it in a Stack object.

Stacking the Data

Since UndoableBase is an implementation of n-level undo capability, each object could end up storing a number of snapshots. As each undo or accept operation occurs, it gets rid of the most recently stored snapshot; this is the classic behavior of a "stack" data structure. Fortunately, the .NET Framework includes a prebuilt Stack<T> class that implements the required functionality. It is declared as follows:

[NotUndoable]
    private Stack<byte[]> _stateStack = new Stack<byte[]>();

This field is marked as NotUndoable to prevent taking a snapshot of previous snapshots. CopyState() should just record the fields that contain actual business data. Once a snapshot has been taken of the object's data, the snapshot is serialized into a single byte stream. That byte stream is then put on the stack. From there, it can be retrieved and deserialized to perform an undo operation if needed.

Taking a Snapshot of the Data

The process of taking a snapshot of each field value in an object is a bit tricky. Reflection is used to walk through all the fields in the object. During this process, each field is checked to determine whether it has the NotUndoable attribute. If so, the field is ignored.

The big issue is that field names may not be unique within an object. To see what I mean, consider the following two classes:

namespace Test
{
  public class BaseClass
  {
    int _id;
  }

  public class SubClass : BaseClass
  {
    int _id;
  }
}

Here, each class has its own field named _id, and in most circumstances it's not a problem. However, when using reflection to walk through all the fields in a SubClass object, it will return two _id fields: one for each of the classes in the inheritance hierarchy.

To get an accurate snapshot of an object's data, CopyState() needs to accommodate this scenario. In practice, this means prefixing each field name with the full name of the class to which it belongs. Instead of two _id fields, the result is Test.BaseClass!_id and Test.SubClass!_id. The use of an exclamation point for a separator is arbitrary, but some character is necessary to separate the class name from the field name.

As if this weren't complex enough, reflection works differently with classes that are subclassed from other classes in the same assembly than with classes that are subclassed from classes in a different assembly. If in the previous example, BaseClass and SubClass are in the same assembly, one technique can be used; but if they're in different assemblies, a different technique is necessary. Of course, CopyState() should deal with both scenarios so the business developer doesn't have to worry about these details.

Note

Not all the code for UndoableBase is listed in this book. I only cover the key parts of the algorithm. For the rest of the code, refer to the download at www.apress.com/book/view/1430210192 or www.lhotka.net/cslanet/download.aspx.

The following method deals with all of the preceding issues. I walk through how it works after the listing:

[EditorBrowsable(EditorBrowsableState.Never)]
  protected void CopyState()
  {
    CopyingState();

    Type currentType = this.GetType();
    HybridDictionary state = new HybridDictionary();
    FieldInfo[] fields;

    if (this.EditLevel + 1 > parentEditLevel)
      throw new UndoException(string.Format(
          Resources.EditLevelMismatchException, "CopyState"));

    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 (typeof(Csla.Core.IUndoableObject).
                IsAssignableFrom(field.FieldType))
            {
              // make sure the variable has a value
if (value == null)
              {
                // variable has no value - store that fact
                state.Add(GetFieldName(field), null);
              }
              else
              {
                // this is a child object, cascade the call
                ((Core.IUndoableObject)value).
                  CopyState(this.EditLevel + 1, BindingEdit);
              }
            }
            else
            {
              // this is a normal field, simply trap the value
              state.Add(GetFieldName(field), value);
            }
          }
        }
      }
      currentType = currentType.BaseType;
    } while (currentType != typeof(UndoableBase));

    // serialize the state and stack it
    using (MemoryStream buffer = new MemoryStream())
    {
      ISerializationFormatter formatter =
        SerializationFormatterFactory.GetFormatter();
      formatter.Serialize(buffer, state);
      _stateStack.Push(buffer.ToArray());
    }
    CopyStateComplete();
  }

  [EditorBrowsable(EditorBrowsableState.Advanced)]
  protected virtual void CopyingState()
  {
  }

  [EditorBrowsable(EditorBrowsableState.Advanced)]
  protected virtual void CopyStateComplete()
  {
  }

The CopyState() method is scoped as protected because BusinessBase subclasses UndoableBase, and the BeginEdit() method in BusinessBase will need to call CopyState().

To take a snapshot of data, there needs to be somewhere to store the various field values before they are pushed onto the stack. A HybridDictionary is ideal for this purpose, as it stores name-value pairs. It also provides high-speed access to values based on their names, which is important for the undo implementation. Finally, the HybridDictionary object supports .NET serialization, which means that it can be serialized and passed by value across the network as part of a business object.

The CopyState() routine is essentially a big loop that starts with the outermost class in the object's inheritance hierarchy and walks back up through the chain of classes until it gets to UndoableBase. At that point, it can stop—it knows that it has a snapshot of all the business data.

At the start and end of the process, methods are called so a subclass can do pre- and post-processing. Notice that CopyingState() and CopyStateComplete() are virtual methods with no implementation. The idea is that a subclass can override these methods if additional actions should be taken before or after the object's state is copied. They provide an extensibility point for advanced business developers.

Getting a List of Fields

It's inside the loop where the real work occurs. The first step is to 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);

It doesn't matter whether the fields are public—they all need to be recorded regardless of scope. What's more important is to only record instance fields, not those declared as static. The result of this call is an array of FieldInfo objects, each of which corresponds to a field in the business object.

Avoiding Double-Processing of Fields

As discussed earlier, the FieldInfo array could include fields from the base classes of the current class. Due to the way the JIT compiler optimizes code within the same assembly, if some base classes are in the same assembly as the actual business class, the same field name may be listed in multiple classes. As the code walks up the inheritance hierarchy, it could end up processing those fields twice. To avoid this, the code only looks at the fields that directly belong to the class currently being processed:

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, it is established that the current FieldInfo object refers to a field within the object that's part of the current class in the inheritance hierarchy. However, a snapshot of the field should only be taken if it doesn't have the NotUndoable attribute:

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

Having reached this point, it is clear that the field value needs to be part of the snapshot, so there are two possibilities: this may be a regular field or it may be a reference to a child object that implements Csla.Core.IUndoableObject.

Cascading the Call to Child Objects or Collections

If the field is a reference to a Csla.Core.IUndoableObject, the CopyState() call must be cascaded to that object so that it can take its own snapshot:

if (typeof(Csla.Core.IUndoableObject).
                  IsAssignableFrom(field.FieldType))
              {
                // make sure the variable has a value
if (value == null)
                {
                  // variable has no value - store that fact
                  state.Add(GetFieldName(field), null);
                }
                else
                {
                  // this is a child object, cascade the call
                  ((Core.IUndoableObject)value).
                    CopyState(this.EditLevel + 1, BindingEdit);
                }
              }

If a field has a null value, a placeholder is put into the state dictionary so UndoChanges() can restore the value to null if needed.

Non-null values represent a child object, so the call is cascaded to that child. Notice that the parent object doesn't directly manipulate the state of its children because that would break encapsulation. Instead, it is up to the child object to manage its own state. Keep in mind that if the child object is derived from BusinessListBase, the call will automatically be cascaded down to each individual child object in the collection.

Tip

Of course, the GetValue() method returns everything as type object, so the value is cast to IUndoableObject in order to call the CopyState() method.

I want to call your attention to the BindingEdit property that is passed as a parameter to the child's CopyState() method. The BindingEdit property indicates whether this object is currently data bound to a UI or not. If BindingEdit is true, the object is currently bound to the UI and data binding has called BeginEdit() through the IEditableObject interface.

Because this parameter is included, the code is calling the following overload of CopyState():

void IUndoableObject.CopyState(int parentEditLevel, bool parentBindingEdit)
  {
    if (!parentBindingEdit)
      CopyState(parentEditLevel);
  }

This overload is obviously not complex. It simply ensures that the child object only takes a snapshot of its own state when the parent is not using data binding. The idea is to provide the behaviors required by data binding and the behaviors for manual invocation, as I discussed earlier in the chapter.

You might wonder why the parent even tries to cascade the call to the child if BindingEdit is true. The reason is that it is the child object's decision whether it should take a snapshot of its state or not. This approach preserves encapsulation by letting the child object determine its own behavior. Some child objects, such as a BusinessBase child, will ignore the call. But other child objects such as FieldDataManager always take a snapshot of their state. In the case of FieldDataManager this is important, because it contains field values that are directly part of the containing object, so it isn't really a "child" in the same sense as an editable child that inherits from BusinessBase.

Later in this chapter you'll see that the methods to undo or accept any changes will work the same way—that is, they'll cascade the calls to any child objects. This way, all objects handle undo without breaking encapsulation.

Taking a Snapshot of a Regular Field

With a regular field, the code simply stores the field value into the HybridDictionary object, associating that value with the combined class name and field name:

// this is a normal field, simply trap the value
                state.Add(GetFieldName(field), value);

Note that these "regular" fields might actually be complex types in and of themselves. All that is known is that the field doesn't reference an editable business object because the value didn't implement IUndoableObject. It could be a simple value such as an int or a string, or it could be a complex object (as long as that object is marked as Serializable).

Having gone through every field for every class in the object's inheritance hierarchy, the HybridDictionary contains a complete snapshot of all the data in the business object.

Note

This snapshot includes some fields put into the BusinessBase class to keep track of the object's status (such as whether it's new, dirty, deleted, etc.). The snapshot also includes the collection of broken rules that will be implemented later. An undo operation restores the object to its previous state in every way.

Serializing and Stacking the HybridDictionary

At this point, the object's field values are recorded but the snapshot is in a complex data type: a HybridDictionary. To further complicate matters, some of the elements contained in the HybridDictionary might be references to more complex objects. In that case, the HybridDictionary 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. The BinaryFormatter or NetDataContractSerializer can be used to convert the HybridDictionary to a byte stream, reducing it from a complex data type to a very simple one for storage. Better yet, the very process of serializing the HybridDictionary automatically serializes any objects to which it has references.

This does require that all objects referenced by any business object must be marked as Serializable so that they can be included in the byte stream. If referenced objects aren't serializable, the serialization attempt results in a runtime error. Alternatively, any nonserializable object references can be marked 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
      using (MemoryStream buffer = new MemoryStream())
      {
        ISerializationFormatter formatter =
          SerializationFormatterFactory.GetFormatter();
        formatter.Serialize(buffer, state);
        _stateStack.Push(buffer.ToArray());
      }

The SerializationFormatterFactory uses the CslaSerializationFormatter config setting to determine whether to use the BinaryFormatter (the default) or the NetDataContractSerializer. This is set in the app.config or web.config file in the appSettings element:

<add key="CslaSerializationFormatter" value="NetDataContractSerializer" />

Either formatter works with Serializable objects, but only NetDataContractSerializer works with DataContract objects.

Regardless of which formatter is used, the formatter object serializes the HybridDictionary (and any objects to which it refers) into a stream of bytes in an in-memory buffer. The byte stream is simply extracted from the in-memory buffer and pushed onto the stack:

_stateStack.Push(buffer.ToArray());

Converting a MemoryStream to a byte array is not an issue because the MemoryStream is implemented to store its data in a byte array. The ToArray() method simply returns 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 the business object is passed across the network by value while it is being edited, the stack of states needs to be serializable.

Tip

Passing objects across the network while they're being edited is not anticipated, but since business objects are Serializable, you can't prevent the business developer from doing just that. If the stack were to reference a MemoryStream, the business application would get a runtime error as the serialization fails, and that's not acceptable. Converting the data to a byte array avoids accidentally crashing the application on the off chance that the business developer does decide to pass an object across the network as it's being edited.

At this point, we're a third of the way through implementing n-level undo support. It is now possible to create a stack of snapshots of an object's data. It is time to move on and discuss 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 HybridDictionary, and then takes each value from the HybridDictionary and restores it into the appropriate object field. Like CopyState(), there are virtual methods called before and after the process to allow subclasses to take additional actions.

The hard issues of walking through the types in the object's inheritance hierarchy and finding all the fields in the object are solved in the implementation of CopyState(). The structure of UndoChanges() is virtually identical, except that it restores field values rather than takes a snapshot of them.

Since the overall structure of UndoChanges() is essentially the reverse of CopyState(), I won't show the entire code here. Rather, I'll focus on the key functionality.

EditLevel

It is possible for a business developer to accidentally trigger a call to UndoChanges() when there is no state to restore. If this condition isn't caught, it will cause a runtime error. To avoid such a scenario, the first thing the UndoChanges() method does is to get the "edit level" of the object by retrieving the Count property from the stack object. If the edit level is 0, there's no state to restore, and UndoChanges() just exits without doing any work.

This edit level concept is even more important in the implementation of BusinessListBase, so you'll notice that the value is implemented as a property.

Also notice that the edit level is checked to make sure it is in sync with this object's parent object (if any):

if (this.EditLevel - 1 < parentEditLevel)
        throw
          new UndoException(string.Format(
               Resources.EditLevelMismatchException, "UndoChanges"));

All three of the undo methods do this check, and these exceptions help the business developer debug her code when using n-level undo.

The most common place where these exceptions occur is when using Windows Forms data binding because it is very easy for a UI developer to forget to properly unbind an object from the UI, leaving the object in a partially edited state. These exceptions help identify those situations so the developer can fix the UI code.

Re-Creating the HybridDictionary Object

Where CopyState() serializes the HybridDictionary into a byte array at the end of the process, the first thing UndoChanges() needs to do is pop the most recently added snapshot off the stack and deserialize it to re-create the HybridDictionary object containing the detailed values:

HybridDictionary state;
      using (MemoryStream buffer = new MemoryStream(_stateStack.Pop()))
      {
        buffer.Position = 0;
        ISerializationFormatter formatter =
          SerializationFormatterFactory.GetFormatter();
        state = (HybridDictionary)formatter.Deserialize(buffer);
      }

This is the reverse of the process used to put the HybridDictionary onto the stack in the first place. The result of this process is a HybridDictionary containing all the data that was taken in the original snapshot.

Restoring the Object's State Data

With the HybridDictionary containing the original object values restored, it is possible to loop through the fields in the object in the same manner as CopyState().

When the code encounters a child business object that implements IUndoableObject, it cascades the UndoChanges() call to that child object so that it can do its own restore operation. Again, this is done to preserve encapsulation—only the code within a given object should manipulate that object's data.

With a "normal" field, its value is simply restored from the HybridDictionary:

// this is a regular field, restore its value
                  field.SetValue(this, state[GetFieldName(field)]);

At the end of this process, the object is reset to the state it was in when the most recent snapshot was taken. All that remains is to implement a method to accept changes, rather than to undo them.

AcceptChanges

AcceptChanges() is actually the simplest of the three methods. If changes are being accepted, it means that the current values in the object are the ones that should be kept, and the most recent snapshot is now meaningless and can be discarded. Like CopyState(), once this method is complete, a virtual AcceptChangesComplete() method is called to allow subclasses to take additional actions.

In concept, this means that all AcceptChanges() needs to do is discard the most recent snapshot:

_stateStack.Pop();

However, it is important to remember that the object may have child objects, and they need to know to accept changes as well. This requires looping through the object's fields to find any child objects that implement IUndoableObject. The AcceptChanges() method call must be cascaded to them, too.

The process of looping through the fields of the object is the same as in CopyState() and UndoChanges(). The only difference is where the method call is cascaded:

// the field is undoable so see if it is a child object
              if (typeof(Csla.Core.IUndoableObject).
                  IsAssignableFrom(field.FieldType))
              {
                object value = field.GetValue(this);
                // make sure the variable has a value
                if (value != null)
                {
                  // it is a child object so cascade the call
                  ((Core.IUndoableObject)value).AcceptChanges(
                      this.EditLevel, BindingEdit);
                }
              }

Simple field values don't need any processing. Remember that the idea is that the current values have been accepted—so there's no need to change those current values at all.

You should now understand how the three undo methods in UndoableBase are able to take and restore a snapshot of an object's state, and how the calls are cascaded to child objects in a way that preserves encapsulation while supporting the needs of both data binding and manual invocation of the undo functionality.

BusinessBase Class

The UndoableBase class does the majority of the work to support the undo functionality. However, BusinessBase does include some methods that allow an editable object to participate in the undo process.

In particular, it is BusinessBase that implements ISupportUndo, and so it is BusinessBase that implements methods such as BeginEdit(). These methods are public to enable the manual invocation of the undo functionality.

BeginEdit Method

I'll start by discussing the simplest of the three methods. Here's the BeginEdit() method:

public void BeginEdit()
  {
    CopyState(this.EditLevel + 1);
  }

When the business or UI developer explicitly calls BeginEdit() the object takes a snapshot of its state and cascades that call to its child objects. You've already seen how this is done in the CopyState() method implemented in UndoableBase, and BeginEdit() relies on that preexisting behavior.

Remember that the undo methods in UndoableBase throw an exception if the object's edit level gets out of sync with its parent object. The parent's edit level is passed in as a parameter to each undo method, such as CopyState().

When manually invoking CopyState(), it is necessary to pass in a parameter indicating the future state of the edit level. When taking a snapshot, the future edit level is one higher than the current edit level.

In other words, this would cause an exception:

CopyState(this.EditLevel);

The reason is that CopyState() would see that it is about to raise the object's edit level above the value passed in as a parameter, so it would throw an exception. By passing in EditLevel + 1, the BeginEdit() method is effectively giving permission for the object to take a snapshot of its state.

CancelEdit Method

The CancelEdit() method is a little more complex. Actually, it isn't CancelEdit() that's more complex but the required post-processing implemented in an override of UndoChangesComplete() that is complex:

public void CancelEdit()
  {
    UndoChanges(this.EditLevel - 1);
  }

  protected override void UndoChangesComplete()
  {
    BindingEdit = false;
    ValidationRules.SetTarget(this);
    InitializeBusinessRules();
    OnUnknownPropertyChanged();
    base.UndoChangesComplete();
  }

Like BeginEdit(), the CancelEdit() method lets UndoableBase do the hard work. But when the UndoChanges() method is complete, there is some housekeeping that must be done by BusinessBase, and that is handled by the UndoChangesComplete() override.

When either CancelEdit() or ApplyEdit() is called, that call ends any data binding edit currently in effect, so the BindingEdit property is set to false. If data binding calls BeginEdit() through IEditableObject, BindingEdit will be set to true again, but any cancel or accept operation ends that edit process.

An UndoChanges() operation effectively deserializes some data that is stored in the Stack object, as discussed earlier in the chapter. This means it is necessary to ensure that all the internal references between objects are correct. For example, the ValidationRules.SetTarget() method ensures that this object's ValidationRules object has the correct reference as the target for all business rules.

It is also the case that an undo operation probably changed one or more property values. Most likely, the user has changed some property values and the undo reset them to some previous values. The call to OnUnknownPropertyChanged() raises a PropertyChanged event so data binding knows that the UI needs to be refreshed to reflect the changes.

ApplyEdit Method

The ApplyEdit() method is also a little complex. It also does some processing beyond that provided by UndoableBase:

public void ApplyEdit()
  {
    _neverCommitted = false;
    AcceptChanges(this.EditLevel - 1);
    BindingEdit = false;
  }

  protected override void AcceptChangesComplete()
  {
    if (Parent != null)
      Parent.ApplyEditChild(this);
    base.AcceptChangesComplete();
  }

Again, when either CancelEdit() or ApplyEdit() are called, that call ends any data binding edit currently in effect, so the BindingEdit property is set to false.

There's also a _neverCommitted flag that is used to track whether the ApplyEdit() method on this object has ever been called. This field is used in the IEditableObject implementation to support the automatic removal of child objects from a list but is only used for Windows Forms 1.0 style data binding (so is nearly obsolete at this point in time).

The most interesting bit of code here is in AcceptChangesComplete(), where a Parent.ApplyEditChild() method is called. This occurs if this object is a child of some other object and the ApplyEditChild() method is used to tell the parent object that its child's ApplyEdit() is invoked. The EditableRootListBase class uses this to determine that the user has completed editing a particular child object in the list, so it knows to trigger an immediate save of that child object's data. I discuss EditableRootListBase in more detail in Chapter 16.

At this point you've seen the code for the public methods that support the undo functionality. There are also three similar methods that support data binding through the IEditableObject interface.

IEditableObject Interface

The IEditableObject interface is used by data binding to interact with an object. As I discuss earlier in the chapter, data binding expects a single level of undo and that each object is independent of other objects (so no cascading of undo calls from parent to children).

The BeginEdit() method must detect whether the developer has disabled IEditableObject support and must also only honor the first call to BeginEdit() by data binding. It is important to realize that data binding may make many calls to BeginEdit() and only the first one should have any effect:

void System.ComponentModel.IEditableObject.BeginEdit()
  {
    if (!_disableIEditableObject && !BindingEdit)
    {
      BindingEdit = true;
      BeginEdit();
    }
  }

The BindingEdit property is set to true to indicate that the object is now bound to a UI. As you've already seen, this property is used to alter the behavior of other undo features and most importantly prevents cascading of method calls to child objects.

The CancelEdit() method is more complex because it supports the automatic removal of certain new objects when they are a child of a list:

void System.ComponentModel.IEditableObject.CancelEdit()
  {
    if (!_disableIEditableObject && 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 cancelled back to
        // where we were added so we should have ourselves
        // removed from the parent collection
        if (Parent != null)
          Parent.RemoveChild(this);
      }
    }
  }

This call to Parent.RemoveChild() is only useful for Windows Forms 1.0 data binding when using the Windows Forms 1.0 grid control. When using modern data binding and grid controls, this code is unused because BindingList<T> handles the removal of child objects by itself.

The simplest of the three methods is ApplyEdit():

void System.ComponentModel.IEditableObject.EndEdit()
  {
    if (!_disableIEditableObject && BindingEdit)
    {
      ApplyEdit();
    }
  }

Assuming IEditableObject isn't disabled and that the object is currently data bound, this method simply delegates to the ApplyEdit() method I discussed earlier.

As you can see, the BusinessBase class exposes the undo functionality in three ways:

  • Through public methods for manual invocation

  • Through the ISupportUndo interface for manual invocation

  • Through the IEditableObject interface for data binding

It also interacts with its UndoableBase base class to do most of the work, including interacting with child objects, such as editable collections. This means BusinessListBase must also participate in the undo functionality.

BusinessListBase Class

The BusinessListBase class combines some of what UndoableBase does and some of what you've seen in BusinessBase. Like UndoableBase, the BusinessListBase class implements IUndoableObject, and like BusinessBase it implements ISupportUndo and the three public undo methods.

The implementation of this functionality in a collection isn't as complex as it is for editable objects, however, because a collection is primarily responsible for cascading the method calls to all the child objects it contains.

Edit Level Tracking

The hardest part of implementing n-level undo functionality is that not only can child objects be added or deleted but they can be "undeleted" or "unadded" in the case of an undo operation. Csla.Core.BusinessBase and UndoableBase use the concept of an edit level. The edit level allows the object to keep track of how many BeginEdit() calls have been made to take a snapshot of its state without corresponding CancelEdit() or ApplyEdit() calls. More specifically, it tells the object how many states have been stacked up for undo operations.

BusinessListBase needs 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, the edit level can be tracked using a simple numeric counter. It merely counts how many unpaired BeginEdit() calls have been made:

// keep track of how many edit levels we have
    private int _editLevel;

The implementations of CopyState(), UndoChanges(), and AcceptChanges() alter this value accordingly.

Reacting to Insert, Remove, or Clear Operations

Collection base classes don't implement Add() or Remove() methods directly because those are implemented by Collection<T>, which is the base class for BindingList<T>. However, they do need to perform certain operations any time that an insert or remove operation occurs. To accommodate this, BindingList<T> invokes certain virtual methods when these events occur. These methods can be overridden to respond to the events.

Child objects also must have the ability to remove themselves from the collection. Remember the implementation of System.ComponentModel.IEditableObject in Clsa.Core.BusinessBase—that code included a parent reference to the collection object and code to call a RemoveChild() method. This RemoveChild() method is part of the IEditableCollection interface implemented by BusinessListBase.

The important thing to realize is that BusinessListBase does extra processing in addition to the default behavior of the BindingList base class:

  • It moves "deleted" items to and from a DeletedList so items can be "undeleted."

  • It completely removes deleted items that are new (and thus not "undeleted" in an undo operation).

  • It maintains any LINQ to CSLA indexes (as discussed in Chapter 14).

  • It ensures that newly added child objects have an edit level corresponding to the collection's edit level.

To do this, the methods listed in Table 13-3 are implemented.

Table 13.3. Methods Implemented for Insert, Remove, and Clear Operations

Method

Source

Description

RemoveChild()

IEditableCollection

Removes a child object from the list

RemoveItem()

base

Removes an item (by index) from the list

InsertItem()

base

Inserts a new child into the list

The RemoveChild() method is called by a child object contained within the collection. This is called when a Windows Forms grid control requests that the child remove itself from the collection via the System.ComponentModel.IEditableObject interface.

Note

In reality, this shouldn't be a common occurrence. Windows Forms 2.0 uses a new interface, ICancelAddNew, that is implemented by BindingList<T>. This interface notifies the collection that the child should be removed rather than notifying the child object itself. The code in the RemoveItem() method takes care of the ICancelAddNew case automatically, so this code is really here to support backward compatibility for anyone explicitly calling the IEditableObject interface on child objects.

The RemoveItem() method is called when an item is being removed from the collection. To support the concept of undo, the object isn't actually removed because it might need to be restored later. Rather, a DeleteChild() method is called, passing the object being removed as a parameter. You'll see the implementation of this method shortly. For now, it's enough to know that it keeps track of the object in case it must be restored later.

The InsertItem() method is called when an item is being added to the collection. The EditLevelAdded property is changed when a new child object is added to the collection, thus telling the child object the edit level at which it's being added. Recall that this property was implemented in BusinessBase to merely record the value so that it can be checked during undo operations. This value will be used in the collection's UndoChanges() and AcceptChanges() methods later on.

Also notice that the child object's SetParent() method is called to make sure its parent reference is correct. This way, if needed, it can call the collection's RemoveChild() method to remove itself from the collection.

Deleted Object Collection

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

Along with implementing this list, there needs to be a ContainsDeleted() method so that the business or UI logic can find out whether the collection contains a specific deleted object.

BindingList already includes a Contains() method so that the UI code can ask the collection whether it contains a specific item. Since a BusinessListBase collection is unusual in that it contains two lists of objects, it's appropriate to allow client code to ask whether an object is contained in the deleted list as well as in the nondeleted list.

The list of deleted objects is kept as a List<C>—a strongly typed collection of child objects. That list is then exposed through a protected property so it is available to subclasses. Subclasses have access to the nondeleted items in the collection, so this just follows the same scoping model. The list object is created on demand to minimize overhead in the case that no items are ever removed from the collection.

Given the list for storing deleted child objects, it is possible to implement the 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 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, the child object must be moved from DeletedList back to the list of active objects in the collection.

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 to get a good understanding of what happens as child objects are deleted and undeleted.

First, consider a case in which the collection has been loaded with data from a database and the database includes one child object: A. Then, the UI calls BeginEdit() on the collection and adds a new object to the collection: B. Figure 13-2 shows what happens if these two objects are removed and then CancelEdit() is called on the collection object.

Tip

In Figure 13-2, 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.

After both objects are 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 the collection still has access to them if needed.

After the CancelEdit() call, the collection's 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.

Edit process in which objects are removed and CancelEdit is called

Figure 13.2. Edit process in which objects are removed and CancelEdit is called

Another common scenario follows the same process but with a call to ApplyEdit() at the end, as shown in Figure 13-3.

Edit process in which objects are removed and ApplyEdit is called

Figure 13.3. Edit process in which objects are removed and ApplyEdit is called

The first two steps are identical, of course, but after the call to ApplyEdit(), things are quite different. Since changes to the collection are accepted rather than rejected, the changes become permanent. Child A remains marked for deletion, and if the collection is saved back to the database, the data for child A is deleted from the database. Child B is totally gone at this point. It is a new object added and deleted at edit level 1, and all changes made at edit level 1 are accepted. Since the collection knows that B was never in the database (because it was added at edit level 1), it 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. It involves nested BeginEdit(), CancelEdit(), and ApplyEdit() calls on the collection. This can easily happen if the collection contains child or grandchild objects and they are displayed in a Windows Forms UI that uses modal dialog windows to edit each level (parent, child, grandchild, etc.).

Again, child A is loaded from the database and child B is added at edit level 1. Finally, C is added at edit level 2. Then all three child objects are removed, as shown in Figure 13-4.

The result after loading, adding, and removing objects

Figure 13.4. The result after loading, adding, and removing objects

Suppose ApplyEdit() is now called on the collection. This applies all edits made at edit level 2, putting the collection 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 illustrated in Figure 13-5.

Both objects remain marked for deletion because the changes made at edit level 2 were applied. Were CancelEdit() called now, the collection would return to the same state as when the first BeginEdit() was called, meaning that only child A (not marked for deletion) would be left.

Result after calling ApplyEdit

Figure 13.5. Result after calling ApplyEdit

Alternatively, a call to ApplyEdit() 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 because it was added and deleted at edit level 1. Both of these possible outcomes are illustrated in Figure 13-6.

Result after calling either CancelEdit or ApplyEdit

Figure 13.6. Result after calling either CancelEdit or ApplyEdit

Having gone through all that, let's take a look at the code that implements these behaviors. The DeleteChild() and UnDeleteChild() methods deal with marking the child objects as deleted and moving them between the active items in the collection and the DeletedList object:

private void DeleteChild(C child)
  {
    // set child edit level
    Core.UndoableBase.ResetChildEditLevel(child, this.EditLevel, false);
    // remove from the index
    RemoveIndexItem(child);
    // remove from the position map
    RemoveFromMap(child);
    // mark the object as deleted
    child.DeleteChild();
    // and add it to the deleted collection for storage
    DeletedList.Add(child);
  }

  private void UnDeleteChild(C child)
  {
    // since the object is no longer deleted, remove it from
    // the deleted collection
    DeletedList.Remove(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;
    InsertIndexItem(child);
    Add(child);
    child.EditLevelAdded = saveLevel;
  }

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. In the InsertItem() method I discussed earlier, the assumption is that any child being added to the collection is a new object and therefore InsertItem() sets its edit level value to the collection's current value. However, the InsertItem() method is run when this preexisting object is reinserted into the collection, altering its edit level. That would leave the child object with an incorrect edit level value.

The problem is that in this case, the child object isn't a new object; it is a preexisting object that is just being restored to the collection. To solve this, the object's edit level value is stored in a temporary field, the child object is re-added to the collection, and then the child object's edit level value is reset to the original value, effectively leaving it unchanged.

CopyState

Everything has so far laid the groundwork for the n-level undo functionality. All the pieces now exist to make it possible to implement the CopyState(), UndoChanges(), and AcceptChanges() methods, and then the BeginEdit(), CancelEdit() and ApplyEdit() methods.

The CopyState() method needs to take a snapshot of the collection's current state. It is invoked when the BeginEdit() method is called on the root object (either the collection itself or the collection's parent 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 states as well:

void Core.IUndoableObject.CopyState(
    int parentEditLevel, bool parentBindingEdit)
  {
    if (!parentBindingEdit)
      CopyState(parentEditLevel);
  }

  private void CopyState(int parentEditLevel)
  {
    if (this.EditLevel + 1 > parentEditLevel)
      throw new Core.UndoException(
        string.Format(Resources.EditLevelMismatchException, "CopyState"));

    // we are going a level deeper in editing
    _editLevel += 1;

    // cascade the call to all child objects
    foreach (C child in this)
      child.CopyState(_editLevel, false);

    // cascade the call to all deleted child objects
    foreach (C child in DeletedList)
      child.CopyState(_editLevel, false);
  }

There are technically two CopyState() methods—one for the Csla.Core.IUndoableObject interface and the other a private implementation for use within BusinessListBase itself. The interface implementation merely delegates to the private implementation.

As CopyState() takes a snapshot of the collection's state, it increases the edit level by one. Remember that UndoableBase relies on the Stack object to track the edit level, but this code just uses a simple numeric counter. 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 it needs to do is keep track of how many times CopyState() has been called, so the collection can properly implement the adding and removing of child objects, as described earlier.

Notice that the CopyState() call is also cascaded to the objects in DeletedList. This is important because those objects might at some point get restored as active objects in the collection. Even though they're not active at the moment (because they're marked for deletion), they need to be treated the same as regular nondeleted objects.

Overall, this process is fairly straightforward: the CopyState() call is just cascaded 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 added since the latest snapshot. Those objects must be removed from the collection and discarded because an undo operation means that it must be as though they were never added. Furthermore, it needs to find any objects deleted since the latest snapshot. Those objects must be re-added to the collection.

Here's the complete method:

void Core.IUndoableObject.UndoChanges(
    int parentEditLevel, bool parentBindingEdit)
  {
    if (!parentBindingEdit)
      UndoChanges(parentEditLevel);
  }

  private bool _completelyRemoveChild;

  private void UndoChanges(int parentEditLevel)
  {
    C child;

    if (this.EditLevel - 1 < parentEditLevel)
      throw new Core.UndoException(
        string.Format(Resources.EditLevelMismatchException, "UndoChanges"));

    // we are coming up one edit level
    _editLevel -= 1;
    if (_editLevel < 0) _editLevel = 0;

    bool oldRLCE = this.RaiseListChangedEvents;
    this.RaiseListChangedEvents = false;
    try
    {
      // Cancel edit on all current items
      for (int index = Count - 1; index >= 0; index—)
      {
        child = this[index];

        DeferredLoadIndexIfNotLoaded();
        _indexSet.RemoveItem(child);

        child.UndoChanges(_editLevel, false);

        _indexSet.InsertItem(child);

        // if item is below its point of addition, remove
        if (child.EditLevelAdded > _editLevel)
        {
          bool oldAllowRemove = this.AllowRemove;
          try
          {
            this.AllowRemove = true;
            _completelyRemoveChild = true;
            RemoveIndexItem(child);
            RemoveAt(index);
          }
finally
          {
            _completelyRemoveChild = false;
            this.AllowRemove = oldAllowRemove;
          }
        }
      }

      // cancel edit on all deleted items
      for (int index = DeletedList.Count - 1; index >= 0; index—)
      {
        child = DeletedList[index];
        child.UndoChanges(_editLevel, false);
        if (child.EditLevelAdded > _editLevel)
        {
          // if item is below its point of addition, remove
          DeletedList.RemoveAt(index);
        }
        else
        {
          // if item is no longer deleted move back to main list
          if (!child.IsDeleted) UnDeleteChild(child);
        }
      }
    }
    finally
    {
      this.RaiseListChangedEvents = oldRLCE;
      OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, −1));
    }
  }

First of all, _editLevel is decremented to indicate that one call to CopyState() has been countered.

Notice that the loops going through the collection itself and the DeletedList collections go from bottom to top, using a numeric index value. This is important because it allows safe removal of items from each collection. Neither a foreach loop nor a forward-moving numeric index would allow removal of items from the collections without causing a runtime error.

UndoChanges() is called on all child objects in the collection so that they can restore their individual states. After a child object's state is restored, the child object's edit level is checked to see when it was added to the collection. If the collection's new edit level is less than the edit level when the child object was added, it is a new child object that now must be discarded:

// if item is below its point of addition, remove
          if (child.EditLevelAdded > _editLevel)
          {
            bool oldAllowRemove = this.AllowRemove;
            try
            {
              this.AllowRemove = true;
              _completelyRemoveChild = true;
              RemoveIndexItem(child);
              RemoveAt(index);
            }
finally
            {
              _completelyRemoveChild = false;
              this.AllowRemove = oldAllowRemove;
            }
          }

The same process occurs for the objects in DeletedList; again, UndoChanges() is called on each child object. Then there's a check to see if the child object is a newly added object that can now be discarded:

if (child.EditLevelAdded > _editLevel)
        {
          // if item is below its point of addition, remove
          DeletedList.RemoveAt(index);
        }

A bit more work is required when dealing with the deleted child objects. It is possible that the undo operation needs to undelete an object. Remember that the IsDeleted flag is automatically maintained by UndoChanges(), so it is possible that the child object is no longer marked for deletion. In such a case, the object must be moved back into the active list:

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

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

AcceptChanges

The AcceptChanges() method isn't nearly as complicated as UndoChanges(). It also decrements the _editLevel field to counter one call to CopyState(). The method then cascades the AcceptChanges() call to each child object so that each child object can accept its own changes. The only complex bit of code is that the "edit level added" value of each child must also be altered:

void Core.IUndoableObject.AcceptChanges(
    int parentEditLevel, bool parentBindingEdit)
  {
    if (!parentBindingEdit)
      AcceptChanges(parentEditLevel);
  }

  private void AcceptChanges(int parentEditLevel)
  {
    if (this.EditLevel - 1 < parentEditLevel)
      throw new Core.UndoException(
        string.Format(Resources.EditLevelMismatchException, "AcceptChanges"));

    // we are coming up one edit level
    _editLevel -= 1;
    if (_editLevel < 0) _editLevel = 0;
// cascade the call to all child objects
    foreach (C child in this)
    {
      child.AcceptChanges(_editLevel, false);
      // 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
    for (int index = DeletedList.Count - 1; index >= 0; index—)
    {
      C child = DeletedList[index];
      child.AcceptChanges(_editLevel, false);
      // if item is below its point of addition, remove
      if (child.EditLevelAdded > _editLevel)
        DeletedList.RemoveAt(index);
    }
  }

While looping through the collection and DeleteList, the code makes sure that no child object maintains an EditLevelAdded value that's higher than the collection's new edit level.

Think back to the LineItem example and suppose the collection is at edit level 1 and the changes are accepted. In this case, the newly added LineItem object is to be kept—it's valid. Because of this, its EditLevelAdded property 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, the collection shouldn't remove the previous LineItem object accidentally. It was already accepted once, and it should stay accepted.

This method won't remove any items from the collection as changes are accepted, so the simpler foreach looping structure can be used rather than the bottom-to-top numeric looping structure needed in the UndoChanges() method.

When looping through the DeletedList collection, however, the bottom-to-top approach is still required. This is because DeletedList may contain child items newly added to the collection and then marked for deletion. Since they are new objects, they have no corresponding data in the database, so they can simply be dropped from the collection in memory. In such a case, those child objects are removed from the list based on their edit level value.

This completes all the functionality needed to support n-level undo, allowing BusinessListBase to integrate with the code in the UndoableBase class.

BeginEdit, CancelEdit, and ApplyEdit

With the n-level undo methods complete, it is possible to implement the methods that the UI needs to control the edit process on a collection. Remember, though, that this control is only valid if the collection is a root object. If it's a child object, its edit process should be controlled by its parent object. This requires a check to ensure that the object isn't a child before allowing these methods to operate:

public void BeginEdit()
  {
    if (this.IsChild)
      throw new NotSupportedException(Resources.NoBeginEditChildException);

    CopyState(this.EditLevel + 1);
  }
public void CancelEdit()
  {
    if (this.IsChild)
      throw new NotSupportedException(Resources.NoCancelEditChildException);

    UndoChanges(this.EditLevel - 1);
  }

  public void ApplyEdit()
  {
    if (this.IsChild)
      throw new NotSupportedException(Resources.NoApplyEditChildException);

    AcceptChanges(this.EditLevel - 1);
  }

All three methods are very straightforward and allow developers to create a UI that starts editing a collection with BeginEdit(), let the user interact with the collection, and then either cancel or accept the changes with CancelEdit() or ApplyEdit().

These methods also provide the implementation for ISupportUndo, allowing a UI developer or other framework author to polymorphically interact with n-level undo on any editable object.

Conclusion

The n-level undo functionality provided by CSLA .NET is very flexible and powerful. It is also designed to incur no overhead if it isn't used. Many web applications or service-oriented applications don't need this feature so pay no cost in terms of performance.

Virtually all Windows Forms and WPF applications use at least the support provided for data binding and can choose to leverage the more powerful features for certain types of user interface.

Although the implementation of undo is quite complex and must deal with many edge cases, thanks to data binding and changes to the .NET Framework over the years, the complexity is encapsulated within CSLA .NET. Business object and UI developers can use the abstract API exposed by ISupportUndo or simply use data binding to interact with the undo functionality.

You may have noticed that there are numerous points in BusinessListBase where methods are called to maintain index maps. This is required for LINQ to CSLA, which is the topic of Chapter 14.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset