Building a Business Object Base Class
Posted
Tue, Jul 21 2009 11:49
by
Deborah Kurata
If you are building applications in .NET to manage data for a business, you are most likely creating business object classes. Depending on the business, these classes could include Customer, Product, Order, Invoice, PurchaseOrder, Employee, TimeCard and so on.
A simple sample Customer class is shown here.
There are some features that all business objects need to support. For example:
- Entity State: The business object needs to track whether it is new, updated, or deleted so it can make the appropriate change to the database.
- Validation: Is the current data defined for the business object valid?
- Save: Saves the changes to the database.
Notice that data retrieval functionality is not included in this list. That is because in most cases, you may never retrieve a single business object. Rather, you would retrieve a set of them. For example, all active orders or all customers with overdue invoices. So the retrieve functionality is not included in the class that manages a single object. (Most on this in a later post.)
Instead of repeating this common functionality in each business object, it makes sense to build a base class. Each business object can then inherit from this base class to share this common functionality.
So let’s get started.
In C#:
using System;
using System.ComponentModel;
public abstract class BoBase :
IDataErrorInfo,
INotifyPropertyChanged
{
}
In VB:
Imports System.ComponentModel
Public MustInherit Class BOBase
Implements IDataErrorInfo
Implements INotifyPropertyChanged
End Class
The class is abstract (MustInherit in VB) to indicate that it is meant to be a base class and not to be used on its own. An abstract class cannot be instantiated directly, so no other code can create an instance of the class.
The class then implements two interfaces:
- IDataErrorInfo: This interface provides error information that the user interface can use to report validation errors to the user. It works well with the ErrorProvider control provided in WinForms and supports ASP.NET and WPF features.
- INotifyPropertyChanged: This interface ensures that the user interface is notified when a property value changes, keeping your business object and user interface in sync.
We’ll look at the implementation of these interfaces shortly.
First, define the valid set of entity states.
In C#:
protected internal enum EntityStateType
{
Unchanged,
Added,
Deleted,
Modified
}
In VB:
Protected Friend Enum EntityStateType
Unchanged
Added
Deleted
Modified
End Enum
The Enum is declared Protected because the entity state should only be accessible from the business object itself. However, Internal (Friend in VB) was added so that related objects (such as Order and OrderLineItem) could reference the related object state.
The business object state is retained using an EntityState property.
In C#:
protected EntityStateType EntityState { get; private set; }
In VB:
Private _EntityState As EntityStateType
Protected Property EntityState() As EntityStateType
Get
Return _EntityState
End Get
Private Set(ByVal value As EntityStateType)
_EntityState = value
End Set
End Property
This property is protected, but the setter is private. So the business objects that inherit from this class can read the entity state, but only the base class can set the value.
Additional properties provide a way to get the entity’s state in an easier fashion. These properties are not required, but they make the base class a little easier to use.
In C#:
[BindableAttribute(false)]
[BrowsableAttribute(false)]
public bool IsDirty
{
get { return this.EntityState != EntityStateType.Unchanged; }
}
[BindableAttribute(false)]
[BrowsableAttribute(false)]
public bool IsNew
{
get { return this.EntityState == EntityStateType.Added; }
}
In VB:
<BindableAttribute(False)> _
<BrowsableAttribute(False)> _
Public ReadOnly Property IsDirty() As Boolean
Get
Return Me.EntityState <> EntityStateType.Unchanged
End Get
End Property
<BindableAttribute(False)> _
<BrowsableAttribute(False)> _
Public ReadOnly Property IsNew() As Boolean
Get
Return Me.EntityState = EntityStateType.Added
End Get
End Property
The IsDirty and IsNew properties are public, so they can be accessed from anywhere. They are marked with two attributes:
- Bindable: Defines whether the property should be used for binding. In this case the value is false because the user interface should not be able to bind to this property.
- Browsable: Defines whether the property should be displayed in the Properties window. Again, the value is false.
Two other properties handle the validation.
In C#:
[BindableAttribute(false)]
[BrowsableAttribute(false)]
public bool IsValid
{
get { return (ValidationInstance.Count == 0); }
}
protected Validation ValidationInstance { get; set; }
In VB:
<Bindable(False)> _
<BrowsableAttribute(False)> _
Public ReadOnly Property IsValid() As Boolean
Get
Return (ValidationInstance.Count = 0)
End Get
End Property Private _ValidationInstance As Validation
Protected Property ValidationInstance() As Validation
Get
Return _ValidationInstance
End Get
Private Set(ByVal value As Validation)
_ValidationInstance = value
End Set
End Property
Again, the public property is marked with the Browsable and Bindable attributes. The ValidationInstance property retains an instance of the Validation class for the business object. The code for the Validation class is here.
The constructor creates an instance of the Validation class.
In C#:
protected BoBase()
{
ValidationInstance = new Validation();
}
In VB:
Protected Sub New()
ValidationInstance = New Validation
End Sub
The following is the implementation of IDataErrorInfo.
In C#:
#region IDataErrorInfo Members
[BrowsableAttribute(false)]
[BindableAttribute(false)]
public string Error
{
get { return ValidationInstance.ToString(); }
}
[BrowsableAttribute(false)]
[BindableAttribute(false)]
public string this[string columnName]
{
get { return ValidationInstance[columnName]; }
}
#endregion
In VB:
#Region " Properties required by the IDataErrorInfo"
<Bindable(False)> _
<BrowsableAttribute(False)> _
Public ReadOnly Property [Error]() As String _
Implements IDataErrorInfo.Error
Get
Return ValidationInstance.ToString
End Get
End Property
<BrowsableAttribute(False)> _
<Bindable(False)> _
Default Protected ReadOnly Property Item(ByVal columnName _
As String) As String _
Implements IDataErrorInfo.Item
Get
Return ValidationInstance.Item(columnName)
End Get
End Property
#End Region
The Error property uses the overridden ToString method of the validation class to return the full list of validation errors.
The Item property provides access to the validation errors given a property name. This property is implemented as the class indexer in C#.
The following in the implementation of INotifyPropertyChanged.
In C#:
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
In VB:
#Region " Events required by INotifyPropertyChanged"
Public Event PropertyChanged(ByVal sender As Object, _
ByVal e As System.ComponentModel.PropertyChangedEventArgs) _
Implements INotifyPropertyChanged.PropertyChanged
#End Region
This interface only defines a single event. This event should be raised whenever the data is changed.
Since every business object will have unique requirements for the save operation, the SaveItem method is not implemented. Rather it is defined as abstract.
In C#:
public abstract Boolean SaveItem();
In VB:
Public MustOverride Function SaveItem() As Boolean
Defining an abstract (MustOverride in VB) SaveItem method ensures that every business object has a SaveItem method.
Finally, since the EntityState property setter is private, the base class needs a method to set the entity state.
In C#:
protected internal void SetEntityState(EntityStateType newEntityState)
{
switch (newEntityState)
{
case EntityStateType.Deleted:
case EntityStateType.Unchanged:
case EntityStateType.Added:
this.EntityState = newEntityState;
break;
default:
if (this.EntityState == EntityStateType.Unchanged)
this.EntityState = newEntityState;
break;
}
}
protected internal void SetEntityState(EntityStateType newEntityState, string propertyName)
{
SetEntityState(newEntityState);
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
In VB:
Protected Friend Sub SetEntityState(ByVal dataState As EntityStateType)
SetEntityState(dataState, Nothing)
End Sub
Protected Friend Sub SetEntityState( _
ByVal newEntityState As EntityStateType, ByVal propertyName As String)
Select Case newEntityState
Case EntityStateType.Deleted, _
EntityStateType.Unchanged, _
EntityStateType.Added
Me.EntityState = newEntityState
Case Else
If Me.EntityState = EntityStateType.Unchanged Then
Me.EntityState = newEntityState
End If
End Select
If Not String.IsNullOrEmpty(propertyName) Then
Dim e As New PropertyChangedEventArgs(propertyName)
RaiseEvent PropertyChanged(Me, e)
End If
End Sub
The SetEntityState method has two overloads. The first is used when changing the entity state in general and the second is used when changing the entity state because a specific property is changed.
For example, when setting an object as Unchanged, Added, or Deleted, it does not matter which property was changed. But when a particular property is changed, the code must also raise the PropertyChanged event.
In this case, the C# and VB code was implemented differently. In the C# code, the code to set the entity state is in the first overload. The second overload then calls the first and then raises the event.
In the VB code, the first overload simply calls the second overload. The second overload then sets the entity state and then raises the event as appropriate. You can use either technique in either language.
That’s it! You now have a base class that can be used with any business object class. If you have any other common functionality, you can add it to this base class.
Here is an example of how you use this base class.
In C#:
public class Customer : BoBase
{
private string _LastName;
public string LastName
{
get { return _LastName; }
set
{
if (_LastName == null || _LastName != value)
{
string propertyName = "LastName";
_LastName = value;
// Validate the last name
ValidationInstance.ValidateClear(propertyName);
ValidationInstance.ValidateRequired(propertyName, value);
SetEntityState(EntityStateType.Modified, propertyName);
}
}
}
public override Boolean SaveItem()
{
// TODO: Add code here
}
}
In VB:
Public Class Customer
Inherits BoBase
Private _LastName As String
Public Property LastName() As String
Get
Return _LastName
End Get
Set(ByVal value As String)
If _LastName Is Nothing OrElse _LastName <> value Then
Dim propertyName As String = "LastName"
_LastName = value
' Validate the last name
ValidationInstance.ValidateClear(propertyName)
ValidationInstance.ValidateRequired(propertyName, value)
SetEntityState(EntityStateType.Modified, propertyName)
End If
End Set
End Property
Public Overrides Function SaveItem() As Boolean
' TODO: Add code here
End Function
End Class
Notice how the class statement includes the syntax to inherit from BoBase. The LastName property uses the ValidationInstance defined in the base class to validate the value. It also sets the entity state when the last name is changed.
Enjoy!