zondag 8 januari 2006

Business Objects Framework – Part 1

Introduction


More than one year ago, I started reading Expert C# Business Objects by Rockford Lhotka.
In his book, ‘Rocky’ Lhotka explains us how he has developed his CSLA.NET framework.
While his framework has some really nice features, that tackle some problems, it also has some disadvantages.


There are 2 issues in the CSLA.NET framework that are –in my opinion- a big problem:

  • There is data-access code inside the business object.

  • The business object takes care of transaction-handling.


The first issue can be resolved by factoring out the data-access code out of the business object, and putting it in a separate object.
The second issue is a bigger problem. In my opinion, the business object should not be responsible for transaction handling, since the business object does not know the context in where it is used. It is the client itself who should be responsible for the transaction handling, since this is the only place where the context in where the Business Object is used, is known.
(Although the framework has some disadvantages, I think the book is recommended reading material for anyone who is interested in .NET & enterprise applications).

Because of these drawbacks of the CSLA.NET framework, I’ve decided to create my own 'framework', in where I'll implement some good things of CSLA.NET (like n-level undo support, data-binding), but, try to find another solution for the things I do not like.
I also wanted to be able to support lazy-loading of collections.

I will try to explain the things I've done in order to create this set of classes in a serie of articles on this weblog.

N-Level Undo support


So, I already told that the CSLA.NET framework has some features that I really like, like n-level undo support.
This is a feature that I also want in my 'framework'.
Rockford Lhotka has decided to create an UndoableBase base-class that already contains all the functionality for n-level undo support. However, I've thought that it would be appropriate to create an interface that would define the contract to which all types that support n-level undo support must comply to.

This interface just defines 3 methods and looks like this:
public interface IUndoable
{
void CreateSnapshot();
void CommitSnapshot();
void RevertToPreviousState();
}

If you're familiar with the CSLA.NET framework, you probable know (or you can see), that, in the UndoableBase class, a check is performed if a member of the class inherits from the BusinessBase or BusinessCollectionBase class when a snapshot is taken and when the state of an object is reverted to its previous state.
This is a little bit 'dirty' in my opinion, since this code creates some kind of a 'circular reference'. (BusinessBase inherits from UndoableBase, and UndoableBase 'knows' about the existence of BusinessBase).
The introduction of this interface will resolve this problem:
Instead of the two checks (one for BusinessBase and one for BusinessCollectionBase), only one check will have to be performed. The circular reference will also be gone, because the class(es) that will implement IUndoable interface, should then only check if their members implement IUndoable interface as well.

BusinessObject base class


Then, I’ve created an abstract base class BusinessObject from which all the concrete business-objects of the problem-domain should inherit from.
BusinessObject implements the IUndoable and the IEditableObject interfaces.
The latter interface is necessary if you want to support data-binding.

While mr Lhotka created 3 'base-classes' (BusinessBase which inherits from UndoableBase which in turn inherits from BindableBase), I've decided to create only one base-class from which all my business objects will inherit from. I've done this because, at this moment, I do not see the advantage of having the other 2 base-classes.

Since the BusinessObject base-class implements the IUndoable and IEditableObject interfaces, this class is off-course responsible for the n-level undo support and for the databinding.
Apart from that, BusinessObject also contains some 'state tracking' functionality (which is necessary if you do not use an ORM-tool like NHibernate which does the state tracking for you).

The rough skeleton of the BusinessObject class looks like this:
[Serializable]
public abstract class BusinessObject : IUndoable, IEditableObject
{
#region State Tracking functionality
#endregion

#region IUndoable implementation
#endregion

#region IEditableObject implementation
#endregion
}


The class is abstract because it is offcourse intented that you do not use this class directly. You should inherit your 'specific' businessobjects from this class.
The class is also marked as 'Serializable'. This is necessary if you want to use your business objects for instance in a remoting scenario.
Another reason for the Serializable attribute will be clear soon, since Serialization is used to achieve the n-undo functionality.

I still have to discuss the implementation of this class, so let's do it right away.

State tracking


The state tracking code is quite simple. We only need some flags and methods that take care of this.
This code is quite simple and nothing (or very little) has been changed compared with the code that is contained in the BusinessBase class of the CSLA.NET framework, so instead of commenting this code in this post, I'll refer to the 'Expert C# Business Objects' book for further information.
#region State Tracking functionality

private bool _isNew = true;
private bool _isDirty = false;
private bool _isDeleted =false;

protected void MarkDirty()
{
_isDirty = true;
}

protected void MarkClean()
{
_isDirty = false;
}

protected void MarkNew()
{
_isNew = true;
_isDeleted = false;
MarkDirty();
}

internal void MarkDeleted()
{
_isDeleted = true;
MarkDirty();
}

protected void MarkOld()
{
_isNew = false;
MarkClean();
}

public bool IsNew
{
get
{
return _isNew;
}
}

public bool IsDeleted
{
get
{
return _isDeleted;
}
}

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

#endregion


Implementing N-level undo support


