Introduction
This article is about building a comprehensive and easy-to-use multi-tiered framework. In this framework, I brought some ideas together that were floating around in my head for a while. Throughout the article, I'll explain the different parts on which this framework is built and will explain their usage as clearly as possible. Before I forget, I should thank Philippe Leybaert for the great ORM mapper that I used as the backend for my data transport, as well as Rockford Lothka who brought me the basic idea of the DataPortal. Thanks also go to Kevin S. Goff for the BusinessFactory used in this framework.
Global framework solution layout in .NET
As you can see, the whole framework is composed of a bunch of projects. Each have their use, of course, which usage I'll explain throughout the consecutive paragraphs.
Data access layer explained
Activa.CoolStorage
Activa.CoolStorage
is an awesome and easy-to-use ORM tool. It compiles into a DLL and you can use it as the backend for all of your data transport. CoolStorage has a lot of features for loading relational data into object properties. It also supports many types of object mapping, including 1-to-many and many-to-many. Unlike other ORM-mappers, which spawn a lot of static code for data access and mapping, CoolStorage is based on reflection, generics and attribute usage. This makes data access and ORM-mapping transparent for the programmer. The proposed framework will take advantage of the CoolStorage ORM mapper in the DataService layer -- to load or persist data -- and the BusinessLogic layer, to filter or validate data. You can find CoolStorage here.
DataPortal
DataPortal's sole purpose is to serve as a router for our data transport between layers. Before getting further, it's important to know the difference between a "layer" and a "tier." A "layer" is a logical representation, whereas a "tier" is a physical representation. This means that a layered framework can reside on one sole machine -- i.e. the DataAccess, Business and Presentation code reside on the same machine -- or we can spread each layer to another machine so that you get multiple physical "tiers." As you will see from the code that I'll explain in a moment, DataPortal is designed to work in local mode or remote mode. The mode used depends on settings in the App.Config file in both Business and DataAccessLayer.
ClientSide DataPortal
This class is intended for use by the client, while our previous two classes, DataService
and DataPortalServer
, were intended for use on the server. The namespaces help keep the purpose of the classes clear to the programmers who will be using the data portal. To make the use of the data portal as simple as possible, this class contains all of the code necessary to find the data portal, either in the client's process or on a remote machine. The actual location of the data portal is determined by a configuration file setting.
public class DataPortal< T >
{
#region "Storage"
private static Server.DataPortal< T > _Portal;
#endregion "Storage"
#region "Private Interface"
private static Server.DataPortal< T > Portal()
{
try
{
if (_Portal == null)
{
string svr =
ConfigurationManager.AppSettings[
"BGPRemoteDataPortalServer"];
if (svr != null)
{
if (svr.Length > 0)
{
RemotingConfiguration.RegisterWellKnownClientType(
typeof(Server.DataPortal< T >), svr);
}
}
_Portal = new Server.DataPortal< T >();
}
return _Portal;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion "Private Interface"
#region "Public Interface"
public static object GetObjectData(string p_service,
object p_criteria, Predicates p_predicate)
{
try
{
return Portal().GetObjectData(p_service,
p_criteria, p_predicate);
}
catch (Exception ex)
{
throw ex;
}
}
public static int PersistObjectData(string p_service,
ref object p_object, Predicates p_predicate,
out DbConcurrencyHandler< T > p_dbConcHandler)
{
try
{
return Portal().PersistObjectData(p_service,
ref p_object, p_predicate, out p_dbConcHandler);
}
catch (Exception ex)
{
throw ex;
}
}
#endregion "Public Interface"
}
This code creates a shared function that returns a data portal object. The Portal
function contains the code to read the application's configuration file to find the location of the portal server. If the portal server entry doesn't exist in the configuration file, then we assume that the data services should run in the same process as the client, so remoting is not configured. However, if the PortalServer
variable is found in the configuration file, then remoting is configured such that the data portal object is created by using the URL contained in the PortalServer
entry in the App.Config file. Notice that the Portal
method is scoped as Private
. There's no need for the client application to know about these details. The client code should be concerned merely with retrieving or updating data.
ServerSide DataPortal
This code acts merely as a router, finding the appropriate data service DLL on request from a client application. It will work when running in the same process as the UI or when running on a different machine using remoting.
public class DataPortal< T > : MarshalByRefObject
{
private DataService< T > GetService(string p_serviceName)
{
try
{
string[] s = p_serviceName.Split(',');
Assembly a = Assembly.Load(s[0]);
Type t = a.GetType(s[1]);
return (DataService< T >)Activator.CreateInstance(t, true);
}
catch (Exception ex)
{
throw ex;
}
}
public virtual object GetObjectData(string p_service,
object p_criteria, Predicates p_predicate)
{
try
{
return GetService(p_service).GetObjectData(p_criteria,
p_predicate);
}
catch (Exception ex)
{
throw ex;
}
}
public virtual int PersistObjectData(string p_service,
ref object p_object, Predicates p_predicate,
out DbConcurrencyHandler< T > p_dbConcHandler)
{
try
{
return GetService(p_service).PersistObjectData(ref p_object,
p_predicate, out p_dbConcHandler);
}
catch (Exception ex)
{
throw ex;
}
}
}
In the GetService
function, we are using reflection to dynamically load the data service DLL into our process and then to create an instance of the appropriate data service class. The result is a dynamically loaded data service object based on the assembly and type name string value provided by the UI code. This mechanism eliminates any need for the client code to directly reference the data service DLL. Instead, the client code needs only to reference the vbDataPortal
project and it will take care of finding and loading the data service DLL.
Because we've eliminated this reference, we can more easily update both the UI and the data service DLL without having to worry about dependencies that might force us to recompile and redeploy code unnecessarily. Notice that this code is also contained within the vbDataPortal.Server
namespace. This is the same namespace as the DataService
class, which makes sense because both of these classes will be running on the server.
DataService Interface
This is the base class that implements IDataService
and from which any data service will inherit. Data services should provide CRUD -- i.e. create, read, update, delete -- services for our application. This is achieved via the GetObjectData()
and PersisObjectData()
methods.
public abstract class DataService< T > : IDataService< T >
{
#region "Storage"
private Exception _dataServiceException;
public Exception DataServiceException
{
get { return this._dataServiceException; }
set { this._dataServiceException = value; }
}
private int _affectedRecords;
public int AffectedRecords
{
get { return this._affectedRecords; }
set { this._affectedRecords = value; }
}
#endregion "Storage"
#region "Protected Interface"
protected DataService()
{
}
#endregion "Protected Interface"
#region "Public Interface"
public virtual object GetObjectData(object p_criteria,
Predicates p_predicate)
{
Exception ex =
new System.Exception("DataService, GetData not allowed!");
throw ex;
}
public virtual int PersistObjectData(ref object p_object,
Predicates p_predicate,
out DbConcurrencyHandler < T > p_dbConcHandler)
{
Exception ex =
new System.Exception("DataService, SaveData not Allowed !");
throw ex;
}
#endregion "Public Interface"
}
public interface IDataService< T >
{
object GetObjectData(object p_criteria, Predicates p_predicate);
int PersistObjectData(ref object p_object,
Predicates p_predicate,
out DbConcurrencyHandler< T > p_dbConcHandler);
}
As you may have noticed, I've added a DBConcurrencyHandler
parameter. As the underlying data storage handler CoolStorage doesn't currently support Optmistic Concurrency, this parameter isn't used for the moment. I recently read on the CodePlex site -- home of CoolStorage -- that there are plans to implement DbConcurrency in the near future!
DataServices
As the DataPorta
l part creates -- either locally or remotely, depending on the application settings -- instances of the DataService
class, the DataService
class itself implements the methods for retrieving and persisting our business object data. DataService relies on the CoolStorage ORM mapper to get the work done.
public class CategoryDataService : DataService< Category >
{
#region "Private Interface"
private CategoryDataService()
{
}
private object CategoriesGetAll()
{
return (object)Category.List();
}
private object IsUniqueCategoryName(List< String > p_criteria)
{
int categoryID = Convert.ToInt32(p_criteria[0]);
string categoryName = p_criteria[1];
Category oCategory =
Category.ReadFirst(
"CategoryID <> @CategoryID AND CategoryName=@CategoryName",
"@CategoryID",categoryID,
"@CategoryName", categoryName);
if (oCategory == null)
return (object)true;
return (object)false;
}
#endregion "Private Interface"
#region "Public Interface"
public override object GetObjectData(object p_criteria,
Predicates p_predicate)
{
try
{
switch (p_predicate)
{
case Predicates.CategoriesGetAll:
return this.CategoriesGetAll();
case Predicates.IsUniqueCategoryName:
return this.IsUniqueCategoryName(
(List< String >)p_criteria);
default:
Exception invalidPredicateEx =
new Exception(string.Format("{0} [{1}]",
ResourceParser.GetResourceString("INVALIDPREDICATE",
"rsErr"), p_predicate));
throw invalidPredicateEx;
}
}
catch (Exception ex)
{
throw ex;
}
}
public override int PersistObjectData(ref object p_object,
Predicates p_predicate,
out DbConcurrencyHandler< Category > p_dbConcHandler)
{
AffectedRecords = 0;
p_dbConcHandler = new DbConcurrencyHandler< Category >();
using (CSTransaction trans =
new CSTransaction(IsolationLevel.ReadCommitted))
{
int n = (p_object as CSList< Category >).Count;
int c = 0;
while (n > 0)
{
Category oCategory = (p_object as CSList< Category >)[c];
if (oCategory.IsMarkedForDelete)
{
oCategory.Delete();
}
else
{
oCategory.Save();
c++;
}
AffectedRecords++;
n--;
}
trans.Commit();
}
return AffectedRecords;
}
#endregion "Public Interface"
}
Accessing DataService
components through DataPortal
and abstracting the DataService
classes from BusinessLayer
has several benefits. Using DataPortal
for DataService
creation and manipulation benefits us by making our service "remotable." As such, you can create a Windows service to ghost your data service objects and expose them through HTTP or FTP channels.
Alternatively, you can ghost your data service classes in IIS. One drawback concerning IIS hosting is the fact that HTTP via SOAP does not support generic stuff. A possible solution is to "pack" generic instances in byte-arrays and serialize on the client. Deserializing would occur on the server. However, this is not within the scope of this article.
At this point, I also discuss briefly the Predicate
enumeration. Each BusinessLogic
class will consist of several methods for data retrieval and update. Predicate
will hold the type of action to execute on the server. This abstracts the business layer programmer from knowing the implementation details of the DataService
layer.
Domain layer explained
Our domain layer describes our domain objects. They inherit from the CoolStorage.CSObject
class. Properties should be exposed as public abstract
. For more information on using CoolStorage domain classes, feel free to visit the home of CoolStorage at CodePlex.
[MapTo("Categories")]
public abstract partial class Category : CSObject< Category, int >
{
[DefaultSort]
public abstract System.Int32 CategoryID { get;}
public abstract System.String CategoryName { get; set;}
public abstract System.String Description { get; set;}
public abstract System.Byte[] Picture { get; set;}
}
BusinessLayer explained
BusinessBase
This is an abstract base class for our real-world BusinessLogic
implementation classes. It contains all necessary methods to retrieve, validate and persist our business object data. This class is hooked to the client part of our DataPortal
. You may have noticed that only changed objects -- as in newly inserted, modified or deleted objects -- are sent over the borders. The changed objects list is sent by "ref," which facilitates server-side calculation. An example would be calculating autonumbers to be mapped back immediately to the underlying object list and refreshed in the Presentation
layer. Refer to the code below for more detailed information.
namespace BGP.BOL.Base
{
public abstract class BusinessBase< T > :
MarshalByRefObject where T : CSObject< T >
{
#region "Storage"
protected int _affectedRows;
public int AffectedRows
{
get { return this._affectedRows; }
protected set { this._affectedRows = value; }
}
protected string _serviceName;
public string ServiceName
{
get { return this._serviceName; }
set { this._serviceName = value; }
}
private BusinessRule< T > _businessRuleObject;
public BusinessRule< T > BusinessRuleObject
{
get { return this._businessRuleObject; }
set { this._businessRuleObject = value; }
}
protected DbConcurrencyHandler< T > _dbConcHandler;
public List< T > DbConcurrencyErrors
{
get { return this._dbConcHandler.DbConcurrencyErrors; }
}
protected List< T > _originalObjectList;
protected CSList< T > _currentObjectList;
protected List< int > _lineNums;
#endregion "Storage"
#region "Public Interface"
public BusinessBase()
{
_originalObjectList = new List< T >();
_lineNums = new List< int >();
}
public abstract int Persist();
#endregion "Public Interface"
#region "IDataService Implementation"
public object GetObjectData(object p_criteria, Predicates p_predicate)
{
try
{
return DATAPORTAL.Client.DataPortal< T >.GetObjectData(
this.ServiceName, p_criteria, p_predicate);
}
catch (Exception ex)
{
throw ex;
}
}
public int PersistObjectData(Predicates p_predicate,
out DbConcurrencyHandler< T > p_dbConcHandler)
{
p_dbConcHandler = new DbConcurrencyHandler< T >();
try
{
int AffectedObjects = 0;
this._lineNums.Clear();
CSList< T > changes = this.GetChanges();
if (ValidateObjectData(changes))
{
object p_changes = (changes as object);
AffectedObjects +=
DATAPORTAL.Client.DataPortal< T >.PersistObjectData(
this.ServiceName, ref p_changes,
p_predicate, out p_dbConcHandler);
this._originalObjectList.Clear();
foreach (T oDomainObject in this._currentObjectList)
{
this._originalObjectList.Add(oDomainObject);
}
}
else
{
AffectedObjects = -1;
}
return AffectedObjects;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion "IDataService Implementation"
#region "Protected Interface"
protected static bool IsNewOrDirty(CSObject< T > p_domainObject)
{
if (p_domainObject != null)
return (p_domainObject.IsNew || p_domainObject.IsDirty);
return false;
}
protected static bool IsNew(CSObject< T > p_domainObject)
{
if (p_domainObject != null)
return (p_domainObject.IsNew);
return false;
}
protected static bool IsDeleted(CSObject< T > p_domainObject)
{
if (p_domainObject != null)
return (p_domainObject.IsDeleted);
return false;
}
protected static bool IsMarkedForDelete(CSObject< T > p_domainObject)
{
if (p_domainObject != null)
return (p_domainObject.IsMarkedForDelete);
return false;
}
protected static bool IsDirty(CSObject< T > p_domainObject)
{
if (p_domainObject != null)
return (p_domainObject.IsDirty);
return false;
}
#endregion "Protected Interface"
#region "Private Interface"
private bool ValidateObjectData(CSList< T > p_domainObjectList)
{
bool businessRulesPassed = true;
if (this.BusinessRuleObject == null)
return businessRulesPassed;
this.BusinessRuleObject.ClearBrokenRules();
int lineIndex = 0;
foreach (T domainObject in p_domainObjectList)
{
if (domainObject.IsDirty || domainObject.IsNew)
{
if (!(this.CheckRules(domainObject,
this._lineNums[lineIndex])) && businessRulesPassed)
{
businessRulesPassed = false;
}
}
lineIndex++;
}
return businessRulesPassed;
}
private bool CheckRules(T p_domainObject, int p_currentLineNumber)
{
try
{
if (this.BusinessRuleObject != null)
{
this.BusinessRuleObject.CheckRequiredFields(
p_domainObject, p_currentLineNumber);
this.BusinessRuleObject.CheckRulesHook(p_domainObject,
p_currentLineNumber);
}
return (this.BusinessRuleObject.BrokenRuleCount == 0);
}
catch (Exception ex)
{
throw ex;
}
}
private CSList< T > GetChanges()
{
CSList< T > changedObjects = new CSList< T >();
changedObjects.RemoveAll();
int lineIndex = 0;
foreach (T oDomainObject in this._currentObjectList)
{
lineIndex++;
if (oDomainObject.IsNew || oDomainObject.IsDirty)
{
changedObjects.Add(oDomainObject);
this._lineNums.Add(lineIndex);
}
}
for (int i = 0; i < this._originalObjectList.Count; i++)
{
if (!this._currentObjectList.Contains(
this._originalObjectList[i]))
{
lineIndex++;
T domainObject = this._originalObjectList[i];
domainObject.MarkForDelete();
changedObjects.Add(domainObject);
this._lineNums.Add(lineIndex);
}
}
return changedObjects;
}
#endregion "Private Interface"
}
}
BusinessRule
The BusinessRule
class takes care of validating the property data before sending the objects to the DAL. By default, two sorts of validation are implemented. Those are required fields and specific RulesHook
s. Broken rules are added to the BrokenRules
property, which can be exposed to the GUI layer. Feel free to examine the code beneath for an in-depth understanding of the BusinessRule
class.
public abstract class BusinessRule< T >
{
private object _businessObject;
public object BusinessObject
{
get { return this._businessObject; }
set { this._businessObject = value; }
}
public StringCollection BrokenRules = new StringCollection();
private StringCollection _requiredFields = new StringCollection();
public StringCollection RequiredFields
{
get { return this._requiredFields; }
set { this._requiredFields = value; }
}
public int BrokenRuleCount
{
get { return this.BrokenRules.Count; }
}
public void AddBrokenRule(string p_brokenRule)
{
try
{
if (!this.BrokenRules.Contains(p_brokenRule))
{
this.BrokenRules.Add(p_brokenRule);
}
}
catch (Exception ex)
{
throw ex;
}
}
public virtual void CheckRequiredFields(T p_domainObject,
int p_lineNumber)
{
try
{
PropertyDescriptorCollection props =
TypeDescriptor.GetProperties(p_domainObject);
int itemIndex = 0;
foreach (string Field in this.RequiredFields)
{
PropertyDescriptor prop = props.Find(Field, true);
if (prop != null)
{
string FieldName = "", FieldDesc = "";
int commapos = Field.IndexOf(",");
if (commapos > 0)
{
FieldName = Field.Substring(0, commapos).Trim();
FieldDesc = Field.Substring(commapos + 1).Trim();
}
else
{
FieldName = Field;
FieldDesc = Field;
}
bool IsEmpty;
string FieldType = prop.Name.GetType().ToString();
switch (FieldType.ToUpper())
{
case "SYSTEM.STRING":
IsEmpty =
prop.GetValue(
p_domainObject).ToString() == "";
break;
case "SYSTEM.INT32":
IsEmpty =
Int32.Parse(
prop.GetValue(
p_domainObject).ToString()) == 0;
break;
case "SYSTEM.DATETIME":
IsEmpty =
prop.GetValue(
p_domainObject).ToString() == "";
break;
case "SYSTEM.DECIMAL":
IsEmpty =
decimal.Parse(
prop.GetValue(
p_domainObject).ToString()) == 0;
break;
case "SYSTEM.DBNULL":
IsEmpty = true;
break;
default:
IsEmpty = false;
break;
}
if (IsEmpty)
{
this.AddBrokenRule(string.Format("{0} {1} [{2}]",
FieldDesc.ToString(),
ResourceParser.GetResourceString("REQUIREDFIELD",
"rsMsg"),p_lineNumber));
}
else
{
this.ClearRule(string.Format("{0} {1} [{2}]",
FieldDesc.ToString(),
ResourceParser.GetResourceString("REQUIREDFIELD",
"rsMsg"),p_lineNumber));
}
}
itemIndex++;
}
}
catch (Exception ex)
{
throw ex;
}
}
public virtual void CheckRulesHook(T p_domainObject, int p_lineNumber)
{
}
public void ClearBrokenRules()
{
this.BrokenRules.Clear();
}
public void ClearRule(string p_brokenRule)
{
try
{
if (this.BrokenRules.Contains(p_brokenRule))
{
this.BrokenRules.Remove(p_brokenRule);
}
}
catch (Exception ex)
{
throw ex;
}
}
public string GetBrokenRule(int p_index)
{
return this.BrokenRules[p_index];
}
}
Business objects
The business object logic classes are the real heart of our application. They hold, manipulate and validate instances of our domain objects. The BusinessLogic
class gets/persists object data through the business base to the DAL layer. The ServiceName
property indicates which DataService
to use. Each BusinessLogic
class should hold a business rule instance if validation is needed. As already mentioned, data access details are abstracted for the business programmer. Each BusinessLogic
class implements a business logic interface. As business objects should be "remotable," the presentation layer should only connect through a "proxy" and the interface contracts the available methods for the GUI programmer.
namespace BGP.BOL.BusinessLogic
{
public class CategoryBL : BusinessBase< Category >, ICategoryBL
{
#region "Storage"
public CSList< Category > Categories
{
get { return this._currentObjectList; }
set { this._currentObjectList = value; }
}
public StringCollection BrokenRules
{
get { return this.BusinessRuleObject.BrokenRules; }
}
#endregion "Storage"
#region "Public Interface"
public CategoryBL()
{
this.ServiceName =
global::BGP.BOL.Properties.Settings.Default.CategoryDataService;
this._dbConcHandler = new DbConcurrencyHandler< Category >();
this.BusinessRuleObject = new CategoryRules();
this.BusinessRuleObject.BusinessObject = this;
}
public void GetAll()
{
Categories =
(CSList< Category >)base.GetObjectData(null,
Predicates.CategoriesGetAll);
if (this._originalObjectList.Count > 0)
this._originalObjectList.Clear();
foreach (Category oCategory in Categories)
{
this._originalObjectList.Add(oCategory);
}
}
public int FilterByCategoryName(string p_categoryName)
{
CSList< Category > filter =
Categories.FilteredBy(
"CategoryName LIKE '" + p_categoryName + "%'");
return this.GetFilterPosition(filter);
}
public int FilterByDescription(string p_description)
{
CSList< Category > filter =
Categories.FilteredBy(
"Description LIKE '" + p_description + "%'");
return this.GetFilterPosition(filter);
}
public override int Persist()
{
AffectedRows =
base.PersistObjectData(Predicates.CategoriesPersist,
out this._dbConcHandler);
return AffectedRows;
}
#endregion "Public Interface"
#region "Private Interface"
private int GetFilterPosition(CSList< Category > p_filter)
{
if (p_filter.Count > 0)
{
int index = 0;
foreach (Category oCategory in Categories)
{
if (p_filter[0] == oCategory)
return index;
index++;
}
}
return -1;
}
#endregion "Private Interface"
}
}
The code below contains some simple validation rules for our category class. Required fields are added for CategoryName
and Description
. A RulesHook
is added saying that all newly added categories should have a unique CategoryName
in the databases. BrokenRule
s are saved in the BrokenRule
property, which can be exposed to the presentation layer. You may also have noticed the globalisation concepts put in the framework. In this, all messages are stored in language-agnostic resource files.
namespace BGP.BOL.BusinessLogic
{
public class CategoryRules : BusinessRule< Category >
{
#region "Public Interface"
public CategoryRules()
{
this.RequiredFields.Add("CategoryName");
this.RequiredFields.Add("Description");
}
public override void CheckRulesHook(Category p_domainObject,
int p_lineNumber)
{
try
{
string categoryID = string.Empty;
string categoryName = string.Empty;
PropertyDescriptorCollection props =
TypeDescriptor.GetProperties(p_domainObject);
PropertyDescriptor prop = props.Find("CategoryID", true);
if (prop != null)
{
categoryID = prop.GetValue(p_domainObject).ToString();
}
else
{
throw new Exception(string.Format(
ResourceParser.GetResourceString("PROPERTYNOTFOUND",
"rsErr"), "CategoryID"));
}
prop = props.Find("CategoryName", true);
if (prop != null)
{
categoryName = prop.GetValue(p_domainObject).ToString();
}
else
{
throw new Exception(string.Format(
ResourceParser.GetResourceString("PROPERTYNOTFOUND",
"rsErr"), "CategoryName"));
}
this.IsUniqueCategoryName(categoryID,
categoryName, p_lineNumber);
}
catch (Exception ex)
{
throw ex;
}
}
#endregion "Public Interface"
#region "Private Interface"
private void IsUniqueCategoryName(string p_categoryID,
string p_categoryName, int p_lineNumber)
{
List< string > criteria = new List< string >();
criteria.Add(p_categoryID);
criteria.Add(p_categoryName);
bool isUnique =
(bool)(this.BusinessObject as CategoryBL
).GetObjectData((object)criteria,
Predicates.IsUniqueCategoryName);
string rule = string.Format("{0} : {1} {2} [{3}]",
ResourceParser.GetResourceString("LBLCATEGORYNAME", "rsMsg"),
p_categoryName,
ResourceParser.GetResourceString("ALREADYEXISTS","rsErr"),
p_lineNumber);
if (!isUnique)
{
this.AddBrokenRule(rule);
}
else
{
this.ClearRule(rule);
}
}
#endregion "Private Interface"
}
}
Each BusinessLogic
class should implement an interface. This interface exposes the public method and properties that can be accessed by the presentation layer programmer. The purpose is double: abstracting business layer details for the presentation layer programmer and programming to the "interface" makes the object remotable.
namespace BGP.INTERFACES.BusinessLogic
{
public interface ICategoryBL
{
CSList< Category > Categories {get;set;}
StringCollection BrokenRules { get;}
void GetAll();
int FilterByCategoryName(string p_categoryName);
int FilterByDescription(string p_description);
int Persist();
}
}
Presentation layer explained
Finally, I'll explain the benefits of our framework as it comes to the presentation layer. Using the concepts mentioned above, programming the UI is really a piece of cake! I built the next little example -- ok it's little, but it explains the the how-tos -- in less than 1 hour, validation included! I've also taken advantage of the new binding features in VS 2005. Refer to my article "Custom Bindable BusinessObjects and the Typed DataSet" in the same section for more details.
So, our little program will consist of loading all "Categories" of the "NorthWind" SQL-Server database onto our form. We will include some validation rules and apply local filtering for CategoryName
and Description
. Before I delve deeper into the code, I should mention another helper class called BusinessFactory
. It prevents the presentation layer programmer from dealing with the creation details of our busines objects and exposes, if necessary, our business objects as remotable proxies. So, our business logic may reside on another machine. Because remoting is not really within the scope of this article, we will default to creating local instances of our objects.
namespace BGP.BOL.BusinessFactory
{
public class BusinessFactory
{
#region "Storage"
private static int _TCPPort;
public int TCPPort
{
get { return _TCPPort; }
set { _TCPPort = value; }
}
private static string _TcpServer;
public string TcpServer
{
get { return _TcpServer; }
set { _TcpServer = value; }
}
private Type _BoInterface;
public Type BoInterface
{
get { return _BoInterface; }
set { _BoInterface = value; }
}
private string _objectName;
public string ObjectName
{
get { return _objectName; }
set { _objectName = value; }
}
private bool _remoteCall = false;
public bool RemoteCall
{
get { return this._remoteCall; }
set { this._remoteCall = value; }
}
#endregion "Storage"
#region "Public Interface"
public BusinessFactory()
{
this.TCPPort = 8228;
string remoteCall =
global::BGP.BOL.BusinessFactory.Properties.Settings.Default.RemoteCall;
if (remoteCall.Trim() != string.Empty) this.RemoteCall = true;
}
public object GetInterfaceObject()
{
try
{
object oAccessObject = new object();
if (this.RemoteCall)
{
IChannel[] myIChannelArray =
ChannelServices.RegisteredChannels;
if (myIChannelArray.Length == 0)
ChannelServices.RegisterChannel(
new TcpClientChannel(), false);
oAccessObject =
Activator.GetObject(this.BoInterface,
this.TcpServer.ToString().Trim() + ":" +
this.TCPPort.ToString().Trim() + "/" + this.ObjectName);
}
else
{
string businessAssembly =
global::BGP.BOL.BusinessFactory.Properties.Settings.Default.BusinessNameSpace;
Assembly a = Assembly.Load(businessAssembly);
Type t =
a.GetType(businessAssembly +
".BusinessLogic." + this.ObjectName);
oAccessObject = Activator.CreateInstance(t);
}
return oAccessObject;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion "Public Interface"
}
}
Creating a simple application
In this last chapter, I'll explain the necessary steps for creating a simple form in the presentation layer. I'll explain the concepts by building a Windows Form, but feel free to port the example to a web-based application. The first thing to do is to add an instance to our business logic object in the storage section. You may notice that we are programming to the "interface," not to the full object.
#region "Storage"
ICategoryBL _categoryBL;
BusinessFactory _businessFactory;
#endregion "Storage"
In the Load
section of our form, we create an instance of BusinessObject
by means of the business factory. Next, we load all categories into our BusinessLogic
object and bind the CategoriesList
to the form's binding source.
private void frmCategories_Load(object sender, EventArgs e)
{
this._businessFactory = new BusinessFactory();
this._businessFactory.ObjectName = "CategoryBL";
this._categoryBL =
(ICategoryBL)this._businessFactory.GetInterfaceObject();
this._categoryBL.GetAll();
this.categoryBindingSource.DataSource = this._categoryBL.Categories;
}
private void descriptionTextBox1_TextChanged(object sender, EventArgs e)
{
try
{
int position =
this._categoryBL.FilterByDescription((sender as TextBox).Text);
if (position != -1)
{
categoryBindingSource.Position = position;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error !");
}
}
I've also added to Filter Texboxes on CategoryName
and Description
. Filter implementation resides in the business logic and is abstracted for the presentation layer programmer.
private void categoryNameTextBox1_TextChanged(object sender, EventArgs e)
{
try
{
int position =
this._categoryBL.FilterByCategoryName((sender as TextBox).Text);
if (position != -1)
{
categoryBindingSource.Position = position;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error !");
}
}
Finally, newly added, modified and deleted domain objects can be easily persisted to the database. BusinessLogic
's BusinessRule
class takes care of handling the validation rules.
private void categoryBindingNavigatorSaveItem_Click(object sender,EventArgs e)
{
try
{
this.categoryBindingSource.EndEdit();
int affectedObjects = this._categoryBL.Persist();
if (affectedObjects == -1)
{
frmGenericViewer bizzRuleViewer =
new frmGenericViewer(this._categoryBL.BrokenRules);
bizzRuleViewer.ShowDialog();
}
else if(affectedObjects > 0)
{
MessageBox.Show(string.Format(
ResourceParser.GetResourceString("SAVESUCCEEDED",
"rsMsg"), affectedObjects));
}
this.categoryBindingSource.ResetItem(
this.categoryBindingSource.Count - 1);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error !");
}
}
For testing purposes, I've tried to add a new category with an already existing name and empty description. As you can see, the rules are validated nicely.
Points of interest
I plan to work further on the proposed framework, to add features for more complex objects such as compositions and so on. The foundation is set, but more spice will be added. I also plan to make a Code Generating Tool for building the base DataService
, BusinessLogic
and DomainObject
classes. The CoolStorage backend is also still evolving and I'll put new features like concurrency and inheritance mapping as CoolStorage evolves. To make the sample code work, your system should have a working instance of SQL-Server 2005 and you should adapt the App.Config file to accommodate the right connection string.
Licences
As already mentioned, the ORM mapper CoolStorage used in this framework is not my property. When people plan to make use of it -- with or without the context of this framework -- for commercial purposes, they should contact the author. As mentioned by the license agreement on CodePlex/CoolStorage, I did not include the source code for it.
History
- 23 July, 2007 -- Original version posted
Working in the IT-Branch for more then 20 years now. Starting as a programmer in WinDev, moved to Progress, shifted to .NET since 2003. At the moment i'm employed as a .NET Application Architect at RealDolmen (Belgium). In my spare time, i'm a die hard mountainbiker and together with my sons Jarne and Lars, we're climbing the hills in the "Flemish Ardens" and the wonderfull "Pays des Collines". I also enjoy "a p'tit Jack" (Jack Daniels Whiskey) or a "Duvel" (beer) for "l'après VTT !".