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, theEndEdit
andCancelEdit
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 collection of businessobjects .
In .NET 2.0, we can use Generics, so that we'll have to create only one
Since CSLA.NET was developped using .NET 1.x, the Collection classes inherited from
This new
BusinessObject
objects. So, in fact, we'll need to be able to store a 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.
4 opmerkingen:
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).
Is there a reason you are not implementing IErrorProvider ?
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.
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.
Een reactie posten