Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Pretty URLs, Separation of Layers and O/R Mapping in Web Forms ASP.NET 2.0

4.47/5 (14 votes)
12 Sep 200732 min read 1   495  
Implementing multi-tier architecture in a web application using ASP.NET 2.0

Screenshot - Depot1.png

Fig. A screen shot of the Depot web application

Introduction

In this article, an approach to implementing multi-tier architecture in web applications is proposed using ASP.NET 2.0. The author achieves a clean separation between Data Model, Business Logic and User Interface by utilizing communication through interfaces. Moreover, in the Data Model a data manipulation is performed using powerful NHibernate NHibernate O/R mapper, which is a port of the Hibernate O/R mapper from the Java world. Lastly, the framework makes use of the so-called pretty URLs, which is also a selling point.

The author is aware of the Castle MonoRails and Maverick.NET frameworks which aim to introduce the MVC concepts into the ASP.NET world. Doing so, the MonoRails framework ignores the page life cycle of ASP.NET. Developers also make claims that webforms used as a view do not work too smoothly. On the other hand, in Maverick.NET a code behind is treated as a controller which encourages mixing Business Logic with Presentation Layer.

Alternatively, readers may read the article by Billy McCafferty Model View Presenter with ASP.NET. In that article the author addresses the separation of layers in a web application in ASP.NET 2.0 by implementing the MVP architecture (plus observer pattern). In contrast, in the approach present here the author removes the knowledge of the view of model. Therefore the proposed approach here should be labeled as "3-tier" rather than "MVP". Another difference is that instead of the Observer Pattern all communication is performed through interfaces by calling each tiers methods directly.

Additionally, this article addresses all of the concerns raised by Acoustic in his article Advancing the Model-View-Presenter Pattern - Fixing the Common Problems. This work does not have as a goal to improve on the two aforementioned solutions by Billy McCafferty and Acoustic. It should be rather considered as a viable alternative, which addresses the concerns mentioned in Advancing the Model-View-Presenter Pattern - Fixing the Common Problems unintentionally.

In this approach, nothing in the ASP.NET 2.0 Framework is ignored while separating Data Model, Business Logic and User Interface. As it will be shown, a full power of the ASP.NET 2.0 facilities (including web forms) is used to obtain a multi-tier architecture avoiding at the same time any collisions with the ASP.NET 2.0 page life cycle. In the author's opinion what makes this mini web framework attractive is that it has all the elements of the web application in order to be used (when a few conventions are followed) in the enterprise environment.

Firstly, the author proposes how to integrate the routing process (without using the System.Reflections namespace) with the page life cycle and to how to easily implement clean URLs. Secondly, the code reuse is an important issue for the author. Therefore a communication between tiers is strictly through interfaces. To achieve full portability of the controller, the Supervising Controller pattern has been implemented in the way that the controller has a minimum knowledge about view.

Thirdly, it is an example of how to integrate the NHibernate O/R mapper (inside fully cached Data Model tier) with the application tiers. The author discusses also how to gradually get rid of the ObjectDataSource control which limits design freedom and is not suitable to use on high traffic websites. The Depot web application is included as the illustration of how to build a fully functional and easy-to-customize, enterprise-ready, mini web framework.

Below is a flowchart of functionality of the web framework presented in this article. All aspects of it will become clearer as the reader goes through subsequent paragraphs of this article.

Screenshot - Depot2.png

Fig. A flowchart of functionality for the presented web framework

Using the Code

Using the Depot demo web application does not require any special configuration besides uncompressing it. The only requirement is ASP.NET 2.0 and SQL Server (was tested on SQL Server 2005 and Visual Web Developer 2005 express editions). A reader can also view the directory structure of this web application in the tree.info file by unzipping the Depot_src.zip file. The password for the superuser "user" on the administrative page login.aspx is "user".

Page Life Cycle

In our framework we are particularly interested in three events (marked in bold) of the HttpApplication class (see MSDN page for reference).

  • BeginRequest
    • Nhibernate session is created
    • Routing is carried out
    • Controller is created and stored in Factory
  • AuthenticateRequest
  • PostAuthenticateRequest
  • AuthorizeRequest
  • PostAuthorizeRequest
  • ResolveRequestCache
  • PostResolveRequestCache
  • PostMapRequestHandler
  • AcquireRequestState
  • PostAcquireRequestState
    • Pre-fetch data for a view
  • PreRequestHandlerExecute
  • PostRequestHandlerExecute
  • ReleaseRequestState
  • PostReleaseRequestState
  • UpdateRequestCache
  • PostUpdateRequestCache
  • EndRequest
    • Delete Nhibernate session

The above three events of interest BeginRequest, PostAcquireRequestState, EndRequest are implemented in RoutingClass which functions as an ASP.NET module. RoutingClass is responsible for many things. First of all, it creates and destroys the NHibernate session and makes it available to the DAL layer. Second of all, it takes care of URL rewriting (uses the RewriteUrlClass class and the FormRewriterControlAdapter class located in /App_Code/Routing/FormRewriter.cs). Lastly, it creates an instance of the controller inferred from URL, stores it in the Factory, determines name of view from the created controller and calls its properties to pre-fetch data for the determined view. Below is a listing of simplified version of the RoutingClass (is located in /App_Code/Routing/RoutingModule.cs).

C#
using System;
using System.Web;
using NHibernate;
using NHibernate.Cfg;
using System.IO;
using System.ComponentModel;

namespace Routing.Module
{
    public sealed class RoutingClass : IHttpModule
    {

     public static readonly string KEY = "NHibernateSession";
        
        public void Dispose() { }

        public void Init(HttpApplication context)
        {            
            context.BeginRequest += new EventHandler(context_BeginRequest);
            context.PostAcquireRequestState += 
                new EventHandler(context_PostAcquireRequestState);
            context.EndRequest += new EventHandler(context_EndRequest);
        }

        private void context_BeginRequest(object sender, EventArgs e)
        {
        .........
                context.Items[KEY] = getFactory().OpenSession(); 
                RewriteUrlClass RewriteUrl = new RewriteUrlClass();
                RewriteUrl.RewriteToCustomUrl();   //Rewrite Url 
        }

        private void context_PostAcquireRequestState(object sender, 
            EventArgs e)
        {
              .....

              PrefetchData();  
                   //given controller pre-fetches data for a view
              .....
              CurrentPage.PreRender += new EventHandler(Page_PreRender);
              ......
        }

        private void Page_PreRender(object sender, EventArgs e)
        {
                MVC.Views.IGeneralView InvokeMethod = 
                    sender as MVC.Views.IGeneralView;  
                    //view implements an interface 
        .....
                InvokeMethod.Alert(
                    MVC.Factories.Factory.PointerToFactory.Message);  
                    //display message stored in  the Factory
        ....
            }
        }

        private  void PrefetchData()
        {
           .......
            PropertyGet.SetValue(PropStoresController.GetValue(
                MVC.Factories.Factory.PointerToFactory), null);
        }
                       
        private void context_EndRequest(object sender, EventArgs e)
        {
              .......

             ISession TempSession = context.Items[KEY] as ISession; 
                if (TempSession != null)  
                {
                    try
                    {
                       TempSession.Flush();  
                       TempSession.Close();  
                    }
                    catch { }
                }
                context.Items[KEY] = null;
            }
        }
        
        private static ISessionFactory factory = null; 
            //is static that is fine, all users can share it
        private static ISessionFactory getFactory() 
            // assigns factory and returns it after
        {
           ........
            config.AddAssembly("App_Code");
            factory = config.BuildSessionFactory();
           .......
            return factory;
        }

        public static ISession NHibernateCurrentSession
        {
           .........
                    return HttpContext.Current.Items[KEY] as ISession;
                   .........
        }
    }
}

