zaterdag 21 januari 2006

Business Objects Framework – Part 2

This is the 2nd article of a serie. The 1ste one can be found here.

In this article, I'll explain the BusinessObjectCollection class. Before starting off with this class, I'd like to go back to the BusinessObject class first, because I want to do a little modification on that class.

Implementation of IEditableObject in BusinessObject


The IEditableObject interface is defined in the .NET framework to control the undo-functionality used by the databinding infrastructure.
There are 2 situations in where this interface is used:

  • If the object is a child of a collection, and this collection is collection is databound to a grid, the IEditableObject interface is used so that the user of the application gets the expected functionality. For instance, when he creates a new row and then presses Escape on that row, the new row should not be added to the collection.

  • When an object is databound to the forms on a control, the IEditableObject.BeginEdit method is called when the object has been changed by the databinding. However, the EndEdit and CancelEdit methods are not called automatically by the infrastructure, and thus, it is the responsability of the application-developer to call these methods.

Since the BusinessObject class already contains the methods defined by the IUndoable interface that are able to undo / commit any changes made to the BusinessObject, I think it is better that the application developer has no 'direct access' to the methods of the IEditableObject interface. The application developer should use our IUndoable methods instead, and the IEditableObject interface should only be used if the BusinessObject is contained in a collection that is databound.

To achieve that the application-developer cannot call the methods of the IEditableObject directly, the IEditableObject interface should be implemented explicitly.
This is done like this:

void IEditableObject.BeginEdit()
{
...
}

void IEditableObject.CancelEdit()
{
...
}

void IEditableObject.EndEdit()
{
...
}


The effect of implementing the IEditableObject interface explicitly is, that the BeginEdit, CancelEdit and EndEdit methods will not appear in the 'Intellisense' list and, that these methods cannot be called directly on instances of BusinessObject.
These methods are only callable if the BusinessObject instance is cast to the IEditableObject type.

Now, the application developer that uses the BusinessObject class, is in a way forced to use the methods of our IUndoable interface if he wants undo-support.

Now we also have to manipulate our _bindingEdit variable in the IUndoable methods instead of in the IEditableObject members:
public void CreateSnapshot()
{
_bindingEdit = true;
...
}

public void CommitSnapshot()
{
_bindingEdit = false;
...
}

public void RevertToPreviousState()
{
_bindingEdit = false;
...
}

void IEditableObject.BeginEdit()
{
if( _bindingEdit == false )
{
this.CreateSnapshot ();
}
}

void IEditableObject.CancelEdit()
{
if( _bindingEdit )
{
this.RevertToPreviousState ();
}
}

void IEditableObject.EndEdit()
{
if( _bindingEdit )
{
this.CommitSnapshot ();
}
}


Now, we can have a look at the BusinessObjectCollection class.

Collections of BusinessObjects


We will need a class that is able to store multiple BusinessObject objects. So, in fact, we'll need to be able to store a collection of businessobjects.
In .NET 2.0, we can use Generics, so that we'll have to create only one BusinessObjectCollection class, and in our application code, we can define what kind of BusinessObject instances this class should contain.

Since CSLA.NET was developped using .NET 1.x, the Collection classes inherited from CollectionBase. However, with .NET 2.0, I can use the Collection<T> class as a base-class for my the BusinessObjectCollection class.
This new Collection<T> class allows me to write less code (I do not have to write my own strong typed Add, Remove, etc... classes), and I do not have to choose in my methods whether I'll use the List or the InnerList property. There's a subtle difference in these properties: List raises events when an item is added / removed, while InnerList does not raise those events.


The rough skeleton of the BusinessObjectCollection class looks like this:

[Serializable]
public class BusinessObjectCollection<T> : Collection<T>,
IUndoable,
IBindingList
where T : BusinessObject
{
}

As you can see, the class is Serializable and inherits from Collection<T> which I just discussed.
Generics are used here so that the application-developer can indicate what kind of objects he wants to put in his collection. However, there is a restriction: the objects that the BusinessObjectCollection can contain, must be inherited from BusinessObject.
So, the application developer can use our BusinessObjectCollection class like this:
BusinessObjectCollection<Customer> customers = 
new BusinessObjectCollection<Customer>();

This will only compile if the Customer class inherits from BusinessObject.

Now, as I said earlier, since this class inherits from Collection<T>, I do not have to write Add, Remove, etc... methods. If I should have used the CollectionBase class, and if I wanted strong typed Add and Remove methods, I should have written those methods like this:
public T this[ int index ]
{
get
{
return (T)List[index];
}
}

public int Add( T item )
{
return List.Add (item);
}


Not only should I have to do this for the Add method and for the indexer, but I should 've done it for the Insert, Remove, ... methods as well.
Now I do not have to do this, because the Collection<T> class already provides this for me. This means that inheriting from Collection<T> saves me a lot of tedious work.

What I do have to do, is, providing extra functionality when a BusinessObject object is added to the collection. If I should have inherited from CollectionBase, I should have been carefull whether I'll use the List or the InnerList property in my own Add and Remove methods, since the List property raises events that I can respond to if I want extra functionality. With the Collection<T> class, I do not have to pay attention to it because this class does not have an InnerList nor List property. It only has an Items property.

I only have to override the InsertItem and the RemoveItem methods.

Every time a BusinessObject is added to the collection, I'll need to keep track of the editlevel at which the object has been added. This is necessary for the 'Undo' functionality of the collection.
Overriding the InsertItem method, allows me to do that:
protected override InsertItem( int index, T item )
{
item.EditLevelAdded = _editLevel;
base.InsertItem (index, item);
}

