Click here to Skip to main content
15,886,199 members
Articles / Desktop Programming / Windows Forms
Article

Remotable Multi-tiered Object-centric Domain Framework

Rate me:
Please Sign up or sign in to vote.
4.84/5 (12 votes)
23 Jul 200711 min read 36.1K   444   79   2
An article on building an agile multi-tiered business framework

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

Screenshot - fig_1.gif

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

Screenshot - fig_2.gif

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.

C#
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.

C#
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 Instance
            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.

C#
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 DataPortal 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.

C#
public class CategoryDataService : DataService< Category >
{
    #region "Private Interface"

    private CategoryDataService()
    {
        // Should not be instantiated manually
    }

    // Retrieves all Categories
    private object CategoriesGetAll()
    {
        return (object)Category.List();
    }

    // Returns true if CategoryName is not Found
    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.

C#
[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.

C#
namespace BGP.BOL.Base
{
    public abstract class BusinessBase< T > : 
        MarshalByRefObject where T : CSObject< T >
    {
        #region "Storage"

        /// Affected Rows When Persisting an Object
        protected int _affectedRows;
        public int AffectedRows
        {
            get { return this._affectedRows; }
            protected set { this._affectedRows = value; }
        }

        /// DataService to Launch
        protected string _serviceName;
        public string ServiceName
        {
            get { return this._serviceName; }
            set { this._serviceName = value; }
        }

        /// Holds the BusinessRule Object
        private BusinessRule< T > _businessRuleObject;
        public BusinessRule< T > BusinessRuleObject
        {
            get { return this._businessRuleObject; }
            set { this._businessRuleObject = value; }
        }

        /// Holds domainobjects which violated concurrency
        protected DbConcurrencyHandler< T > _dbConcHandler;
        public List< T > DbConcurrencyErrors
        {
            get { return this._dbConcHandler.DbConcurrencyErrors; }
        }   

        /// Keep track of original loaded Objects for Delete Purpose
        protected List< T > _originalObjectList;

        /// Holds the Current Object List
        protected CSList< T > _currentObjectList;

        /// For Validation purpose
        protected List< int > _lineNums;
                
        #endregion "Storage"
        #region "Public Interface"

        /// Default c'tor
        public BusinessBase()
        {
            _originalObjectList = new List< T >();
            _lineNums = new List< int >();  
        }

        public abstract int Persist();
        #endregion "Public Interface"
        #region "IDataService Implementation"
        /// Returns an object containing data returned
        /// by executing the specified predicate
        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;
            }
        }
                
        /// Validates BizzRules and Saves the Data
        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);

