Tuesday 24 February 2009

ASP.net MVC Framwork Part 2

In my previous post, I wrote about Microsoft ASP.Net MVC framework and how it resolves the complexity of implementing model-view-controller design pattern in ASP.net web applications. In this post, I will compare what it takes to use MVC pattern in an ASP.net web application and do the same using the ASP.Net MVC framework.

The QuickShop Application
For my example, I will create a very very simple online store, which sells such a small number of items that they can be listed in a drop down list. We will also assume that the name of products are sufficient enough for the client and that all payment is done offline. Purchase through the online store has three steps in total. The first page shows the list of products and allow customers to select product and quanity. The second page shows a confirmation message with the selected product and quanity and the last page creates the order and takes the user back to first page.

Database
The databae of our simple application is also very simple. The database has two tables, Product & Order. The product table contains all the products present in the store and the Order table contain all the Orders, as shown


ASP Web Application
To implement the controller of our application, we create an abstract class called "Process". The class would contain a list of "Actions". The controller would take input from view (ASP.Net pages) and move from one action to another, while working with Model, which is constituted of the database and entity classes.
namespace QuickShopWeb
{

using System.Collections.Generic;

public delegate void ActionChangedEventHandler(object sender, System.EventArgs e);


[System.Serializable]
public abstract class Process
{
private List _actions;
private int _currentActionIndex;

public event ActionChangedEventHandler OnActionChanged;

public List Actions
{
get{ return _actions; }
}

public Action CurrentAction
{
get{ return _actions[_currentActionIndex]; }
}


internal void SetActions(List actionsToSet)
{
this._actions = actionsToSet;

foreach (Action action in this._actions)
{
action.OnCompletion += new ActionCompletionEventHandler(this.Action_OnCompletion);
}
}

private void Action_OnCompletion(object sender, System.EventArgs e)
{
if (this._currentActionIndex >= this._actions.Count - 1){
// Restart
this._currentActionIndex = 0;
}
else{
// Move to next action
this._currentActionIndex++;
}
// Notify the View that we've moved on to a new action
if (this.OnActionChanged != null) {
this.OnActionChanged(this, e);
}

}
}
}


It contains a list of objects of type "Action". There is an indexer and a method that set the list of Actions. Also defined is an event and and event handler. The event is invoked when an action is completed and the EventHandler simply moves the indexer to the next Action in the list.

We add the [System.Serializable] attribute because we would need to persist Controller between views and this is achieved by keeping the controller object in HTTP session object.

We next look at the Action class, which is again a Serializeable abstract class, defines an event for ActionCompletion and a method called Complete. The class is defined as follows:

namespace QuickShopWeb
{
using System;
internal delegate void ActionCompletionEventHandler(object sender, EventArgs e);

[System.Serializable]
public abstract class Action
{
internal event ActionCompletionEventHandler OnCompletion;

internal void Complete(EventArgs e)
{
if (this.OnCompletion != null)
{
this.OnCompletion(this, e);
}
}
}
}
Now that the groundwork is done, all we need now is to define the specific process for our application. Our simple application has three actions
  1. IndexAction: Need to retrieves all the products display them in the view
  2. Confirm: Need to shows confirmation message in the view
  3. CreateOrder: Need to create new order record.
Our process class looks like following:

namespace QuickShopWeb
{
using System.Web;
using System.Collections.Generic;

[System.Serializable]
public class ShopProcess : Process
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public string ProductName { get; set; }

public ShopProcess()
{
List actions = new List();
actions.Add(new IndexAction());
actions.Add(new Confirm());
actions.Add(new CreateOrder());

this.OnActionChanged += new ActionChangedEventHandler(ProcessActionChanged);
this.SetActions(actions);
}

private void ProcessActionChanged(object sender, System.EventArgs e)
{
RedirectToAction(CurrentAction);
}

public void RedirectToAction(Action action)
{
switch(action.GetType().Name)
{
case "IndexAction":
HttpContext.Current.Response.Redirect("~/View/Index.aspx");
break;

case "Confirm":
HttpContext.Current.Response.Redirect("~/View/Confirm.aspx");
break;

case "CreateOrder":
HttpContext.Current.Response.Redirect("~/View/Complete.aspx");
break;
}
}
}
}
As you can see, the ShopProcess initiates all action and creates an EventHandler for OnCompletion event of all actions. The event handler simply redirects to appropriate view.

Now that we have the controller part working, next is to create view. I will explain the only the Index.aspx page. Views for other actions are similar and can be easily understood by going through the code. The default aspx page created an object of type ShopProcess and added it to the Session variable as shown:


public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
ShopProcess goShopping = new ShopProcess();
Session["CurrentProcess"] = goShopping;
goShopping.RedirectToAction(goShopping.CurrentAction);
}
}
This starts the process and takes the user to the index.aspx page. The index.aspx codebehind file looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace QuickShopWeb.View
{
public partial class Index : System.Web.UI.Page
{
public ShopProcess CurrentProcess
{
get
{
return (Session["CurrentProcess"] as ShopProcess);

}
}
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
IndexAction action = CurrentProcess.CurrentAction as IndexAction;
foreach (Product product in action.Products)
{
ProductDropDownList.Items.Add(new ListItem(product.ProductName, product.ProductId.ToString()));
}
}
}