This means that, the BusinessObject class needs an internal member called EditLevelAdded, that keeps track of the 'editlevel' of the collection at which the item has been added to the collection.
This code also means that our BusinessObjectCollection class needs a private member _editLevel.

When a BusinessObject is removed from the collection, we will need to keep track of the BusinessObject as well, since, when the changes that have been made to our collection are undone, it is quite possible that the previously deleted business-object is to be undeleted.
This means that the BusinessObjectCollection will need a list that contains the BusinessObjects that were deleted from the collection. To achieve this, the BusinessObjectCollection class needs a member that is able to save this collection of deleted BusinessObject instances.
Again, the Collection<T> class is perfectly suited for this. (In .NET 1.x you'll have to create a nested type that inherits from CollectionBase that represents this collection).

Now, every time that an item is being removed from our collection, we'll have to add it to the collection that keeps track of the deleted BusinessObjects:
private Collection<T> _deletedItems = new Collection<T> ();

protected override RemoveItem( int index )
{
// Since we do not have direct access to the item that's
// being removed here, we'll have to get it first.
T businessItem = Items[index];
if( businessItem != null )
{
businessItem.MarkDeleted();
_deletedItems.Add (businessItem);
}

base.RemoveItem(index);
}

Since one of my design goals was to get the data access code out of the BusinessObjects, this means that we'll have to have a way to be able to tell our Data Access components which items were removed, and should be deleted in the database.
This means that we'll have to add these 2 extra methods:
public T[] GetDeletedBusinessObjects()
{
List<T> items = new List<T> ();
foreach( T item in _deletedItems )
{
items.Add (item);
}

return items.ToArray();
}

public void ClearDeleted()
{
_deletedItems.Clear();
}

Now, Data Access Components can use these 2 methods of the collection to see which BusinessObjects should be deleted, and once this is done, they should also be removed from the 'deletedItems list'.

Implementing IUndoable


We still need to implement the IUndoable interface.
Implementing the CreateSnapshot method is quite easy. We only have to make sure that we call the CreateSnapshot method of every BusinessObject that is in our collection, but, we must also not forget to create a snapshot on the BusinessObjects that are in our _deletedItems collection.
private int _editLevel = 0;

public void CreateSnapshot()
{
_editLevel++;

foreach( T item in Items )
{
T.CreateSnapshot();
}
foreach( T item in _deletedItems )
{
T.CreateSnapshot();
}
}

The implementation of CommitSnapshot is fairly simple as well. We just need to call the CommitSnapshot method on every BusinessObject that is in our collection and in the _deletedItems collection.

Implementing RevertToPreviousState is a bit more complex, since we might have a situation in where a deleted BusinessObject should be undeleted; for an exhaustive explanation of the functionality, I'll refer to the Expert C# Business Objects book.
This is the code:

_editLevel--;

if( _editLevel < 0 )
{
_editLevel = 0;
}

for( int i = Items.Count - 1; i >= 0; i-- )
{
T item = Items[i];

item.RevertToPreviousState ();

if( item.EditLevelAdded > _editLevel )
{
base.Items.Remove (item);
}
}

for( int i = _deletedItems.Count - 1; i >= 0; i-- )
{
T item = _deletedItems[i];

item.RevertToPreviousState ();

if( item.EditLevelAdded > _editLevel )
{
_deletedItems.Remove (item);
}

if( item.IsDeleted == false )
{
int saveLevel = item.EditLevelAdded;

// Add the business object back to the list.
Items.Add (item);

if( item.EditLevelAdded != saveLevel )
{
item.EditLevelAdded = saveLevel;
}

_deletedItems.Remove (item);

}
}


In the next article, I'll discuss the LazyBusinessObjectCollection class.

6 opmerkingen:

PJ. van de Sande zei

Again, nice post! The deleted item list becomes handy for deleting stuff from your databank when needed, because a collection is not allways 100% filled.

I prever typed-collection instead of using Generics, but this is clear enough.

Only one thing i didn't understand, why the GetDeletedBusinessObjects() doesn't just return _deletedItems.ToArray( typeof( T ) );?

Frederik Gheysels zei

About the typed collections:
When you use generics, you have -in a way- a typed collection.
However, If you want extra functionality, you can always inherit from the BusinessObjectCollection class. (Which reminds me that I think I should seal the overriden InsertItem, ... methods).

Tom Janssens zei

Is there a reason you are not implementing IErrorProvider ?

Frederik Gheysels zei

I am not aware of the existence of IErrorProvider ?
I've searched the MSDN for it, and couldn't find anything about it.

I do know ErrorProvider, but this is a windows-component.

PJ. van de Sande zei

In the InsertItem method of you collection class you have to lookup if the object isn't in the deleted objects list, if it is you have to remove it there.

Frederik Gheysels zei

Why ?
You have deleted something, so it must be in the deleted list.
If you re-add the item, the deleted item stays in the deleted list as long as CommitSnapshot or RevertToPreviousState is not called.

Consider this:
You call CreateSnapShot, you remove an item from the collection. This item is put in the deletedList.
You re-add the item.
Then you call RevertToPreviousState; the deleted item should be undeleted, and the re-added item should be removed.

If you call CommitSnapshot, then the deleted Item should be removed from the deleted list, and the re-added item should stay in the list.