As can be seen from the code, the first thing our module does is create an NHibernate session on the BeginRequest event. Next, this session is stored in "HttpContext.Current.Items dictionary" so every request can have it's own unique session. A DAL layer has access to the NHibernate session through the static NHibernateCurrentSession "getter." On the EndRequest event this session is closed and resources freed. On the BeginRequest event we also call the RewriteToCustomUrl method and as the name indicates it takes care of URL rewriting (see URL Rewriting paragraph). This method also creates a controller instance and stores it in the Factory.

The next important thing our module does is that it pre-fetches data for view (when viewstate is disabled) on the PostAcquireRequestState event. Doing so we call a setter (in the PrefetchData method) of a controller (controller is determined from URL on a run-time) using a method from the System.ComponentModel namespace. Methods from this namesapce are used instead of methods from the System.Reflections namespace because usage of Reflections is restricted on a medium trust hosting environment. A choice of PostAcquireRequestState event is not coincidental at all. When a controller makes data available to a view it may at some point need an HTTP session (e.g for a shopping cart). When we look at the sequence of events of the HttpApplication class we notice clearly that the controller has to fetch data before HTTP handler is executed but after session data is available! At the end of PostAcquireRequestState event handler we also attach Page_Prerender event handler to the PreRender event of the current page being executed. This way a message can be displayed by view on the GET request coming from a controller which does not know about a particular view before page handler is executed (on POST request controller directly notifies view, on GET it cannot). Controller gets to know a particular view when an ASPX page is being executed (more in further paragraphs).

URL Rewriting

As it was mentioned it the previous paragraph the URL rewriting process takes place on the BeginRequest event of the HttpApplication class (in the RewriteToCustomUrl method). Regular expressions were chosen as a tool for URL rewriting. We will use the Depot web application to describe all the aspects of the web framework proposed here. This web application is described in the book "Agile Web Development with Rails (First Edition)". Initially, this project was followed by the author in it's original version written in "Ruby." The same project is used in this article to build a "3-tier" web application entirely written in C# using ASP.NET. We can look at this project grouped by controllers (bold) and it's views (ASPX pages). Controllers are located in the application's /App_Code/Controllers directory and views in /WebPages.

  • Store
    • store.aspx
    • display_cart.aspx
    • checkout.aspx
  • Admin
    • admin.aspx
    • show.aspx
    • create.aspx
    • edit.aspx
    • ship.aspx
  • Login
    • login.aspx
    • add_user.aspx
    • list_users.aspx

The Depot application is an example of a shopping cart used to sell books. A buyer can look at books (store.aspx), add them to a cart, display it (display_cart.aspx) and checkout at the end (checkout.aspx). Administrative tasks are done using Admin controller. Designated users can browse through all books (admin.aspx), display or edit a particular book (show.aspx or edit.aspx), add a new book to a catalog (create.aspx), or ship ordered books (ship.aspx). There is also the "Login" controller which takes care of authorizing and managing designated users with administrative privileges. Within it a user can login (login.aspx), a root can add a new user (add_user.aspx) or list all users (list_users.aspx).

In the Depot application the following regular expression is used to map a given URL

HTML
"/(<action>\w+)?(/)?(?<method>\w+)?(/)?(?<id>\d+)?"

(is stored in the Utils class in Utils.cs file) where action, method and id are named groups. In this framework every URL has to end with the ASPX extension to avoid any add_ons (for URL rewriting) to be installed into IIS. So for example, in our Depot project we can have the following URLs:

Admin controller
URLAction
/admin.aspxgo to administrative page
/admin/create.aspxcreate new product within Admin controller
/admin/edit/1.aspxedit the book with id=1 within Admin controller
/admin/show/1.aspxship the book with id=1 within Admin controller
/admin/sort_by_price.aspxsort books on the administrative page (by price) within Admin controller
/admin/ship_display_to_be_shipped.aspxdisplay ordered books ready to be shipped within Admin controller
/admin/ship_display_shipped.aspxdisplay books which where shipped within Admin controler



Store controller
URLAction
/store.aspxgo to main store.aspx page
/store/display_cart.aspxdisplay cart within store controller
/store/checkout.aspxdisplay checkout page within store controller



Login controller
URLAction
/login.aspxgo to login page
/login/add_user.aspxdisplay add user page within Login controller
/login/list_users.aspxdisplay list users page within Login controller

As we can see URLs are mapped to the physical location of ASPX pages based on the controller's name, method name (either "method".aspx page is called or a different one decided in controller) or the id ("id" provides additional information to the ASPX page). Generally, the "method" corresponds directly to the name of the ASPX page being invoked. It does not have to be always the case though as we have full flexibility. A good example is the following URL admin/sort_by_price.aspx. One may think that sort_by_price.aspx page is being invoked. There is really no need to create a different web page just to display data in a different manner. Instead, in a given controller (see Controller paragraph) we specify which ASPX page should be invoked i.e. admin.aspx. During the routing process a method name i.e sort_by_price is written into the HttpContext.Current.Items dictionary to be available for the admin.aspx page. Next, in the admin.aspx.cs code behind the presence of the method name in the HttpContext.Current.Items dictionary is discovered and data is sorted. This way can have a bookmarkable page displaying sorted data. Alternatively, we could use one of the server-side controls (e.g Button, LinkButton) on admin.aspx page to sort data, but the result would not be bookmarkable (HyperLink control is used instead). Below is included the "trimmed" version of RewriteUrlClass class responsible for URL rewriting and creation of controller (is located in /App_Code/Routing/RewriteUrl.cs).

C#
using System;
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Text.RegularExpressions;
using System.ComponentModel; //instead of System.Reflection, 
                             // which is not allowed on a medium/partial 
                             // trust environment
using MVC.Factories;         // on a shared hosting

public class RewriteUrlClass
{
    //Class responsible for a routing process for given a URL
    // Except routing a proper controller (inferred from Url) is 
    // also created here and
    // stored in the Factory.  

    //Private fields shared among "RewriteToCustomUrl" and 
    //"InitalizeController" methods
  
    private String WhichController; //name of a Controller inferred from Url
                                    // is set in RewriteToCustomUrl
                                    // needed by InitalizeController

    private String ViewToRender = null;// name of a view to render needed by  
                                      //RewriteToCustomUrl method
                                      //is set in InitalizeController method
    
    private Type TypeOfController;    //type of a Controller determined in 
                                      // RewriteToCustomUrl method
                                      // needed by InitalizeController method

    private PropertyDescriptor PropertyGet;//used to determine a view, 
                                           // is set in    
                                           // RewriteToCustomUrl method
                                           // needed by InitalizeController 
                                           // method
            
    public RewriteUrlClass() //contructor
    {
    }