protected void OrderButton_Click(object sender, EventArgs e)
{
CurrentProcess.ProductId = int.Parse(ProductDropDownList.SelectedValue);
CurrentProcess.Quantity = int.Parse(QuantityTextBox.Text);
CurrentProcess.ProductName = ProductDropDownList.SelectedItem.Text;

CurrentProcess.CurrentAction.Complete(e);
}
}
}
The index.aspx page (view) extracts data passed to it from the contoller and displays it to the user. When user input some data, the view passes it to the controller and set the current action to complete (as shown in the OrderButton_Click) event.

The complete listing of web application can be downloaded from this link.

Using MVC Framework
Now, we will create the same application using MVC framework. I have created this example using the Microsoft MVC Release Candidate 1. If you have not downloaded it already, you can download it free from here.

We create a new solution and this time instead of choosing "ASP.Net Web Application", we choose "ASP.Net MVC Web Application" as shown:

The next step in the wizard would ask you if you want to create a unit test project. I select the option "No, do not create a unit test project" for my application. When you click OK, you will see that the wizard has created classes for Controller, home page, about page, etc. We will ignore them now.

What I will do now is to add a dbml class that points to our QuickShop database. Then, click on the HomeController.cs class and rewrite the code for Index() method, so that it returns a list of all Products, so the code would look like:

public ActionResult Index()
{
var products = from p in db.Products orderby p.ProductName select p;
return View(products.ToList());
}
By default the Index method is invoked for the Index page and this list of products would be available to the Index.aspx page (View). All we need to do is to create a codebehind file for Index.aspx and inherit it from System.Web.Mvc.ViewPage<List<Product>> So the codebehind class for our Index page would look like

namespace QuickShop
{
using System;
using System.Collections.Generic;
using System.Web;

public partial class Index : System.Web.Mvc.ViewPage>
{
}
}

To access the data returned from the controller, the aspx page (view) needs to use the ViewData.Model property. The complete listing of our Index.asp page is shown below

<%@ Page Language="C#" AutoEventWireup="false" CodeBehind="Index.cs" Inherits="System.Web.Mvc.ViewPage" %>
<%@ Import Namespace="QuickShop" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Quick Shop</title>
</head>
<body>
<h1>Quick Electronic Shop</h1>
<br />
<br />
<div>
<form method="post" action="/Home/Confirm">
Select Product
<br />
<select name="product" style="width:200px;">
<%
foreach(Product product in (ViewData.Model as List<Product>))
{
Response.Write( string.Format("<option value=\"{0}\">{1}</option>", product.ProductId , product.ProductName ));
}
%>
</select>
<br />
<br/>
Quantity
<br />
<input type="text" name="quantity" style="width:200px;"/>
<br />
<br />
<input type="submit" value="Order" />
</form>

</div>
</body>
</html>
Please note that we are not using any server side components in this page and the action of the forms is set to "/Home/ConfirmOrder". This would automatically invoke the ConfirmOrder method in the Home controller. Moreover, the form input variables are automatically passed as parameters to the method. So, the signature of our Confirm method would look like:

public ActionResult Confirm(string product, string quantity ) { }
Compare this will all the code we had to write in our ASP Web application.

In the confirm method, all we need to do is to return an object in the actionResult, which contains
details of the product and quantity set in the previous page. The complete code of the confirm method is

public ActionResult Confirm(string product, string quantity )
{
string productName = string.Empty;
var products = from p in db.Products where p.ProductId == int.Parse(product) select p;
if (products != null)
{
foreach (Product p in products)
{
productName = p.ProductName;
break;
}
}
Dictionary dict = new Dictionary();
dict.Add("product",product);
dict.Add("productName", productName);
dict.Add("quantity", quantity);
return View( dict );
}
As you can see, the method returns the actionResult with a Dictionary object, containing all the information needed by view. The confirm.aspx page (view) would display this method and show a confirm button, which will set the action to "CreateOrder". The listing of confirm.aspx page is shown below:
<%@ Page Language="C#" AutoEventWireup="false" Inherits="System.Web.Mvc.ViewPage" Codebehind="~/Views/Home/Confirm.cs" %>
<%@ Import Namespace="QuickShop" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Quick Shop</title>
</head>
<body>
<h1> Confirm your order...</h1>
<div>
<%
Dictionary<string, string> values = (ViewData.Model as Dictionary<string, string>);
%>
You have chose to buy <b><%=values["quantity"]%></b> pieces of <B><%=values["productName"]%></B>. Are you sure?
<form method="post" action="/Home/CreateOrder">
<input type="hidden" name="product" value="<%=values["product"]%>"/>
<input type="hidden" name="quantity" value="<%=values["quantity"]%>"/>

<input type="submit" value="confirm />
</form>
</div>
</body>
</html>

Now, the last bit of detail needed in our MVC application is to create a method for CreateOrder in the HomeController. This method again takes two parameters -same as the input submitted through the HTML form. The method would create a new order record in the database and then redirects the view back to Index page.
public ActionResult CreateOrder(string product, string quantity)
{
Order newOrder = new Order();

newOrder.OrderDate = DateTime.Now;
newOrder.ProductId = int.Parse(product);
newOrder.Quantity = int.Parse(quantity);
db.Orders.InsertOnSubmit(newOrder);
db.SubmitChanges();

return RedirectToAction("Index");
}
The complete listing of the quickshop application using MVC can be downloaded from this link.

Conclusion
I hope that the example, although overly simplistic, would have helped you in understanding how MVC framework simplifies using the MVC design pattern in the asp.net web applications. The framework does a lot of plumbing work for in getting and setting data from HTTP posts. For its functionalty the MVC framework uses it's own HTTPHandler.

In my next post, I will write about what happens behind the scenes in MVC framework and how does it work internally.

No comments: