Introduction
I've decided to write this article for two reasons: first, my enthusiasm with the Monorail framework, and second, the .NET community has displayed a growing interest on MVC as an alternative to traditional WebForms applications, not only because of Monorail, but also due to the rise of the new ASP.NET MVC framework.
The MVC Pattern
MVC is the acronym for Model-View-Controller, a design pattern that enforces the separation of concerns on the presentation layer. Note that MVC is in no way limited to web applications; it might also be applied to other platforms that expose a presentation layer, e.g., a desktop environment.
MVC enables the decoupling of data access logic (model) from user interface logic (view), and establishing the controller as an intermediate component.
Model: Represents the domain, that is, the entities that encapsulate raw data plus the domain logic. Note that the MVC pattern doesn't mention how the data persistence should be implemented. Usually, it is encapsulated by the Model component.
View: This comprises the user interface components. They know how to present data, and expose graphic elements such as buttons, lists, and text boxes, that allow user interaction.
Controller: The controller is the component that orders the rendering of the view, responds to events of the user interface, and invokes changes in the model.
Although some authors conclude that WebForms implement the MVC pattern, it must be said that in WebForms, the controller rule is shared between the ASPX file, the code-behind, and the web controls, which goes against the separation of concerns and the single responsibility principle.
The Monorail Framework
Monorail, developed by Castle Project since 2003, is an Open Source MVC framework for .NET web applications, inspired on the Ruby on Rails Action Pack. The Monorail flow could be described in a few lines, but since a picture is worth 10,000 words...
Note that Monorail has an implementation of IHttpHandler
, so it still takes advantage of the ASP.NET infrastructure (e.g.: session management, events, and security).
Having said that, we can enumerate some advantages of Monorail over traditional WebForm applications:
- Unlike WebForms, Monorail doesn't have a complex page life cycle.
- In Monorail, the separation of concerns principle is enforced. When you develop your Monorail app, you naturally think of it as a structure of models, views, and controllers. In WebForms, on the other hand, the designer hides the relationship of these structures from the developer, often resulting in a bad practice of mixing view code and controller code. Monorail doesn't have a designer, so you must write your views manually. It might be considered a bad thing, that's true, but you have total control over how your HTML is written. In WebForms, on the other hand, the designer often messes up your HTML code, and even introduces unneeded code.
- With Monorail, one doesn't have to be concerned with view state. Although HTTP is stateless by nature, with WebForms, Microsoft has introduced the idea of a view state, maintaining the view data through a page's life cycle. This is good, but adds too much complexity to the development, and decreases overall performance, and introduces security issues.
- Monorail allows unit testing over controllers, due to the decoupled design. On the other hand, WebForms design makes it difficult to perform unit testing on the code-behind.
Monorail Hands-On: The Northwind Traders Sample Application
The goal of the Northwind Traders application is to provide simple CRUD functionalities for Products and Suppliers of the company. That is, we must be able to create, retrieve, update, and delete products and suppliers of Northwind Traders through our Monorail application.
The structure of the Northwind Traders project follows Monorail standards: we have a Monorail project, much like a traditional WebForms project, and it has separate folders for Models, Views, and Controllers.
The Models folder contains the entities Product and Supplier, which are persisted to the SQL Server database through the ActiveRecord framework.
The Views folder contains the .vm files, which are basically NVelocity template files used by Monorail to generate the final HTML code for the view.
The Controllers folder contains the controller classes, ProductController
and SupplierController
. Each controller has the methods representing the actions emitted against the controller to perform the CRUD functionalities on the entities.
URL Routing
Unlike WebForms websites, where each URL is redirected to a physical file in the website, in Monorail, the URL is written to be re-routed not to a file, but to a specific action in a specific controller, following this format:
http://website/MonorailApplication/controller/action.rails
Example:
http://localhost/monorail/product/masterdetail.rails
The result of this request is the rendering of the following page:
Let's try to understand how we got there from our URL:
http://localhost/monorail[1]/product[2]/masterdetail[3].rails[4]
- [1] monorail is our website.
- [2] product tells the Monorail framework to instantiate the
ProductController
class. - [3] masterdetail tells the Monorail framework to invoke the
MasterDetail
method in the ProductController
class. - [4] .rails is already configured in IIS as an extension associated with the ASP.NET ISAPI. Once ASP.NET is called, it investigates our web.config file, where there is a configuration (
httphandlers
tag) that tells ASP.NET to redirect every ".rails" request to the Monorail Framework.
The Domain Model
I could have added more entities to our model, but for the sake of simplicity, I decided to keep only the Product and Supplier. Each entity class is inherited from the ActiveRecordBase
class, which automatically provides Refresh, Delete, Update, Create, and Save functionalities.
The code below represents the Product
class:
using System;
using Castle.ActiveRecord;
namespace Northwind_Monorail.Models
{
[ActiveRecord("Products")]
public class Product : ActiveRecordBase<Product>
{
private int id;
private String name;
private decimal price;
private Supplier supplier;
[PrimaryKey("ProductID")]
public int Id
{
get { return id; }
set { id = value; }
}
[Property("ProductName")]
public string Name
{
get { return name; }
set { name = value; }
}
[Property("UnitPrice")]
public decimal Price
{
get { return price; }
set { price = value; }
}
[BelongsTo("SupplierId")]
public Supplier Supplier
{
get { return supplier; }
set { supplier = value; }
}
public new static Product[] FindAll()
{
Product[] products = (Product[])FindAll(typeof(Product));
return products;
}
public static Product FindById(int id)
{
return (Product) FindByPrimaryKey(typeof(Product), id);
}
}
}
Just a few notes on the Product class:
- Note that the
Product
class is decorated by the [ActiveRecord("Products")]
attribute, which means that the Product
class is mapped to the Products table in the Northwind database. - The
Id
property is decorated by [PrimaryKey("ProductID")]
, which means that it is mapped to Products' primary key column (ProductID) in the Northwind database. - Each non-key property is mapped to the table through the
[Property()]
attribute. - The
Product
class has a reference to the Supplier
class. This is done by the Supplier
property, which is of Supplier
type. The attribute [BelongsTo("SupplierId")]
informs which property of the Supplier
(SupplierID
) is the foreign key property.
The Views
The view files in our project have the .vm extension, which means they will be processed by NVelocity. NVelocity is a template engine; that is, NVelocity files will be injected with information from controllers in order to form the final HTML code. You can use other view engines, like Brails or even .aspx (although this last one is not advisable, since you wouldn't be able to use Web Controls). But here, we will deal with the NVelocity view engine only.
As an example, the following code snippet represents the index.vm file:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
$AjaxHelper.GetJavascriptFunctions()
$ScriptaculousHelper.GetJavascriptFunctions()
<script language="javascript" type="text/javascript">
.
.
.
<div id="headerwrap">
<div id="header">
#parse("menu\\headermenu.vm")
</div>
<div id="middlewrap">
<div id="middle">
<div id="sidebar">
#parse("menu\\shortcut.vm")
</div>
<div id="content">
<div id="statusbar"></div>
<div id="details">#parse("product\\edit.vm")<div>
<div id="search">#parse("product\\search.vm")</div>
<div id="list">#parse("product\\list.vm")</div>
</div>
</div>
</div>
</div>
<div id="footerwrap">
<div id="footer">
</div>
</div>
</body>
<html>
Notice the #parse
directives in the view above. The role of the #parse
directive is to render the content of another view within the view, so that you can keep clean, small, and cohesive views for your website.
The following picture explains how partial views are rendered by the main product view (index view):
The Controllers
The code below represents the ProductController
class:
using System;
using System.Collections;
using System.Collections.Generic;
using Castle.MonoRail.Framework;
using Castle.MonoRail.Framework.Helpers;
using Castle.Monorail.JSONSupport;
using Newtonsoft.Json;
using NHibernate.Expression;
using Northwind_Monorail.Models;
using Northwind_Monorail.Queries;
namespace Northwind_Monorail.Controllers
{
[Layout("default"), Rescue("generalerror"),
Helper(typeof(JSONHelper), "Json")]
public class ProductController : SmartDispatcherController
{
.
.
public void Index([DataBind("product",
Validate = true)] Product product)
{
PropertyBag["products"] = new Product();
string productName = "";
int supplierId = 0;
PropertyBag["products"] =
PaginationHelper.CreatePagination(this,
GetProducts(productName, supplierId), 10);
PropertyBag["suppliers"] = GetSuppliersWithABlankItem();
List<Shortcut> shortcuts = new List<Shortcut>();
Shortcut scProduct = new Shortcut();
scProduct.Text = "Products";
scProduct.Image = "\\Images\\products.gif";
scProduct.Url = "\\product\\Index.rails";
scProduct.DetailUrl = "\\product\\edit.rails";
scProduct.SearchUrl = "\\product\\search.rails";
scProduct.ListUrl = "\\product\\list.rails";
scProduct.Tooltip = "Manage Products";
Shortcut scSupplier = new Shortcut();
scSupplier.Text = "Suppliers";
scSupplier.Image = "\\Images\\suppliers.gif";
scSupplier.Url = "\\supplier\\Index.rails";
scSupplier.DetailUrl = "\\supplier\\edit.rails";
scSupplier.SearchUrl = "\\supplier\\search.rails";
scSupplier.ListUrl = "\\supplier\\list.rails";
scSupplier.Tooltip = "Suppliers";
shortcuts.Add(scProduct);
shortcuts.Add(scSupplier);
PropertyBag["shortcuts"] = shortcuts;
}
.
.
.
Suppose you request the following URL to the website:
http://localhost/monorail/product/index.rails
The following events will take place:
- The Monorail framework will invoke the action (i.e., the
index()
method) of an instance of the ProductController
class. - The index method will retrieve data from model (both product and supplier data) and store it in a
PropertyBag
list. - After the execution of the product controller, the Monorail framework will render the view corresponding to the action (index.vm).
- The view will populate the dropdown list with supplier information, and the grid with the product information.
Downloading and Installing the Sample Application
Installing the Northwind Database
Since the sample application depends on the Northwind database:
- You must have a local SQL Server 2000 or 2005, or have access to a remote one.
- Make sure your server has a Northwind sample database installed. If not, please download it from the Microsoft Download Center and restore it on your server.
IIS Configuration
A request to a Monorail application is identified by IIS by the URL extension. Usually, that extension is .rails (like in our sample), but you can use other extensions, if they aren't in use yet. When users call a URL of your Monorail web application, the Internet Information Server must first pass that information to the ASP.NET ISAPI, so that the ASP.NET framework can read your app's web.config, where there is an explicit instruction to hand over control to the Monorail framework whenever the .rails extension is found:
<httpHandlers>
<add verb="*" path="*.rails"
type="Castle.MonoRail.Framework.MonoRailHttpHandlerFactory,
Castle.MonoRail.Framework"/>
.
.
.
</httpHandlers>
Httphandler tag in Web.config file
But, in order for the .rails extension to be processed, it must first be associated with the ASP.NET ISAPI. You can do it by configuring IIS as follows:
- Open the IIS Management Console.
- Right-click the Default Web Site item and choose Properties.
- Select the Home Directory tab.
- Click Configurations.
- Click Add.
- Select the ISAPI DLL. You can copy-and-paste the complete DLL file name from another extension, such as .aspx. In most systems, it will be something like C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll (for .NET 1.1) or C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll (for .NET 2.0).
- Fill in the extension (for example, .rails) as the file extension (make sure you do not omit the leading dot).
- Uncheck the Check file exists check box.
For more detailed information, see: Installing Monorail.
Note: We had to perform this configuration above in the IIS because in this sample, we are using the .rails extension. However, as our reader mentifex pointed out in his comments, you can make your life easier by changing your web.config file to use the .ashx extension instead of .rails. ASHX (ASP.NET Web Handler File) is an ASP.NET native extension, and you don't have to configure manually its mapping on the IIS, since it is already mapped to the ASP.NET ISAPI. Besides, in cases when you don't have permission to change your IIS configuration, .ashx is your only choice. Just change your web.config to map .ashx files to the Monorail framework, and you're done.
Downloading the Sample Application
Next, you download the Northwind Traders application attached to this article to a local folder:
Then, you create a virtual directory for the application:
Important: Make sure your site was created for ASP.NET 2.0.
That's it!
Downloading the Source Code
In order to open the Northwind Monorail project attached to this article, you should first install Castle Project Release Candidate 3 MSI.
History