    public void RewriteToCustomUrl()
    {
        String MatchedMethod;            
        //name of the method infered from Url
        String Url;                      //self-explanatory
        PropertyDescriptorCollection PropsOfController;  //self-explanatory

        Url = HttpContext.Current.Request.Path;
        .....

        String ReqUrlVar = Utils.Utils.ReqUrlVar; 
            //pattrern of a URL to be matched-is equal to
            // "/(?<action>\w+)?(/)?(?<method>\w+)?(/)?(?<id>\d+)?"

        Regex ReqUrlVarRegex = new Regex(ReqUrlVar, 
            RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
        Match UrlMatched = ReqUrlVarRegex.Match(Url.ToLower());

        if (UrlMatched.Success)     //if current Url matches 
        {
            String[] GetGrNames = ReqUrlVarRegex.GetGroupNames(); 
                // get  array of named capturing groups
                // skip unuseful zero element  
            try
            {
                //cannot really refactor anything here
                if (UrlMatched.Groups["method"].Value != "") 
                    //if  method supllied
                {
                    WhichController = 
                        Utils.Utils.UCFLetter(
                        UrlMatched.Groups["action"].Value);
                   ......
                    MatchedMethod = UrlMatched.Groups["method"].Value;
                    PropertyGet = PropsOfController[MatchedMethod]; 
                }
                else  //no method but the right controller or index
                {
                    if (UrlMatched.Groups["action"].Value == "index") 
                        //index
                    {
                        WhichController = 
                            Utils.Utils.UCFLetter(Utils.Utils.Controller);
                       ......
                        PropertyGet = 
                            PropsOfController[Utils.Utils.Controller]; 
                        HttpContext.Current.Items["action"] = 
                            WhichController.ToLower(); //store controller
                        HttpContext.Current.Items["method"] = 
                            Utils.Utils.Method;        //store method 
                    }
                    else //if we have a controller 
                    {
                        WhichController = 
                            Utils.Utils.UCFLetter(
                            UrlMatched.Groups["action"].Value);
                        ......
                        PropertyGet = 
                            PropsOfController[WhichController.ToLower()]; 
                    }
                }
                //here we fill Context.Items with the parsed Url
                // needed for views

                if (UrlMatched.Groups["action"].Value != "index") 
                    // for index it was done above
                    foreach (String SingleGroup in GetGrNames)
                    {
                        HttpContext.Current.Items[SingleGroup] = 
                            (String)UrlMatched.Groups[SingleGroup].Value;
                    }
                InitalizeController(); 
                // creates and stores controller, determines view

                //All that for this one line that translates pretty url into
                // physical location of a *.aspx file
                // Pages are stored in a ~/Webpages/WhichController directory
                // The view name is ViewToRender.aspx -- 
                // everything infrerred from Url and a controller properties
                HttpContext.Current.RewritePath("~/WebPages/" + 
                    WhichController + "/" +
                    ViewToRender + ".aspx", false);
            }
            catch (Exception) 
                // if something goes wrong  write an  error message
            {
                HttpContext.Current.Response.Write(
                    "Am error occured while routing!");
            }
        }
        else //if there is no match  display ~/index.aspx page
        {
            HttpContext.Current.RewritePath("~/index.aspx", false);
        }
    }

    private void InitalizeController()
    {
        //This methods creates controller, stores it 
        // in Factory 

        Factory FactoryInstance;         //self-explanatory  
        Type TypeOfFactory;              //type of a Factory
        PropertyDescriptorCollection PropsOfFactory; 

        object o = Activator.CreateInstance(TypeOfController); 
            //controller was created
        ViewToRender = PropertyGet.GetValue(o).ToString(); 
            // view was determined from a controller
            // for a given controller and method
            // is needed by RewriteToCustomUrl method

        //here we  store the instance of the contoller in the Factory

        //below code  block needs  WhichController field  - 
        // was determined above

        FactoryInstance = new Factory();   //create a factory instance
        Factory.PointerToFactory = FactoryInstance; 
            //store Factory instance in it's static field
            //same approach as in  Singleton pattern 

        TypeOfFactory = Type.GetType("MVC.Factories.Factory");
        PropsOfFactory = 
            TypeDescriptor.GetProperties(TypeOfFactory,
            Utils.Utils.ControllerInstanceProperties); 
            //get props of the Factory
        PropertyGet = PropsOfFactory[WhichController + "Controller"]; 
            //get property which stores controller
        PropertyGet.SetValue(FactoryInstance, o);                     
            // store controller instance in Factory 
    }
}

The RewriteToCustomUrl method is called on BeginRequest event in RoutingClass. First, the URL is dissected piece by piece and controller, method (all the named groups of regular expression) and so on are determined and stored in the HttpContext.Current.Items dictionary (for use by views). Once controller and a method are determined InitalizeController method is called. This method creates the Factory and stores it's instance in the HttpContext.Current.Items dictionary. In the next step, controller is created and stored in the Factory. The ASPX page to be invoked is determined from the created controller. After that, the RewritePath method is called (URL rewriting takes place) which takes as the input the name of controller and name of the ASPX page.

The above approach describes a situation when we have more than one controller and we call methods of the controller. There can be other situations when we do not have neither a controller nor a method to be called. In such cases we simply do not call the InitalizeController method, we change the line containing the RewritePath method and simplify the RewriteToCustomUrl method (removing anything unnecessary). An example can be the URL mapping pattern from the thekaulmd.info website which was recently created by the author. The regular expression for the URL is:

HTML
"/(?<DIR1>\w+)?(/)?(?<DIR2>\w+)?(/)?(?<DIR3>\w+)?(/)?(?<DIR4>\w+)?"

, where DIR1, DIR2... are named groups.

URLAction
/home/home.aspxgo to home.aspx page located in ~/.../home directory
/generalinfo/breathing.aspxgo to breathing.aspx page located in ~/.../generalinfo directory
/diseases/pulmonary/asthma.aspxgo to asthma.aspx page located in ~/.../diseases/pulmonary directory

Table. Example URLs from the thekaulmd.info website.

In such case instead of mapping into proper controller or a method we map from categories e.g. diseases, pulmonary etc. into physical location of the ASPX files.

View

A particalar view is executed on PreRequestHandlerExecute event of HttpApplication class. (See MSDN page for reference ). When constructing a view in a web application one has several choices. Obviously, this article is about the separation of Data Access, Business logic and Presentation Layer from each other. Therefore, a choice is dictated by a fact of how much of a view specific logic is handled in a code behind (code behind+aspx page=view ). In this framework all the view specific logic is handled in a code behind (Supervising Controller approach) as opposed to letting controller perform all the manipulation of view (passive view approach ).

The "Supervising Controller" approach was chosen because of it's greater portability in case of changes in view. This way a controller should not change if we decide to couple it with either a console, windows forms or web application. In contrast, if one uses a Passive View approach some controller's code has to be thrown away. So how is controller coupled with view ?

Screenshot - Depot3.png

Fig. Linear architecture of our framework. All the views are located in " /WebPages" directory.

As can be seen on the above figure, view requests data change from controller and controller notifies back the view about the status of the change. Controller on the other hand (see Controller paragraph) handles updating the data (does it of course through model). Such an approach leaves us with a linear architecture of our framework similar to three-tier architecture . This means that the presentation layer never communicates directly with model and uses controller to do so. In contrast, the MVC framework architecture is triangular, because view receives data directly from model (controller and model interact of course).

Anything not related to view specific logic and related to the Business Logic is forwarded to controller. Again here, we have some choice how view communicates with controller. One can implement a so-called observer pattern where controller listens to view's events. Whenever view requests a data change it throws an event to which controller subscribes. Another approach is to handle all actions through URLs as e.g. in the Rails web framework. I chose a third approach where view calls controller's method directly! I hear right now people screaming that I am exposing controller's methods! Yes, controller's methods are exposed through an interface. This is a very common approach demonstrated e.g. in the following article MVC in ASP.NET 2.0. Using an interface allows us to limit knowledge of view about controller in a controlled manner (see Controller paragraph).

Below is a simplified version of the admin.aspx.cs code behind view (located in located in /WebPages/Admin/admin.aspx.cs).

C#
using System;

using System.Web.UI.WebControls;
using MVC.Factories;
using NHibernate.MapClasses;
using System.Collections.Generic;
public partial class AdminClass : System.Web.UI.Page, MVC.Views.IGeneralView
{
    private MVC.Controllers.IAdminController _controller;   
        //stores controller instance
  
