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.
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).
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();
}
private void context_PostAcquireRequestState(object sender,
EventArgs e)
{
.....
PrefetchData();
.....
CurrentPage.PreRender += new EventHandler(Page_PreRender);
......
}
private void Page_PreRender(object sender, EventArgs e)
{
MVC.Views.IGeneralView InvokeMethod =
sender as MVC.Views.IGeneralView;
.....
InvokeMethod.Alert(
MVC.Factories.Factory.PointerToFactory.Message);
....
}
}
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;
private static ISessionFactory getFactory()
{
........
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).
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
"/(<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 |
URL | Action |
/admin.aspx | go to administrative page |
/admin/create.aspx | create new product within Admin controller |
/admin/edit/1.aspx | edit the book with id=1 within Admin controller |
/admin/show/1.aspx | ship the book with id=1 within Admin controller |
/admin/sort_by_price.aspx | sort books on the administrative page (by price) within Admin controller |
/admin/ship_display_to_be_shipped.aspx | display ordered books ready to be shipped within Admin controller |
/admin/ship_display_shipped.aspx | display books which where shipped within Admin controler |
Store controller |
URL | Action |
/store.aspx | go to main store.aspx page |
/store/display_cart.aspx | display cart within store controller |
/store/checkout.aspx | display checkout page within store controller |
Login controller |
URL | Action |
/login.aspx | go to login page |
/login/add_user.aspx | display add user page within Login controller |
/login/list_users.aspx | display 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).
using System;
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Text.RegularExpressions;
using System.ComponentModel;
using MVC.Factories;
public class RewriteUrlClass
{
private String WhichController;
private String ViewToRender = null;
private Type TypeOfController;
private PropertyDescriptor PropertyGet;
public RewriteUrlClass()
{
}
public void RewriteToCustomUrl()
{
String MatchedMethod;
String Url;
PropertyDescriptorCollection PropsOfController;
Url = HttpContext.Current.Request.Path;
.....
String ReqUrlVar = Utils.Utils.ReqUrlVar;
Regex ReqUrlVarRegex = new Regex(ReqUrlVar,
RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
Match UrlMatched = ReqUrlVarRegex.Match(Url.ToLower());
if (UrlMatched.Success)
{
String[] GetGrNames = ReqUrlVarRegex.GetGroupNames();
try
{
if (UrlMatched.Groups["method"].Value != "")
{
WhichController =
Utils.Utils.UCFLetter(
UrlMatched.Groups["action"].Value);
......
MatchedMethod = UrlMatched.Groups["method"].Value;
PropertyGet = PropsOfController[MatchedMethod];
}
else
{
if (UrlMatched.Groups["action"].Value == "index")
{
WhichController =
Utils.Utils.UCFLetter(Utils.Utils.Controller);
......
PropertyGet =
PropsOfController[Utils.Utils.Controller];
HttpContext.Current.Items["action"] =
WhichController.ToLower();
HttpContext.Current.Items["method"] =
Utils.Utils.Method;
}
else
{
WhichController =
Utils.Utils.UCFLetter(
UrlMatched.Groups["action"].Value);
......
PropertyGet =
PropsOfController[WhichController.ToLower()];
}
}
if (UrlMatched.Groups["action"].Value != "index")
foreach (String SingleGroup in GetGrNames)
{
HttpContext.Current.Items[SingleGroup] =
(String)UrlMatched.Groups[SingleGroup].Value;
}
InitalizeController();
HttpContext.Current.RewritePath("~/WebPages/" +
WhichController + "/" +
ViewToRender + ".aspx", false);
}
catch (Exception)
{
HttpContext.Current.Response.Write(
"Am error occured while routing!");
}
}
else
{
HttpContext.Current.RewritePath("~/index.aspx", false);
}
}
private void InitalizeController()
{
Factory FactoryInstance;
Type TypeOfFactory;
PropertyDescriptorCollection PropsOfFactory;
object o = Activator.CreateInstance(TypeOfController);
ViewToRender = PropertyGet.GetValue(o).ToString();
FactoryInstance = new Factory();
Factory.PointerToFactory = FactoryInstance;
TypeOfFactory = Type.GetType("MVC.Factories.Factory");
PropsOfFactory =
TypeDescriptor.GetProperties(TypeOfFactory,
Utils.Utils.ControllerInstanceProperties);
PropertyGet = PropsOfFactory[WhichController + "Controller"];
PropertyGet.SetValue(FactoryInstance, o);
}
}
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:
"/(?<DIR1>\w+)?(/)?(?<DIR2>\w+)?(/)?(?<DIR3>\w+)?(/)?(?<DIR4>\w+)?"
, where DIR1
, DIR2
... are named groups.
URL | Action |
/home/home.aspx | go to home.aspx page located in ~/.../home directory |
/generalinfo/breathing.aspx | go to breathing.aspx page located in ~/.../generalinfo directory |
/diseases/pulmonary/asthma.aspx | go 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 ?
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).
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;
public delegate IListListOfBooks_delegate();
public void Alert(string message)
{
notice.Text = message;
}
protected void Page_Load(object sender, EventArgs e)
{
if (Session["user_id"] != null)
{
_controller = Factory.PointerToFactory.GetAdminController(this);
}
else
{
Response.Redirect(Utils.Utils.GetUrl("action=>login"));
}
BindControlToODS();
}
private void BindControlToODS()
{
ObjectDataSource1.TypeName =
_controller.GetType().FullName;
ListOfBooks_delegate GiveMethodName =
new ListOfBooks_delegate(_controller.ListOfBooks);
ObjectDataSource1.SelectMethod = GiveMethodName.Method.Name;
GridView1.DataSourceID = ObjectDataSource1.ID;
}
protected void Page_PreRender(object sender, EventArgs e)
{
SetView();
}
protected void ObjectDataSource1_ObjectCreating(object sender,
ObjectDataSourceEventArgs e)
{
e.ObjectInstance = _controller;
}
protected void GridView1_RowCommand(object sender,
GridViewCommandEventArgs e)
{
String cmd = e.CommandName;
if (cmd == "destroy")
{
try
{
int index = (Int32)Convert.ChangeType(e.CommandArgument,
typeof(Int32));
_controller.EraseRow(index);
BindControlToODS();
}
catch (Exception)
{
}
}
}
protected void GridView1_RowDataBound(object sender,
GridViewRowEventArgs e)
{
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()
{
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).
.........
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"];
_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.
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.
(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).
using System;
using System.Collections.Generic;
using System.Collections;
using NHibernate.MapClasses;
using MVC.Models;
using CreateUpdateDeleteMethods;
using Utils;
namespace MVC.Controllers
{
public class AdminController: IAdminController
{
private MVC.Views.IGeneralView _view;
public MVC.Views.IGeneralView View
{
get { return _view; }
set { _view = value; }
}
private MVC.Models.IBooksModel _dataBooksModel;
private MVC.Models.IOrders _dataOrdersModel;
private Product _singlebook;
private IList_listOfBooks ;
private IList<order /> _listOfOrders;
private IList<quantitytitle /> _listQuantityTitle;
public Product SingleBook()
{
return _singlebook;
}
public IListListOfBooks()
{
return _listOfBooks;
}
public IList<order /> ListOfOrders()
{
return _listOfOrders;
}
public IList<quantitytitle /> ListQuantityTitle()
{
return _listQuantityTitle;
}
public AdminController()
{
_dataBooksModel = MVC.Factories.Factory.GetBooksModel();
_dataOrdersModel = MVC.Factories.Factory.GetOrdersModel();
}
public void RemoveMarkedOrders(List<int> ListOfIds,string method)
{
int affrows = 0;
affrows = _dataOrdersModel.RemoveOrders(ListOfIds);
if (affrows != 0)
{
switch (method)
{
case "ship_display_to_be_shipped":
{
_listOfOrders =
_dataOrdersModel.FindAllToBeShipped();
_view.Alert("Orders were cancelled !");
break;
}
case "ship_display_shipped":
{
_listOfOrders =
_dataOrdersModel.FindAllShipped();
_view.Alert("Shipped Orders were removed");
break;
}
}
}
else _view.Alert("Could not remove items");
}
public void ShowLineItems(int id)
{
_listQuantityTitle = _dataOrdersModel.ShowLineItems(id);
}
public void ShipMarkedOrders(List<int> ListOfIds)
{
int affrows = 0;
affrows = _dataOrdersModel.ShipOrders(ListOfIds);
if (affrows != 0)
{
_listOfOrders = _dataOrdersModel.FindAllToBeShipped();
_view.Alert("Books were shipped");
}
else _view.Alert("Could not ship Books!");
}
public void InsertBook(IDictionary values)
{
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)
{
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");
}
}
public void EraseRow(int id)
{
int affrows = 0;
affrows = _dataBooksModel.DeleteRow(id);
if (affrows != 0)
{
_listOfBooks = _dataBooksModel.FindAllBooks();
_view.Alert(" Just erased a row");
}
else _view.Alert("Could not delete a row");
}
#region IController Members
#endregion
[ActionProperty]
public string admin
{
get
{
return "admin";
}
set
{
_listOfBooks = _dataBooksModel.FindAllBooks();
}
}
[ActionProperty]
public string show
{
get
{
return "show";
}
set
{
if (Utils.Utils.IsGetRequest)
{
int id = 0;
id = Utils.Utils.GetInfoFromUrl<int>("id");
if (id != 0)
{
_singlebook =_dataBooksModel.ShowBookByID(id);
}
}
}
}
[ActionProperty]
public string edit
{
get
{
return "edit"; ;
}
set
{
if (Utils.Utils.IsGetRequest)
{
int id = 0;
id = Utils.Utils.GetInfoFromUrl<int>("id");
if (id != 0)
{
_singlebook =_dataBooksModel.ShowBookByID(id);
}
}
}
}
[ActionProperty]
public string sort_by_price
{
get
{
return "admin";
}
set
{
if (Utils.Utils.IsGetRequest)
{
_listOfBooks = _dataBooksModel.ShowBooksSortedByPrice();
MVC.Factories.Factory.PointerToFactory.Message =
"Books are now sorted by Price";
}
}
}
[ActionProperty]
public string create
{
get { return "create"; }
set
{
if (Utils.Utils.IsGetRequest)
{
_singlebook = new Product();
}
}
}
[ActionProperty]
public string ship_display_to_be_shipped
{
get
{
return "ship";
}
set
{
_listOfOrders = _dataOrdersModel.FindAllToBeShipped();
}
}
[ActionProperty]
public string ship_display_shipped
{
get
{
return "ship";
}
set
{
_listOfOrders = _dataOrdersModel.FindAllShipped();
}
}
}
}
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 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.
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.
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.
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).
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.
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).
using System;
using System.Collections.Generic;
using NHibernate;
using NHibernate.MapClasses;
using CreateUpdateDeleteMethods;
using Routing.Module;
using NHibernate.Mapping;
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)
{
int result = -10;
ISession session = RoutingClass.NHibernateCurrentSession;
using (session.BeginTransaction())
{
Product retrived_product = session.Get(prod_old.Id);
session.Evict(retrived_product);
if (CudMethod.ComparePropertiesOfObjects(
prod_old, retrived_product))
{
try
{
session.Update(prod_new);
session.Transaction.Commit();
result = 1;
}
catch (TransactionException) { result = 0; }
}
else result = 0;
}
return result;
}
public int DeleteRow(int Id_In)
{
ISession session = RoutingClass.NHibernateCurrentSession;
using (session.BeginTransaction())
{
try
{
Int32 Id_struct_type = Id_In;
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:
.....
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.
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).
="1.0"="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 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.
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).
using System;
using MVC.Controllers;
using Utils;
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()
{
}
public string Message = "";
public IAdminController _adminController;
[ControllerInstanceProperty]
public object AdminController
{
set { _adminController = (IAdminController)value; }
get { return _adminController; }
}
public IStoreController _storeController;
[ControllerInstanceProperty]
public object StoreController
{
set { _storeController = (IStoreController)value; }
get { return _storeController; }
}
public ILoginController _loginController;
[ControllerInstanceProperty]
public object LoginController
{
set { _loginController = (ILoginController)value; }
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;
}
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