                    //--- Rebuild Original list
                    this._originalObjectList.Clear();
                    foreach (T oDomainObject in this._currentObjectList)
                    {
                        this._originalObjectList.Add(oDomainObject);
                    }
                }
                else
                {
                    //--- Indicate that 1 or more Rules are Broken.
                    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"

        /// Checks the businessrules for every Newly 
        /// Added or Modified domainobject
        private bool ValidateObjectData(CSList< T > p_domainObjectList)
        {  
            bool businessRulesPassed = true;
            if (this.BusinessRuleObject == null)
                return businessRulesPassed;

            //--- Clear any broken rules
            this.BusinessRuleObject.ClearBrokenRules();
            int lineIndex = 0;

            //--- Check all businessrules on all changed 
            // domain objects in the objectlist
            foreach (T domainObject in p_domainObjectList)
            {
                //--- Only validate new or modified doman objects
                if (domainObject.IsDirty || domainObject.IsNew)
                {
                    if (!(this.CheckRules(domainObject, 
                        this._lineNums[lineIndex])) && businessRulesPassed)
                    {
                        businessRulesPassed = false;
                    }
                }
                lineIndex++;
            }             
            return businessRulesPassed;
        }

        /// Check's the BusinessRules before Persisting the domainobjects
        private bool CheckRules(T p_domainObject, int p_currentLineNumber)
        {
            try
            {
                if (this.BusinessRuleObject != null)
                {
                    //--- First check for any required fields
                    this.BusinessRuleObject.CheckRequiredFields(
                        p_domainObject, p_currentLineNumber);

                    //--- Check other Business Rules
                    this.BusinessRuleObject.CheckRulesHook(p_domainObject, 
                        p_currentLineNumber);
                }
                return (this.BusinessRuleObject.BrokenRuleCount == 0);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// Get Newly added, Modified or Deleted domainobjects
        private CSList< T > GetChanges()
        {
            //--- Create domain object list to hold changes
            CSList< T > changedObjects = new CSList< T >();
            changedObjects.RemoveAll();

                      
            //--- Next add new and modified to changed, new to original
            int lineIndex = 0;
            foreach (T oDomainObject in this._currentObjectList)
            {

                lineIndex++;

                if (oDomainObject.IsNew || oDomainObject.IsDirty)
                {
                    changedObjects.Add(oDomainObject);
                    this._lineNums.Add(lineIndex);
                }

            }

            //--- Next Add Deleted
            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);
                }
            }

            //--- Finally Return the changes
            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 RulesHooks. 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.

C#
public abstract class BusinessRule< T >
{
    /// Holds the Contained BusinessObject
    private object _businessObject;
    public object BusinessObject
    {
        get { return this._businessObject; }
        set { this._businessObject = value; }
    }

    /// Holds the BrokenRules
    public StringCollection BrokenRules = new StringCollection();

    /// Holds the Required Fields
    private StringCollection _requiredFields = new StringCollection();
    public StringCollection RequiredFields
    {
        get { return this._requiredFields; }
        set { this._requiredFields = value; }
    }

    /// Counts Number of BrokenRules
    public int BrokenRuleCount
    {
        get { return this.BrokenRules.Count; }
    }

    /// Add Broken Rule to Broken Rules Collection
    public void AddBrokenRule(string p_brokenRule)
    {
        try
        {
            if (!this.BrokenRules.Contains(p_brokenRule))
            {
                this.BrokenRules.Add(p_brokenRule);
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    /// Check if all Required fields are entered for a given row
    public virtual void CheckRequiredFields(T p_domainObject, 
        int p_lineNumber)
    {
        try
        {
            //--- Get the properties of the domainobject
            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;
        }
    }

    /// Check specific business rules others then required fields
    /// Implementation comes in inherited object
    public virtual void CheckRulesHook(T p_domainObject, int p_lineNumber)
    {       
    }

    /// Clears the broken rules collection
    public void ClearBrokenRules()
    {
        this.BrokenRules.Clear();
    }

    /// Clears a specific rule from the broken rules collection
    public void ClearRule(string p_brokenRule)
    {
        try
        {
            if (this.BrokenRules.Contains(p_brokenRule))
            {
                this.BrokenRules.Remove(p_brokenRule);
            }
        }
        catch (Exception ex)
        {
           throw ex;
        }
    }

    /// Gets a specific broken rule by index
    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.

C#
namespace BGP.BOL.BusinessLogic
{
    public class CategoryBL : BusinessBase< Category >, ICategoryBL
    {
        #region "Storage"

        /// DataContainer
        public CSList< Category > Categories
        {
            get { return this._currentObjectList; }
            set { this._currentObjectList = value; }
        }

        /// Used by the Presentation Layer
        public StringCollection BrokenRules
        {
            get { return this.BusinessRuleObject.BrokenRules; }
        }

        #endregion "Storage"

        #region "Public Interface"

        /// Default c'tor
        public CategoryBL()
        {
            //--- Set DataServiceName
            this.ServiceName = 
              global::BGP.BOL.Properties.Settings.Default.CategoryDataService;

            //--- Create Optimistic ConcurrencyHandler
            this._dbConcHandler = new DbConcurrencyHandler< Category >();

            //--- Hook BusinessRule Object
            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. BrokenRules 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.

C#
namespace BGP.BOL.BusinessLogic
{
    public class CategoryRules : BusinessRule< Category >
    {
        #region "Public Interface"
        
        public CategoryRules()
        {
            //--- Required fields for Category
            this.RequiredFields.Add("CategoryName");
            this.RequiredFields.Add("Description");
        }

        public override void CheckRulesHook(Category p_domainObject, 
            int p_lineNumber)
        {
            try
            {
                //--- Init Var
                string categoryID = string.Empty;
                string categoryName = string.Empty;

                //--- Get the properties of the domainobject
                PropertyDescriptorCollection props = 
                    TypeDescriptor.GetProperties(p_domainObject);
                
                //--- Extract the categoryID
                PropertyDescriptor prop = props.Find("CategoryID", true);

                if (prop != null)
                {
                    //--- Get value
                    categoryID = prop.GetValue(p_domainObject).ToString();
                }
                else
                {
                    throw new Exception(string.Format(
                        ResourceParser.GetResourceString("PROPERTYNOTFOUND", 
                        "rsErr"), "CategoryID"));
                }

                //--- Extract the categoryName
                prop = props.Find("CategoryName", true);

                if (prop != null)
                {
                    //--- Get value
                    categoryName = prop.GetValue(p_domainObject).ToString();
                }
                else
                {
                    throw new Exception(string.Format(
                        ResourceParser.GetResourceString("PROPERTYNOTFOUND", 
                        "rsErr"), "CategoryName"));
                }

                 //--- Check if new or modified categoryname is unique
                 this.IsUniqueCategoryName(categoryID,
                     categoryName, p_lineNumber);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        #endregion "Public Interface"
        #region "Private Interface"

        /// A CategoryName should only be put ones in the category table
        private void IsUniqueCategoryName(string p_categoryID, 
            string p_categoryName, int p_lineNumber)
        {
            //--- Format criteria
            List< string > criteria = new List< string >();
            criteria.Add(p_categoryID);
            criteria.Add(p_categoryName);
            
            //--- Check if categoryname is unique
            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)
            {
                //--- Rule is Broken
                this.AddBrokenRule(rule);
            }
            else
            {
                //--- Rule is Not Broken, clear if exists
                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.

C#
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.

C#
namespace BGP.BOL.BusinessFactory
{
    public class BusinessFactory
    {
        #region "Storage"

        /// Set at startup when user logs in, comes from 
        /// connection profile, default is 8228
        private static int _TCPPort;
        public int TCPPort
        {
            get { return _TCPPort; }
            set { _TCPPort = value; }
        }

        /// Server Host
        private static string _TcpServer;
        public string TcpServer
        {
            get { return _TcpServer; }     
                // this is concatenated with the port...it could be: 
                // "tcp://localhost:"
            set { _TcpServer = value; }   
                // by the time they're all concatened, 
                // it could be "tcp://localhost:8228/CUSTOMERLOADER");
        }

        /// BO Interface Type
        private Type _BoInterface;
        public Type BoInterface
        {
            get { return _BoInterface; }
            set { _BoInterface = value; }
        }

        /// BO ObjectName
        private string _objectName;
        public string ObjectName
        {
            get { return _objectName; }
            set { _objectName = value; }
        }

        /// Indicates if bizzObjects should be remotely created
        private bool _remoteCall = false;
        public bool RemoteCall
        {
            get { return this._remoteCall; }
            set { this._remoteCall = value; }
        }

        #endregion "Storage"
        #region "Public Interface"

        /// Default c'tor
        public BusinessFactory()
        {
            // Set default TCP port
            this.TCPPort = 8228;

            // Set Remote Call
            string remoteCall = 
       global::BGP.BOL.BusinessFactory.Properties.Settings.Default.RemoteCall;

            if (remoteCall.Trim() != string.Empty) this.RemoteCall = true;
        }

        /// Gets Object Instance for the Given Interface
        public object GetInterfaceObject()
        {
            try
            {

                // Generic back-end object (will be cast to interface)
                object oAccessObject = new object();

                if (this.RemoteCall)
                {
                    // TCP remoting....must create new TCP channel
                    IChannel[] myIChannelArray = 
                        ChannelServices.RegisteredChannels;
                    if (myIChannelArray.Length == 0)
                        ChannelServices.RegisterChannel(
                        new TcpClientChannel(), false);

                    // activate back-end object
                    oAccessObject =
                      Activator.GetObject(this.BoInterface,
                      this.TcpServer.ToString().Trim() + ":" +
                      this.TCPPort.ToString().Trim() + "/" + this.ObjectName);
                }
                else
                {
                    // Return local Instance
                    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.

C#
#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.

C#
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;
}

/// Filter on Description
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.

C#
/// Filter on CategoryName
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.

C#
private void categoryBindingNavigatorSaveItem_Click(object sender,EventArgs e)
{
    try
    {
        this.categoryBindingSource.EndEdit();

        int affectedObjects = this._categoryBL.Persist();

        if (affectedObjects == -1)
        {
            //--- Rules are broken, show them
            frmGenericViewer bizzRuleViewer = 
                new frmGenericViewer(this._categoryBL.BrokenRules);
            bizzRuleViewer.ShowDialog();
        }
        else if(affectedObjects > 0)
        {
            //--- Save Succeeded
            MessageBox.Show(string.Format(
            ResourceParser.GetResourceString("SAVESUCCEEDED", 
            "rsMsg"), affectedObjects));
        }

        //--- Refresh to reflect new CategoryID
        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.

Screenshot - fig_3.gif

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect REALDOLMEN
Belgium Belgium
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 !".

Comments and Discussions

 
GeneralNeed some help on a similar one that I am looking into Pin
VinodBhawnani2-Sep-08 10:32
VinodBhawnani2-Sep-08 10:32 
GeneralNice! Smart! Perfect! [modified] Pin
depmoddima19-Dec-07 22:47
depmoddima19-Dec-07 22:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.