    public delegate IListListOfBooks_delegate(); 
        //delegate used to extract  method name

    public void Alert(string message)
    {
        notice.Text = message;
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (Session["user_id"] != null) //if you are logged in 
        {
            _controller = Factory.PointerToFactory.GetAdminController(this); 
        }
        else  // if you are not logged in
        {
            Response.Redirect(Utils.Utils.GetUrl("action=>login"));
        }
        BindControlToODS();  //bind control to ODS
    }

    private void BindControlToODS()
    {
        //We delete on PostBack therefore we need to refresh list of books
        //without url change

        ObjectDataSource1.TypeName = 
            _controller.GetType().FullName;  //get controller name
        ListOfBooks_delegate GiveMethodName = 
             new ListOfBooks_delegate(_controller.ListOfBooks);
        ObjectDataSource1.SelectMethod = GiveMethodName.Method.Name; 
             //get method name from delegate
        GridView1.DataSourceID = ObjectDataSource1.ID; //bind GV to ODS
    }

    protected void Page_PreRender(object sender, EventArgs e)
    {
        //setting navurls attributes for  Hyperlinks
        SetView();
    }

    protected void ObjectDataSource1_ObjectCreating(object sender, 
        ObjectDataSourceEventArgs e)
    {
        e.ObjectInstance = _controller; 
            //ODS calls now only controllers methods 
        
    }

    protected void GridView1_RowCommand(object sender, 
        GridViewCommandEventArgs e)
    {
    // GV specific even handler is coded here
    // as you can see we call here controllers methods    
        String cmd = e.CommandName;

        if (cmd == "destroy")  //item is deleted on a postback
        {
            try
            {
                int index = (Int32)Convert.ChangeType(e.CommandArgument, 
                    typeof(Int32));
                _controller.EraseRow(index);
                BindControlToODS(); // twice b/c viestate is disabled
            }

            catch (Exception)
            {
            }
        }
    } //RowCommand
    protected void GridView1_RowDataBound(object sender, 
        GridViewRowEventArgs e)
    {
        //setting NavUrl for edit and show links
        HyperLink EditLink;
        HyperLink ShowLink;
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            EditLink = (HyperLink)e.Row.FindControl("EditLink");
            ShowLink = (HyperLink)e.Row.FindControl("ShowLink");
            EditLink.NavigateUrl = 
                Utils.Utils.GetUrl(@"action=>admin,method=>edit" +
                ",id=>" + ((Product)(e.Row.DataItem)).Id);
            ShowLink.NavigateUrl = 
               Utils.Utils.GetUrl(@"action=>admin,method=>show" +
               ",id=>" +  ((Product)(e.Row.DataItem)).Id  );
        }
    }

    private void SetView()
    {
        // setting navurls for Sortby price and create new book links
        ContentPlaceHolder MasterPlaceH = 
            (ContentPlaceHolder)this.Master.FindControl("MainCol");
        HyperLink SortByPriceLink = 
            (HyperLink)MasterPlaceH.FindControl("SortByPriceLink");
        HyperLink CreateNewBookLink = 
            (HyperLink)MasterPlaceH.FindControl("CreateNewBookLink");
        SortByPriceLink.NavigateUrl = 
            Utils.Utils.GetUrl("action=>admin,method=>sort_by_price");
        CreateNewBookLink.NavigateUrl = 
            Utils.Utils.GetUrl("action=>admin,method=>create");
    }
}

Let's walk through the above code in a form of a bulleted list.

  • view implements the interface (MVC.View.IGeneralView) method "Alert" which controller uses to notify view
  • an instance of a controller is stored in the "_controller" field
  • the ListOfBooks_delegate delegate is used to extract the select method name for a given ObjectDataSource control. This way we can avoid hard coded strings and have a method name checked at a compile time
  • on Page's Load event
    • controller's instance is obtained from factory (view's instance is also passed to controller)
    • type of a controller is extracted (hard coded string is avoided)
    • the name of select method of ObjectDataSource control is extracted from a delegate (hard coded string is avoided)
    • the "Id" of ObjectDataSource control (hard coded string is avoided) is assigned to the GridView control
  • on Page's PreRender event we set the NavigateUrl attribute for HyperLink controls
  • on ObjectDataSource1's ObjectCreating event we expose controller's instance (stored in the "_controller" field) to the ObjectDataSource control
  • on GridView1's RowCommand event a particular record is deleted on PostBack. Controller's method is called directly to do so.
  • on GridView1's RowDataBound event we set the NavigateUrl attribute for embedded HyperLink controls

As can be seen events are either handled directly in the code behind when view manipulation is requested (GridView1's RowDataBound event) or handled by controller when Business Logic type manipulation is required (GridView1's RowCommand event).

