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.