Implementing the n-level undo support requires some more work and is a little bit more complicated then the state-tracking.
Just like in the CSLA.NET framework, this functionality relies heavely on .NET reflection. (In fact, I've played copy-cat here. Apart from the usage of the IUndoable interface, this code is almost identical as the code that Rockford Lhotka has written).

Since the CSLA.NET framework is free to be downloaded (you can get it here), I'll only post some relevant code.

#region IUndoable implementation

[NotUndoable]
/// The stack that will keep track of all our states.
private Stack _stateStack = new Stack();

public void CreateSnapshot()
{
Type currentType;
Hashtable state = new Hashtable();
FieldInfo[] fields;
string fieldName;

currentType = this.GetType();

do
{
// Get the members of this type.
fields = currentType.GetFields (BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance);

foreach( FieldInfo field in fields )
{
// If this field is declared in the current type
// and the field is undoable, keep track of it's state
if( field.DeclaringType == currentType &&
this.IsUndoableField (field) )
{
object fieldValue = field.GetValue(this);

// If the member implements IUndoable, cascade
// the call.
IUndoable uf = fieldValue as IUndoable;

if( uf != null )
{
uf.CreateSnapshot();
}
else
{
fieldName = currentType.Name + "." + field.Name;
state.Add (fieldName, fieldValue);
}

}
}

currentType = currentType.BaseType;

}while( currentType != typeof(BusinessObject) );

// Now, use serialization to save the state in the
// statestack
MemoryStream buffer = new MemoryStream ();
BinaryFormatter fmt = new BinaryFormatter ();
fmt.Serialize (buffer, state);
_stateStack.Push (buffer.ToArray ());

}
#endregion


If you have a look at the code above, and compare it with the CSLA.NET code, you'll notice the slight difference.
The implementation of RevertToPreviousState and CommitSnapshot is also almost identical to the UndoChanges and AcceptChanges methods of the UndoableBase class of the CSLA.NET framework. Just as in the CreateSnapshot method, the only change I've made, is checking for the IUndoable interface.
The RevertToPreviousState and CommitSnapshot pop the last saved state from the stack. CommitSnapshot just throws it away, and calls CommitSnapshot for every member of the class that is an IUndoable type, while ReverToPreviousState uses the last saved state to reset the value of all class-members to their previous state.

The [NotUndoable] attribute is a custom attribute that defines that a field that is decorated with this attribute should not be 'undoable'.
The IsUndoableField method is a private member method of BusinessObject. This method just checks whether or not the given field is decorated with the NotUndoable Attribute.

IEditableObject implementation


The IEditableObject implementation makes use of the IUndoable functionality.
For databinding purposes, it must be possible that the state of an object is saved when a user changes something to the object. This is necessary because the object must be reset in it's original state when the user decides to cancel the changes he has made.

We also need a flag that indicates wether or not we have to save the current state.
This is necessary because, in a databinding scenario, the BeginEdit method is called on every databound control in where you change the (databound) content.
Since it is not necessary to keep track of the changes on such a fine-grained level (we will want to keep track of the changes on a 'form level'), we control the 'state support' by this flag.
#region IEditableObject Implementation

[NotUndoable]
private bool _bindingEdit = false;

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

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

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

#endregion


So, that's enough typing for today. If you have comments, feel free to post them here or to send me an email.
In the next article of this series, I'll discuss the base classes that are used for collections of BusinessObjects.

Since I have modified the CSLA.NET framework for learning purposes, the same licence applies to it as the licence that applies to the CSLA.NET framework.

6 opmerkingen:

Anoniem zei

Hiya,
I'm just implementing this in VB due to the same concerns about CLSA.
An error in the article though:
need to add currentType = currentType.BaseType just before the end of the CreateSnapshot loop

Frederik Gheysels zei

Yes indeed; I must 've forgotten it while typing this post.
Anyway, I'll add it. Thx.

Anoniem zei

My major concern about CSLA.NET is its excessive reliance on reflection and "tricks" in the framework. For example, using the StackTrace class to determine the name of properties (which requires that every property accessor in your business object be marked NoInlining). Then, the strange almost-AOP implementation of transactions in the DataPortal is also a little scary to me. Rocky's got some good ideas in there, but I think he could have implemented it a much cleaner way.

Frederik Gheysels zei

Is this in CSLA.NET 2.0 that he uses the StackTrace class to determine the name of the properties ?
I'm not familiar with csla.net 2.0, and I do not remember that I've seen anything like that in CSLA.NET 1.0 ?

Sergey Barskiy zei

It is using stak trace, but this use is optional. Framework provides overloads that can take property names, thus eliminating the need for reflection use. Transactional support is actually pretty clean in my oipnion and is one the recommended Microsoft approaches. It is also optional and you can use your own transactions based on context.

Sergey Barskiy zei

It is using stak trace, but this use is optional. Framework provides overloads that can take property names, thus eliminating the need for reflection use. Transactional support is actually pretty clean in my oipnion and is one the recommended Microsoft approaches. It is also optional and you can use your own transactions based on context.