A careful reader may also notice that only retrieving operation is handled by the ObjectDataSource control despite that we also have a delete operation present in the GridView1_RowCommand event handler. Throughout this web application the ObjectDataSource control handles only (R) retrieving operation while creation, updating and deleting (CUD) operations are handled directly (by calling controller's methods) in the appropriate, data-bound, control's event handlers (see more in Controller paragraph).

Ideally, one would call controller's method directly for all "CRUD" operations without relying on any data source controls (if you prefer to work in code behind from a declarative way). Among many advantages, one big disadvantage of the ObjectDataSource control is the fact that it uses reflections to call "CRUD" methods and to create custom data objects. If we need to handle high traffic sites, this control may not be the best solution (reflections calls are expensive). It is a known fact that ObjectDataSource control also imposes limits on the way a given system is designed. One approach might be to rewrite it (see e.g. ExtendedObjectDataSource control). I propose to stay away from it as much as you can. You may have noticed, however, that it is still used throughout this application. The reason is that data-bound controls make their dictionaries (during CUD operations) available only if they are bound to a data-source control!

A minimum requirement for the ObjectDataSource control to work is to specify at least select method (real method, not only a name). That is the way the ObjectDataSource control is still used in this web application until it will be eventually substituted.

Below is included an event handler method ODS_CheckOut_ItemUpdating of DetailsView control (can be found in WebPages/Store/checkout.aspx.cs).

C#
.........
protected void ODS_CheckOut_ItemUpdating(object sender, 
    DetailsViewUpdateEventArgs e)
    {
        DropDownList DrDl_PayTypeList = 
            (DropDownList)OrderForm.FindControl("PayTypeList");
        string selection = DrDl_PayTypeList.SelectedItem.ToString();
        
        e.NewValues.Add("Pay_Type", selection);

        List<cartmember /> SessionList =
                        (List<cartmember />)Session["cart"];

        // we pass by ref to be sure that shopping cart is 
        //passed by reference

        _controller.CreateOrder(e.NewValues, ref SessionList);

        e.Cancel = true;
       
    }
.......

As you can see a controller's method CreateOrder is called (new shopping cart entry is created) with one of the arguments being a dictionary provided by the DetailsView control.

Controller

When we talk about a controller in this article we mean a middle tier which sits between DAL and presentation layer (we do not mean controller from MVC architecture). It's instance is stored in the Factory (see details in the Factory paragraph) after it is created on the BeginRequest event during the routing process (see RoutingClass). Below is a graphic illustration of an interaction of controller between model and view.
Screenshot - Depot4.png

(controllers are located in App_Code/Controllers directory).

As can be seen from the above figure in this framework, controller interacts with model and view strictly through interfaces (see e.g. "MVC in ASP.NET 2.0" article). It means that our middle tier has a limited knowledge about other tiers and does not see complete classes. In order to perform the "CRUD" operations on data controller requests from model by calling it's interface methods (e.g. methods from the IBooksModel interface). It is worth saying in advance (see the Model paragraph) that the given model has only methods (no fields, properties etc.) and does not notify back controller about an update status (controller can infer that from the value returned by called method). On the other hand, controller interacts with view both ways. It implements interface (e.g IAdminController) methods which will be visible for view to call. After a given business logic operation is completed, middle tier notifies view by calling the Alert interface (i.e. IGeneralView) method implemented by view. As of now the Alert method displays only a text message. If the situation requires a more complex notification one can extend the IGeneralView interface or implement communication through events between controller and view. Below is the full source code of an exemplary Admin controller class (located in /App_Code/Controllers/AdminController.cs).

C#
using System;                       //neeeded for convert

using System.Collections.Generic;   //needed
using System.Collections;            //needed for IDictionary
using NHibernate.MapClasses;        //needed
using MVC.Models;                    // needed
using CreateUpdateDeleteMethods;
using Utils;                     //for ActionProperty attribute

namespace MVC.Controllers
{
    public class AdminController: IAdminController  
        //implements interface through which views have
    {   // acess to the controllers' methods
       
    //-------------View Section-----------------------
        /// <summary />
        /// refernce to the view, passed in on  creation
        /// </summary />
        private MVC.Views.IGeneralView _view; //controller can post a 
                                              // message to the view
        /// <summary />

        /// Property used to set,get a view
        /// </summary />
        public MVC.Views.IGeneralView View
        {
            get { return _view; }
            set { _view = value; }
        }
      //--------------------------Models Section---------------
         /// <summary />
         /// references to the IDataModels, retrived 
        /// on creation from the factory
        /// </summary />
        private MVC.Models.IBooksModel _dataBooksModel;      
            //controller has to know about data models
        private MVC.Models.IOrders _dataOrdersModel;
        
      //--------this section contains the data needed to 
      // create a view on GET/POST requests and-------
      //------- methods  which  are used to retrive data by 
      // ObjectDataSource control(ODS)--------------

        private Product _singlebook;
        private IList_listOfBooks ;
        private IList<order /> _listOfOrders;
        private IList<quantitytitle /> _listQuantityTitle;

        public Product SingleBook()  
        {
            return _singlebook;
        }
        public IListListOfBooks()   //select method for ObjectDataSource   
        {       //has to be a method bc. ODS does not play with properties 

            return _listOfBooks;
        }

        public IList<order /> ListOfOrders()  
                //select method for ObjectDataSource 
        {       //has to be a method bc. ODS does not play with properties

            return _listOfOrders;
        }

        public IList<quantitytitle /> ListQuantityTitle()  
              //select method for ObjectDataSource
        {     //has to be a method bc. ODS does not play with properties
            return _listQuantityTitle;
        }
        //----------------------------------------------------

        public AdminController()
                           // this constructor is used to create a controller
        {                  // in the module  at the beginning of the 
                           // reguest
            // During creation contoller retrives instances of the all models
            // from the factory 

            _dataBooksModel = MVC.Factories.Factory.GetBooksModel();
            _dataOrdersModel = MVC.Factories.Factory.GetOrdersModel();
        }
    //------These methods are called from the code behind on PostBack--------

        public void RemoveMarkedOrders(List<int> ListOfIds,string method)  
            //called on PostBack from ship.aspx.cs
        {
            int affrows = 0;
            affrows = _dataOrdersModel.RemoveOrders(ListOfIds);
            if (affrows != 0)
            {
                switch (method)
                {
                    case "ship_display_to_be_shipped":
                        {
                            _listOfOrders = 
                                _dataOrdersModel.FindAllToBeShipped(); 
                                //refresh data
                            _view.Alert("Orders were cancelled !");
                           
                            break;
                        }
                    case "ship_display_shipped":
                        {
                            _listOfOrders = 
                                _dataOrdersModel.FindAllShipped(); 
                                //refresh data
                            _view.Alert("Shipped Orders were removed");
                            break;
                        }
                }
            }
            else _view.Alert("Could not remove items");
        }

        public void ShowLineItems(int id)  
            //select mehod for ODS in  ship.aspx.cs
        {
            _listQuantityTitle = _dataOrdersModel.ShowLineItems(id);
        }

        public void ShipMarkedOrders(List<int> ListOfIds)                
            //called on PostBack from ship.aspx.cs
        {
            int affrows = 0;
            affrows = _dataOrdersModel.ShipOrders(ListOfIds);
            if (affrows != 0)
            {
                _listOfOrders = _dataOrdersModel.FindAllToBeShipped(); 
                    //refresh data
                _view.Alert("Books were shipped");
            }
            else _view.Alert("Could not ship Books!");
        }
     
        public void InsertBook(IDictionary values)    
            //insert method for ODS in create.aspx.cs
        {
            int affrows = 0;
            Product prod = new Product();
            values.Add("Id", 0);
            prod = (Product)CudMethod.BuildDataObject(prod, values);
            affrows = _dataBooksModel.InsertNewBook(prod);
            if (affrows != 0) _view.Alert("Item was inserted");
            else _view.Alert("Could not insert new entry");
        }

        public void UpdateSingleBook(IDictionary keys, 
            IDictionary oldvalues, IDictionary newvalues)  
        {
            //update method for ODS  in edit.aspx.aspx.cs
        
            Product prod_old = new Product();
            Product prod_new = new Product();
            oldvalues.Add("Id", keys["Id"]);
            newvalues.Add("Id", keys["Id"]);

            prod_old = 
               (Product)CudMethod.BuildDataObject(prod_old, oldvalues);
            prod_new = 
               (Product)CudMethod.BuildDataObject(prod_new, newvalues);

            int affrows = 0;
            affrows = _dataBooksModel.UpdateRowBooks(prod_old,prod_new);
            if (affrows != 0)
            {

                _view.Alert("Book was updated");
            }
            else
            {
                _view.Alert("Could not update this Book");
            }
           // _singlebook = _dataBooksModel.ShowByID(Props.Id);  
           // data is not refreshed we use viewstate
        }

        public void EraseRow(int id)       
        ///called on PostBack from  admin.aspx.cs
        {
            int affrows = 0;
            affrows = _dataBooksModel.DeleteRow(id);

            if (affrows != 0)
            {
                _listOfBooks = _dataBooksModel.FindAllBooks();        
                //repopulate right after it is deleted
                _view.Alert(" Just  erased a row");
            }
            else _view.Alert("Could not delete a row");
        }

       //----------------End of Methods---------------------------

        #region IController Members
        #endregion

        //----this part determines the view which will be served
        //-----and sets the data on GET
        // On POST if a viewstate is not used we data can be set too 
        // e.g. "admin"

        // All actions listed below are bookmarkable

        [ActionProperty]
        public string admin  //displays data 
        {
            get
            {
                return "admin";
            }
            set //needed on POST b/c viewstate is disabled
            {
                _listOfBooks = _dataBooksModel.FindAllBooks();  //set data
            }
        }

        [ActionProperty]
        public  string show  //displays data
        {
            get
            {
                return "show";
            }
            
            set
            {
                if (Utils.Utils.IsGetRequest)
                {
                    //we use show/id only on GET
                    int id = 0;
                    //id = 
                    //(int)Convert.ChangeType(
                    //    HttpContext.Current.Items["id"],
                    //    typeof(int));
                    id = Utils.Utils.GetInfoFromUrl<int>("id");
                        
                    if (id != 0)
                    {                       
                       _singlebook =_dataBooksModel.ShowBookByID(id);
                    }
                } //if
            }
        }

        [ActionProperty]
        public string edit  //displays and gathers
        {
            get
            {
                return "edit"; ;
            }

            set
            {
                //on post we want viewstate to take care of things
                if (Utils.Utils.IsGetRequest)
                {
                    int id = 0;
                   // id = (int)Convert.ChangeType(
                   //     HttpContext.Current.Items["id"], typeof(int));
                    id = Utils.Utils.GetInfoFromUrl<int>("id");
                    if (id != 0)
                    {
                       _singlebook =_dataBooksModel.ShowBookByID(id);
                    }
                }
            }
        }

        [ActionProperty]
        public string sort_by_price   //dipslays data
        {
            get
            {
                return "admin";
            }
            set
            {
                if (Utils.Utils.IsGetRequest)
                {
                    //we use sort_by Price  only on GET   
                    _listOfBooks = _dataBooksModel.ShowBooksSortedByPrice();
                    MVC.Factories.Factory.PointerToFactory.Message = 
                        "Books are now sorted by Price";
                } //if
            }
        }

        [ActionProperty]
        public string create   //gathers data
        {
            get { return "create"; }
            set
            {
                if (Utils.Utils.IsGetRequest)
                {
                    //a faked empty  Product
                    _singlebook = new Product();
                }
                //under normal circumstnces setter should be empty, 
                // we create a new entry--no need for data here
                // on POST we  use POSTBACK feature,which does not work 
                // for detailsview in
                // an insert mode
                // therefore we need to work in an edit mode --works !
                // detailsview needs a faked empty  Product to 
                // display form--otherwrise it displays nil!
            }
        }

        [ActionProperty]
        public string ship_display_to_be_shipped             
        {
            get 
            {
                return "ship"; 
            }
            set
            {    //On Post we ship and delete items therefore we need 
                // this on POST  to refresh
                _listOfOrders = _dataOrdersModel.FindAllToBeShipped(); 
                    //set _listoforders
            }
        }

        [ActionProperty]
        public  string ship_display_shipped    
        {
            get
            {
                return "ship";
            }
            set
            {   //On Post we  delete items therefore we need  
                //this to refresh
                _listOfOrders = _dataOrdersModel.FindAllShipped();  
                //set inital data
            }
               
        }
    }//class
}

Let's walk through the above code in the form of paragraphs. Each paragraph corresponds to the code section of controller responsible for a different type of functionality. Paragraphs will be listed chronologically following an order in which each section is accessed during a page life cycle.

Constructor

Constructor is the first one (e.g AdminController) accessed in controller and is obviously used to instantiate it. This is done on the BeginRequest event in RewriteUrlClass class. The body of the contructor takes care of gathering information about models from the Factory. Information about DAL is stored in private interface fields (e.g. _dataBooksModel, _dataOrdersModel) grouped for organizational purposes as a separate section.

Action Properties

The next chronologically accessed section is a fragment of code where public properties decorated with the [ActionProperty] attribute are located. These properties are accessed at the run-time on BeginRequest and PostAcquireRequestState events in the RewriteUrlClass class. The reason that they are decorated with an attribute is for both security and efficiency. Efficiency means that only properties decorated with a given attribute are retrieved so resources are not wasted to obtain unwanted ones. Security aspect was also taken into account here. At run-time the properties of controller are accessed based on the URL. This poses a potential danger that a malicious user can access an arbitrary property of controller. This was eliminated by decorating properties accessed at a run-time with the [ActionProperty] attribute.

The role of these properties is two-fold. First, they return (getter) a name of view to be rendered and provide this information to the routing portion of the RewriteUrlClass (in the InitalizeController method on the BeginRequest event). This provides a very flexible way of determining view where a given URL can be mapped to an arbitrary ASPX file. A careful reader may have noticed that nowhere does controller contain statements such as RenderView, RedirectTo nor does it have reference to the System.Web namespace. The role of controller as the one who renders view was eliminated purposefully after careful studies of problems with such an approach attempted by the MonoRail team. Although the author of this article believes that it can be accomplished, it would require modifying the default page-life cycle event handlers. In the author's opinion the same was achieved in this framework without a need to change anything in the ASP.NET 2.0 run-time.

Screenshot - Depot5.png

The meaning of square brackets is that given operation can be optional. Note the lack of square brackets for the getter.

A Second role of a property decorated with the [ActionProperty] attribute is to pre-fetch data and carry any necessary URL verification and logic. This is done in a "setter" in the PrefetchData method in the PostAcquireRequstedState event handler of a RewriteUrlClass class. The data is fetched by calling model's methods and stored in private fields (i.e. _singlebook, _listOfBooks etc.). These private fields are accessed by corresponding (i.e. Singlebook, ListOfBooks etc.) with public methods which return a value. Ideally one would use public properties to access private fields but the ObjectDataSource control calls only methods using reflections (those public methods are called by the ObjectDataSource control from view). This is an example when a particular ASP.NET control limits design freedom (the ObjectDataSource control will be substituted in the future). Data is fetched and made available to view on both GET and POST requests. If a given view does not need data to render itself (e.g. login page) a setter is simply empty. Beside fetching data on the GET request we can also display a message on view's page about the status of an update (e.g. sort_by_price setter). Fetching data on the POST request depends whether a particular view uses VIEWSTATE to render itself. If we decide to disable VIEWSTATE (to shrink web page) then we have to fetch data on the POST request, otherwise controls would not be able to render themselves. With VIEWSTATE turned on there is obviously no need to do what can be seen in code.

View Property

The first handled event inside view is Page's Load event where, among other things, view passes it's instance to controller (see e.g. admin.aspx.cs code behind). The GetAdminController method is called whose argument is a view's instance. This method is located in the Factory. Besides retrieving an instance of controller it populates controller's View property (this is the 'IGeneralView' interface property) with an instance of view. This is how controller can communicate with view. As of now this communication is simply displaying a message but if should the situation require it, this aspect can be enhanced.

PostBack Methods

After Page's Load event several other events either belonging to the Page or Control classes are handled inside typical view. Inside these event handlers there are called controller's methods exposed by it's interface (e.g. IAdminController). From now on we will call these methods PostBack methods as they are called on POST request. We can see these methods grouped together and declared as public void (e.g. in AdminController we have RemoveMarkedOrders, ShowLineItems, ShipMarkedOrders etc.). Generally, PostBack methods perform Creation, Update, Delete (CUD) operations and notify view by calling it's interface Alert method. Below is a flowchart of structure of a typical postback method performing CUD operation.

Screenshot - Depot6.png

Fig. Flowchart of structure of so-called postback methods of controller. The Meaning of square brackets is that a given operation can be optional.

After a CUD operation is carried out by the postback method (e.g. RemoveMarkedOrders method in AdminController class) view is either notified about status of operation immediately (if VIEWSTATE is ON) or after data is refreshed (if VIEWSTATE is disabled). Controller can determine success or failure of a CUD operation from a value it returns and sends a message to view. The author feels that this mechanism is pretty sufficient. If ones feels a need to enhance it can be accomplished in several ways mentioned in earlier paragraphs. As careful reader probably recalls retrieving operation "R" is handled by Action properties. An exception from that can be ShowLineItems(int id) method which is called (in ship.aspx.cs code behind ) to populate nested GridView control. This cannot be handled only by Action properties. Generally speaking postback methods handle only CUD operations but as reader can see there might some exceptions when situations demands it.

What is worth pointing out here is the fact that controller does not pass raw form's data to model but builds data objects first from dictionaries obtained from view. This normally can be done by the ObjectDataSource control but we decided to handle it independently for reasons mentioned earlier. An example can be UpdateSingleBook method where two data objects are built first (from data of a form before update and after) by CudMethod.BuildDataObject method from CreateUpdateDeleteMethods namespace. This method does exactly what ObjectDataSource's original method does but this time we have more flexibility. We handle concurrency issues originally handled by the ObjectDataSource control independently as well. A careful reader may have noticed that we pass two data objects when calling the UpdateSingleBook method. Model knowing two data objects (handled by NHibernate) can handle concurrency issues easily (see more in Model paragraph).

Model

Finally, we arrive at the last tier of our framework - model. As it was mentioned earlier for the CRUD operations carried in model we use a powerful O/R mapper NHibernate. The reason that we use O/R mapper can be explained by the quote from the book NHibernate in Action (p.12) by Pierre Henri Kuaté, Christian Bauer and Gavin King :

NHibernate is capable of loading and saving interconnected objects and maintain their relationships.

The reason that the author of this article chose NHibernate is because it is a port of the very well proven "Hibernate" O/R mapper from the Java world.

Below is the figure depicting an interaction of controller with model through interfaces.

Screenshot - Depot7.png

Similarly, as controller is seen by view through an interface, a given model is seen by controller through an interface as well. As we recall from the paragraph about controller, middle-tier gets model's instance in it's constructor (e.g see AdminController's constructor).

Below is included a source code of the BooksDB class which is accessed through the IBooksModel interface in the AdminController class (is located in App_Code/Model/Crud/BooksDB.cs).

C#
using System;

using System.Collections.Generic;

using NHibernate;
using NHibernate.MapClasses;
using CreateUpdateDeleteMethods;
using Routing.Module;
using NHibernate.Mapping;
/// <summary />
/// Summary description for BooksDB
/// </summary />
/// 
namespace MVC.Models
{
    public class BooksDB: IBooksModel
    {
        public BooksDB()
        {
        }
        //-------------------------------------------------
      
        public int InsertNewBook(Product Props)
        {
            ISession session = RoutingClass.NHibernateCurrentSession;
            using (session.BeginTransaction())
            {
                try
                {
                    session.Save(Props);
                    session.Transaction.Commit();
                    return 1;
                }
                catch (TransactionException) { return 0; }
            }
        }

        public int UpdateRowBooks(Product prod_old, Product prod_new)
        {
            //this version has concurrency check

            int result = -10;
            ISession session = RoutingClass.NHibernateCurrentSession;
            using (session.BeginTransaction())
            {
                Product retrived_product = session.Get(prod_old.Id);
                session.Evict(retrived_product);  
                //detach retrived_product from session
                // LockMode show = 
                //    session.GetCurrentLockMode(retrived_product); 
                // lock mode is read

                if (CudMethod.ComparePropertiesOfObjects(
                    prod_old, retrived_product))
                {
                    try
                    {

                        session.Update(prod_new);
                            //attach prod_new with the session 
                        session.Transaction.Commit();
                            //and schedule to update
                        result = 1;
                    }
                    catch (TransactionException) { result = 0; }
                }
                else result = 0;
            }
            return result;
        }

        public int DeleteRow(int Id_In)
        {
            //changed, deleting using query without object retrival

            ISession session = RoutingClass.NHibernateCurrentSession;

            using (session.BeginTransaction())
            {
                try
                {
                    Int32 Id_struct_type = Id_In;  //has to be struct type
                    session.Delete("from Product prod where prod.Id=?", 
                        Id_struct_type,
                        NHibernateUtil.Int32);
                    session.Transaction.Commit();
                    return 1;
                }
                catch (TransactionException) { return 0; }
            }
        }

        public IListShowBooksSortedByPrice()
        {
            ISession session = RoutingClass.NHibernateCurrentSession;

            IListlist;

            using (session.BeginTransaction())
            {
                IQuery query = session.CreateQuery(
                    "FROM Product p order by p.Price asc");
                list = query.List();
                session.Transaction.Commit();
            }
            return list;
        }

        public  Product ShowBookByID(int id)
        {
            ISession session = RoutingClass.NHibernateCurrentSession;
            Product product;
            using (session.BeginTransaction())
            {
                product = session.Get(id);
                session.Transaction.Commit();
            }
            return product;
        }

        public  IListFindAllBooks()
        {
            ISession session = RoutingClass.NHibernateCurrentSession;
            IListlist;
            using (session.BeginTransaction())
            {
                IQuery query = session.CreateQuery("FROM Product");
                list = query.List();
                session.Transaction.Commit();
            }
            return list;
        }
    }
}

A careful reader may notice from the above source that model has only methods which either return a number (in case of CUD methods), a list of objects or single data object (in case of R methods). A controller can determine if a CUD operation was successful from the number of affected rows it returns. In case of retrieval an R not null result is considered a success.

The absence of properties or fields in model's body creates an opportunity of caching its instance. This way a DAL can be instantiated only once by the first HTTP request and be available for every other request (this is explained in Factory paragraph).

A closer look at any of methods of model reveals that it typically contains the following code:

C#
.....
 ISession session = RoutingClass.NHibernateCurrentSession;

          ............ 
            using (session.BeginTransaction())
            {  .............
                session.Transaction.Commit();
            }
......

In order for NHibernate to function one needs the so-called NHibernate session (of the interface type ISession). As we recall from the "Page life cycle" paragraph this session is created in the RoutingClass class on the BeginRequest event. It is stored in the HttpContext.Current.Items dictionary which guarantees it uniqueness (one session per request). As can be seen from the code above DAL accesses NHibernate session through static NHibernateCurrentSession getter. On EndRequest the session is closed and resources freed.

Changes to the data objects made in the scope of transaction (see line starting with using (session.BeginTransaction())... ) are not immediately propagated to the database. NHibernate writes SQL to database only after a transaction is committed (see line starting with session.Transaction.Commit(); ). This allows NHibernate to minimize database requests. For more information about NHibernate the author recommends nhibernate.org website and also the book "NHibernate in Action" by Pierre Henri Kuaté, Christian Bauer and Gavin King.

One more thing before we go to the next subparagraph to notice is that in the UpdateRowBooks method of the BooksDD class we perform conflict detection to void updating or deleting a row if the values stored in the table are not the same that those we read (we decided to handle it directly instead of relying on the ObjectDataSource control). This happens when two different users are editing the same row. When one of them updates it, the second update without checking values originally read overrides the first update.

There are many functions that can be handled by NHibernate e.g. paging, filtering, conflict detection, caching etc. This tells us that we do not have to rely on any ASP.NET controls to achieve what O/R mapper already provides (ObjectDataSource present in this framework will be eventually subsituted).

Mapping Classes

One of the reasons one uses the O/R mapper is that CRUD operations can be carried using data objects. This can be noticed in the BooksDB class listed in the previous subparagraph. Before we explain a structure of mapping classes used in this project let's look first at our database. In Our Depot store (see Structure of database tables figure) we sell books (it corresponds to the products table). A client places an order (it corresponds to the orders table) which can contain many books (it corresponds to the line_items table). To maintain one-to-many relationship between orders and line_items, one-to-one relationship between line_items and products the line_items table has two foreign keys: order_id, product_id. The users table has no relations and is used to store users with administrative privileges. If one would use ADO.NET to manage our store it will soon find out that it becomes tedious to perform CRUD operations on related database tables. An additional drawback is that a programmer has to manipulate tables (instead of objects) by writing SQL.

Screenshot - Depot8.png

Screenshot - Depot9.png

When using O/R mapper we deal with data objects. As can be seen (from Mapping classes figure) our database tables are mapped into four classes (one class per table). Moreover, the structure of classes closely resembles the structure of database tables: capitalized names of classes correspond to names of database tables (plus some small modifications), capitalized names of properties correspond to the names of database columns. The main noticeable difference is how the information about relations between data objects is stored inside classes.

In the LineItem class we keep track of Products by storing its Id in the Product_Id integer field (one-to-one association). This field has to be set manually in a code without any help of O/R mapper. A given Product as can be seen has no information about other objects (author feels that there is no need). Relation between LineItem and Order classes is more complicated. A given instance of the Order class keeps track of all the instances of LineItem instances in the NHibernate's own interface collection ISet(one-to-many association). The LineItem object is aware of which instance of the Order class it belongs to by storing its instance in the Order property (many-to-one association). When creating new Order and LineItem one has to call the AddLineItem(LineItem lineitem) method from Order class (download source file Orders.cs) to populate both Order and LineItems properties accordingly. The User class has no relations with the rest of classes.

Mapping Files

ORM tools require metadata to specify the mapping between classes and tables, properties and columns, associations and foreign keys,.NET types and SQL types. There are two different ways to do this: attributes and XML mapping files (the preferred way here are mapping files). Below is included Orders.hbm.xml mapping file (mapping files are located in /App_Data/Resources/Orders.hbm.xml).

XML
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" 
    namespace="NHibernate.MapClasses" assembly="App_Code">
  <class   name="Order"   table="orders" >
    <id  name="Id" column="id" type="int" >
      <generator   class="native"  />

    </id>
    <property name="Name"   type="string"  not-null="true"  />
    <property name="Email"  type="string"  not-null="true"/>
    <property name="Address" type="string"  not-null="true"/>
    <property  name="Pay_Type" type="string"  not-null="true" />
    <property  name="Shipped_At" type="DateTime"  not-null="false"/>

    <set name="LineItems"   inverse="true" cascade="all-delete-orphan">
      <key column="order_id" />
      <one-to-many class="LineItem" />
    </set>
   </class>
  </hibernate-mapping>

We briefly summarize the functionality of the above file in the form of a bulleted list. By no means this a comprehensive description (for details see e.g. "NHibernate in Action").

  • second line specifies name of a namespace NHibernate.MapClasses to which mapping classes belong to as well as the name of assembly they are located
  • third line specifies mapping between a mapping class and the corresponding database table
  • in id tag details about a primary key are specified
  • in property tag mapping between properties of a class and corresponding database columns is defined
  • in set tag one-to-many association is detailed

Factory

Factory is a word that has been mentioned in this article many times. In this version of proposed web framework Factory is an essential ingredient without which 3-tiers cannot function properly. Simply speaking, this is a place where an information about controller and model is stored. As we recall from previous paragraphs view needs to know controller instance and controller on the other hand needs to know model. Below is the figure depicting interaction of individual tiers with the Factory as well as it's source code.

Screenshot - Depot10.png

Fig. Individual tiers accessing Factory. In RoutingClass controller is created (also accessed) and stored in the Factory.

View retrieves an instance of controller from Factory. Controller gets an instance of model from Factory.

Below is included the source code of the Factory class (located in App_Code/Factories/Factory.cs).

C#
using System;
using MVC.Controllers;
using Utils;                  // for ControllerInstanceProperty attribute

namespace MVC.Factories  
{
    public class Factory
    {

    public static Factory PointerToFactory    
        {
            get
            { 
                return HttpContext.Current.Items[
                    "PointerToFactory"] as Factory;
            }
            set 
            { 
                HttpContext.Current.Items["PointerToFactory"]= value; 
            }
        }

        public Factory()  //constructor
        {
        }
        
        public string Message = "";  
            //stores a message posted by controller on GET request

        //------------------Instances of controllers-----------------

        public IAdminController _adminController;  
            //will store an instance of controller 
        [ControllerInstanceProperty]
        public object AdminController
        {

            set { _adminController = (IAdminController)value; } // a trick
            get { return _adminController; }
        }

        public IStoreController _storeController; 
            //will store an instance of controller
        [ControllerInstanceProperty]
        public object StoreController
        {

            set { _storeController = (IStoreController)value; } // a trick
            get { return _storeController; }
        }

        public ILoginController _loginController;  
            //will store an instance of controller
        [ControllerInstanceProperty]
        public object LoginController
        {

            set { _loginController = (ILoginController)value; } // a trick
            get { return _loginController; }
        }

        public  MVC.Controllers.IAdminController GetAdminController(
            MVC.Views.IGeneralView view) 
        {
            _adminController.View = view;
            return _adminController;
           
        }

        public  MVC.Controllers.IStoreController GetStoreController(
            MVC.Views.IGeneralView view)
        {
            _storeController.View = view;
            return _storeController;
        }

        public  MVC.Controllers.ILoginController GetLoginController(
            MVC.Views.IGeneralView view) 
        {
            _loginController.View = view;
            return _loginController;
        }
    
        //-----------------Models data-------------------------------
        
        private static MVC.Models.IBooksModel _modelBooks;
        private static MVC.Models.IOrders _modelOrders;
        private static MVC.Models.IBooksForSale _modelBooksForSale;
        private static MVC.Models.ILoginModel _modelLoginUsers;
        public static MVC.Models.IBooksModel GetBooksModel()
        {
            if (_modelBooks == null)
            {
                _modelBooks = new MVC.Models.BooksDB();
            }
            return _modelBooks;
        }
        public static MVC.Models.IOrders GetOrdersModel()
        {
            if (_modelOrders == null)
            {
                _modelOrders = new MVC.Models.OrdersToShip();
            }
            return _modelOrders;
        }
        public static MVC.Models.IBooksForSale GetBooksForSaleModel()
        {
            if (_modelBooksForSale == null)
            {
                _modelBooksForSale = new MVC.Models.BooksForSaleDB();
            }
            return _modelBooksForSale;
        }
        public static MVC.Models.ILoginModel GetLoginUsersModel()
        {
            if (_modelLoginUsers == null)
            {
                _modelLoginUsers = new MVC.Models.LoginUsers();
            }
            return _modelLoginUsers;
        }
    }
}

As seen from the above code, the Factory class is an instance class. Every request creates its own instance. The reason behind it is that every request needs a unique controller. We could accomplish thread-safety by storing controller's instance in a static field decorated with the [ThreadStatic] attribute. What prohibits us from choosing this solution is the ambiguity of using a [ThreadStatic] attribute is ASP.NET (see e.g. forum discussion). Once Factory is created it's instance is stored in HttpContext.Current.Items dictionary and is retrieved/stored through PointerToFactory static property (for convenience).

Factory stores an instance of each controller in "_controllername"+"Controller" instance field with it's name derived (it is very important) from the controller's name. This instance field then is retrieved/populated through public properties named also after controller's name i.e. ControllerName+Controller. Once again for safety and efficiency these properties are decorated with the [ControllerInstanceProperty] attribute (see explanation of the [ActionProperty] attribute in Controller paragraph).

Models on the other hand can be safely stored in static fields (e.g _modelBooks, _modelOrders etc). Because model does not contain properties or fields this opens a possibility of caching it's single instance. That is, each request does not have to create a given model from scratch. It is most likely already there created by previous request. Static methods are used to retrieve instances of model from Factory e.g GetBooksModel, GetOrdersModel etc. Because the static fields storing instances of models are not accessed at run-time there is no need to use any specific naming convention here.

Conclusions

In this article the author presented an alternative to "Model-View-Presenter" and "Model-View-Controller" patterns used usually in a web application by implementing a multi-tier architecture. All the communication between individual tiers is accomplished through interfaces (for code re-usability) by calling methods instead of an implicit communication based on events and events listeners. The proposed here mini web framework has all the elements to be fully functional and easily customizable solution for the enterprise.

History

  • 30 August, 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