ASP.Net MVC Framework an early look
NOTE: “The information in this post is from an early stage of the MVC Framework and only an overview of the basics. The MVC Framework can and will be changed over time.”
As many of you already know Microsoft is working on a MVC Framework for ASP.Net. MVC stands for “Model View Controller”. The Model View Controller is a design pattern and the idea is to separate business logic from the View (The View is the page that will display content, for example an .aspx page). The MVC in general maintain a clean separation of concerns, it will be easier to test business logic because it’s separated from the View and the View will not have any knowledge of the underlying model. Today when we build ASP.Net applications we have a code-behind which is a partial class of the “View”. With the code-behind model we can’t easy use Test Driven Development (TDD) or unit-test, at least is not easy. We can use the MVP (Model View Presenter), but that require us to write some extra code. With the MVC Framework it will be much easier to apply TDD.
The ASP.Net MVC Framework is integrated with ASP.Net and can use existing infrastructure like caching, session and profile etc. It will also support static and dynamic languages. The MVC Framework will only as it looks now work on ASP.Net 3.5. The .Net 3.5 will be released within any weeks now. The final build of the framework was done only some days ago. With the MVC Framework we will get a Project Template and great tool support. The MVC Framework is extensible and pluggable, so we can replace any system components, it also support Inversion Of Control (IoC)/Dependency Injection (DI). It’s the lifecycle that makes the model extensible. It looks like this:
Request -> Route (IRouteHandler) –> ControlFactory (IControllerFactory) -> Controller (IController) -> ViewFactory (IViewFactory) -> View (IView) -> Response
The Web Server gets browser request, for example http://localhost/Product, the Route to Controller is determined. The Controller is activated and the “Action” method on Controller is invoked. The Controller will then access the model. The Controller will then render View and passing in custom ViewData to the View. The View is rendered.
When we use the MVC Framework we will not specify an URL to a specific page and pass QueryStrings etc. Instead we use a cleaner URL. For example: http://localhost/Products or http://localhost/Producsts/Edit/4 etc. We will map this URL to a specific controller which will be executed when we enter the URL. In the Global.asax we can create routes, which will be handled by an IRouteHandler. The following is the default way to add a route:
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new Route {
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
}
Note: In a later release we will probably be able to add this to a configuration file instead of writing code.
The Route that is added to the RouteTable will use an URL with the following format “[controller]/[Action]/[id]”. The controller is the controller that should be used, the action is the method in the controller that should be executed and the id is a value that can be passed to the controller’s method. If we use this format and enter a URL like the following: http://localhost/Product, the MVC Framework’s IControlleFactory will create and return the controller that belongs to the Product. It will try to locate a controller with the name ProductController. So if you enter an URL like http://localhost/Customer, it will try to instantiate a controller with the name CustomerController. When the controller is instantiated the Index method of the controller will be executed. The Defaults property of the Route class specify the default settings for the route, such as which action method of the controller is the default action (in this case the Index is the default one specified, this can also be specified on a controller with an attribute.), and the default values of the action method’s parameters that should be passed to the method. If we specify a URL like this one: http://localhost/Product/List the List method of the ProductController will be executed instead of the one that is default specified on the Route. The format of the URL specified for the Route was [controller]/[action]/[id], and the [action] in the format specifies which part of the URL is the name of the action method that should be executed. The [id] in the URL is the name of the action method’s parameter and the value enter at this location of an URL will be passed to the action method. If we enter a URL like this one: http://localhost/Product/Edit/4, the Edit method of the ProductController will be executed and the value 4 will be passed as a value to the Edit method’s id argument. The interface of the action method will look like this:
public void Edit(int? id)
I will later in this post write more about the controller.
We can of course change the format of the URL if we want to pass more values to a controller’s action method, for example: “[controller]/[action]/[id]/[pageIndex]”.
The RouteHandler property of the Route specifies which IRouteHandler we want to use.
If we don’t need to use the format “[controller]/[action]/[id]”, for example instead of letting the URL have the name of the controller we can skip the [controller] from the URL and instead specify the type of the controller we want to use when we setup the Route. For example:
RouteTable.Routes.Add(new Route {
Url = "products/[category]",
Defaults = new {
controller = "Product",
action = "Index"
},
RouteHandler = typeof(MvcRouteHandler)
});
Now when we have looked at how we can map a URL to a controller we can take a look at how we can implement a controller. A controller is a normal class that inherits the base class Controller located in the System.Web.Mvc namespace:
using System;
using System.Collections.Generic;
using System.Web.Mvc;
namespace MyControllers
{
public class ProductController : Controller
{
}
}
An action method is a normal method that has the ControllerActionAttribute specified. The ControllerActionAttribute is used because of security reason so not anyone can enter an action in the URL and execute it. The ControllerActionAttbibute has some properties also, for example the DefaultAction which can be used to specify the default action method of the controller.
[ControllerAction(DefaultAction = true)]
public void Index()
{
RenderView("MyView");
}
The RenderView method will render the specified view. In this case the view with the name “MyView”. The RenderView will not do a call to Response.Redirect; instead it will use the Page.ProcessRequest method. By default the RenderView will look into a sub folder of your project with the name Views and then the sub folder with the name of the controller attached to the view.
/root
/Views
/Product
MyView.aspx
The RenderView method is located in the Controller’s base class. This method will execute the IView’s RenderView method. We can create our own IViewFactory which will return our IView representation where we can create our own implementation of the RenderView. For example if we want to render a view based on another format than HTML, for example a jpg or gif image etc. We can create our own IView and make sure our RenderView will render binary data to the output stream instead of text.
The code-behind of the View inherits form the ViewPage instead of the Page object.
Note: We can easy create our own IViewFactory and IView for mock out the views for testing and replace .aspx with other technologies.
To send data to the View that should be displayed we can use the ViewData property of the Controller base class, or we can pass our data to the RenderView method:
[ControllerAction]
public void List(int? page)
{
PagedList<Product> products = repository.GetProducts(page ?? 0, 10);
RenderView("List", products);
}
[ControllerAction]
public void Edit(int id)
{
Product product = repository.GetProductByID(id);
RenderView("Edit", product);
}
[ControllerAction]
public void ShowCustomer(int? id)
{
...
ViewData["CustomerName"] = customer. Name;
RenderView("ShowCustomer");
}
To display this information in our View we can user server side script block:
<table class="product_listing">
<tr>
<th></th>
<th>Product Name</th>
<th>Unit Price</th>
</tr>
<% foreach(var p in ViewData) { %>
<tr>
<td>
<%=Html.Link("Edit", "Products", new { Action="Edit", ID=p.ProductID }) %>
</td>
<td><%=p.ProductName %></td>
<td><%=p.UnitPrice.ToCurrency() %></td>
</tr>
<% } %>
</table>
<h2><%=ViewData.ProductName %></h2>
<h1><%=ViewData["CustomerName"] %></h1>
By using the ViewData collection we can pass several of objects to our view that should be displayed. If we want to pass typed data to the View, we can inherit the Controller<T> where ViewData will be of type T. If we use the Controller<T>, the page which uses the Controller<T> needs to inherit the ViewPage<T> instead of ViewPage.
The MVC Framework doesn’t support postbacks and the use of ViewState, so most of the Controls shipped with ASP.Net can’t be used. Microsoft will probably add new Controls for the MVC Framework.
Note: This framework is not Web Form 4.0 or will replace the existing postback model. The postback model will still exists and Microsoft will still provide new feature to it in the future.
The MVC Framework will be added to the SP1 of VS 2008. This framework is for people that would like to use this MVC design pattern instead of the postback model. The MVC Framework will work better with Test Driven Development and unit-testing, so people that will make it easier to test web apps will also probably use this model. I will use it in every Web project in the future. With this model it would also be easier to maintain applications because we will have more control over the HTML. We don’t need to hook up to an event of a control and try to figure out how to add extra data into a GridView if we want to extend it etc. This model reminds of the old classic ASP, and it will be easier for people that work with classic ASP to move to ASP.net if they start using the MVC Framework.
Testing with MVC will as I mentioned before be much easier. We have several of interfaces that we can use. The Mockable intrinsic objects are IHttpContext, IHttpResponse and IHttpRequest. Because the MVC Framework have interfaces for IRouteHandler, IController, IControllerFactory, IView and IVewFactory we have more extensibility options. If we want to test a Controller’s action we can for example create our own IViewFactory and IView, by doing this we can change the implementation of the RenderView method.
[TestMethod]
Public void TestTheEditActionFotTheProductContoller()
{
ProductController controller = new ProductController(mpr);
TestViewEngine testView = new TestViewEngine();
controller.ViewFactory = testView
controller.Edit(5);
Assert.Equal(testView.Template, "Edit");
Asssert.Equal(testView.GetViewData<Product>().ProductID, 5);
}
If you want to see some sample code, you can download it from Scott Hanselman’s blog.
Watch out for more information on Scott Guthrie blog.