First post in a couple of months. I think I have a valid excuse, however. This was originally intended for a Monday publish, but things got in the way.
If you are not aware of it yet, last night (December 9, 2007), the ASP.NET Team released the latest ASP.NET Extensions, which includes the MVC Framework. You cn find it here. If you are going to play with MVC, make sure you also download the toolkit here.
One note about the MVC Framework. If you are still using IIS 6 (shame on you developers who have not upgraded to Vista yet? – yeah, me too), you will have to alter your bits to use .mvc in your path. This is simply a matter of adding .mvc to the default.aspx page. Just remember to remove this prior to production compile if you want the prettier URL mappings on IIS 7. If you are coding on Vista, remember to set this to .mvc before moving to Windows Server 2003, or you will have an ugly surprise.
In this post, I am going to cover the basics of how MVC works. I will link to sites that have more code specific intros, for those who want to dig deeper. This is a level 100 or 200 discussion.
Basics of MVC
MVC stands for Model-View-Controller. The basics of rendering are handled by the view (UI bits) and the controller (piece of software that determines which view to render). The model holds the "data" you wish to make available to the user via the view. Pretty simple, but it does require a paradigm change to stop thinking in terms of ASPX pages and using ASPX merely to render.
So, why should you make the leap? Well, today there is absolutely no reason. And, if the IIS 6 bits are not changed (or IIS 7 made available for your OS), you might not want to in the future. (Yeah, ranting again, but this one stung me a bit – stop playing with sharp toys (betas and CTPs) if you do not want to get stung).
Okay, off the tangent and back to the why. As those who have seen me talk know, I am a rabid advocate of Agile development methodologies, in particular Test Driven Development and Refactoring. When you build your software in such a way where the UI is just the UI, it becomes much easier to place tests around the actual moving parts. Why unit tests? Because you are a computer scientist, not a computer artist. Of course, that is not always true, as I have been paid well to repaint ugly art. If you put your code into classes, you can create tests around it. If you have tests, you have a means of ensuring the quality of your code. This, of course, assumes you are writing the correct tests. And, if you add a test any time a bug is encountered, you can ensure that future changes do not reintroduce that bug either.
MVC in the ASP.NET MVC Framework
When you install the ASP.NET Extensions Preview, you will find a new project type for an MVC ASP.NET website. To create a new site File >> New Project >> {Choose your language} >> ASP.NET MVC Web Application. When you create this website, you will have the following out of the box:
- Content folder – Place holder for the CSS file
- Controllers folder – Where you place your controllers
- HomeController.[vb/cs] – Default controller, points to Home Directory under Views
- Models folder – Where you add models (like DataSets or LINQ to SQL models (dbml))
- Views – Where you add views, but in a special manner (at least by default)
- Home Folder
- About.aspx – about the company view
- Index.aspx – Index page (default view)
- Shared Folder
- Site.Master – Master page for the site
- Default.aspx page
- Global.asax file – has the goo for setting up the default routes
ROUTES
The first important concept in the MVC Framework is routes. In order to use MVC, you have to tell the system how to find your bits. Fortunately, there is no work to be done here if you like the default routing mechanism (route controllers to files, by name, in the Controllers folder) and Views to ASPX pages that are named according to the view in directories that match the name of the controller.
Now, open up the Global.asax, and you find the following code:
protected void Application_Start(object sender, EventArgs e)
{
// Note: Change Url= to Url="[controller].mvc/[action]/[id]" to enable
// automatic support on IIS6
RouteTable.Routes.Add(new Route
{
Url = "[controller]/[action]/[id]",
Defaults = new { action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
RouteTable.Routes.Add(new Route
{
Url = "Default.aspx",
Defaults = new { controller = "Home", action = "Index", id = (string)null },
RouteHandler = typeof(MvcRouteHandler)
});
}
Look at the first route.
Url = "[controller]/[action]/[id]",
- The item after the first slash in the URL is the controller. In this case, http://www.localhost.com/Home would invoke the Home Controller.
- The second item is the action, which means http://www.localhost.com/Home/Index would use the Index method of the Home Controller.
- The third item is the ID of the item to display with the method and leads us into models, which I am not ready to talk about.
Defaults = new { controller = "Home", action = "Index", id = (string)null },
- With the pattern controller/action/id, the default controller for the application is Home (HomeController.[cs/vb] in the Controllers folder.
- The default action is Index (public void index() {} covered later)
- The id is null
RouteHandler = typeof(MvcRouteHandler)
This is just how it is going to work until you build your own custom route handler(s).
The second route maps Default.aspx to the URL http://localhost/ProjectName/Home/Index. Default.aspx, in this web, is merely a placeholder file. If you delete it, however, it will cause your app to incorrectly render when someone merely types in http://localhost/ProjectName/.
You can set up other routes here if you would like or you can change the custom route to whatever you would like. Note the keywords used, however, as you will have to customize all of this if you want a non-default implementation.
Creating Your Own Controller and View
How do you create a new controller and view? Let’s assume our Controller is named Foo with an action called Bar. To set this up, I need to do the following:
- Create an MVC Controller named FooController.[cs/vb] in the Controllers folder (Add >> New Item >> MVC Controller Class)
- Create a new folder called Foo under views
- Add a new MVC View page to the Foo folder called Bar.aspx
We now have this setup:
One final step here are we are golden. You will have to add the following line to the Index action (highlighted):
namespace MvcApplication1.Controllers
{
public class FooController : Controller
{
[ControllerAction]
public void Index()
{
RenderView("Bar");
}
}
}
You can now surf to the URL http://localhost/foo and reach the view (bar.aspx). Pretty simple, eh?
Now for the test. Why is the method named Index? If you will surf back up again, you will remember the following line:
Defaults = new { action = "Index", id = (string)null },
If we had named a different default, we would use it instead. Note that if you step outside of this box, you will have some manual work to do. Second question: Why am I not surfing to http://localhost/foo/bar? Try it and you will get the following:
Server Error in ‘/’ Application.
An action named ‘Bar’ could not be found on the controller.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: An action named ‘Bar’ could not be found on the controller.
You can fix this, however, by simply adding the following to the FooController:
[ControllerAction]
public void Bar()
{
RenderView("Bar");
}
p>Now, the error is gone. Once again, let’s ask WHY? If we go back to the first route, we see this:
Url = "[controller]/[action]/[id]",
Bar is the action. Okay, so this is pretty inane, but repetition is the key to cementing concepts.
TDD and MVC
As my controller is now a class, I can build tests to test the functionality without having to use funky UI TDD kludges. We will first set up a test around our Index controller action method. This was published earlier in the article, but let’s look at it again.
namespace MvcApplication1.Controllers
{
public class FooController : Controller
{
[ControllerAction]
public void Index()
{
RenderView("Bar");
}
[ControllerAction]
public void Bar()
{
RenderView("Bar");
}
}
}
Fairly simple controller. We have already covered why the first method is named Index (defaults) and how it renders by default (find the directory Foo, under the Views directory and then the view page Bar.aspx). We have also covered the Bar action (which is not case sensitive in the URL string – just a bit of trivia that you might find very useful).
Now, we create a test stub on the Index method. I can do this in VS 2008 Pro or greater (Team System), but not in the lower SKUs (Express and Standard). If you are playing with Express, download a unit testing framework, like nUnit or mbUnit. To create the test, I am going to right click on the FooController class and choose "Create Unit Tests". It will create me a test that looks like this:
[TestMethod()]
[HostType("ASP.NET")]
[AspNetDevelopmentServerHost("%PathToWebRoot%\MvcApplication1\MvcApplication1", "/")]
[UrlToTest("http://localhost:64701/")]
public void IndexTest()
{
FooController target = new FooController(); // TODO: Initialize to an appropriate value
target.Index();
Assert.Inconclusive("A method that does not return a value cannot be verified.");
}
This is not really very nice for me, as it is attempting to test the web application. I am not interested in the web app, so let’s clean this up. First, I am going to create a double for the FooController that allows me to find what view we are attempting to render when we call Index. This class inherits from our FooController and looks like this:
public class FooControllerDouble : FooController
{
private string _selectedView;
public string SelectedView
{
get { return _selectedView; }
private set
{
_selectedView = value;
}
}
protected override void RenderView(string viewName
, string masterName
, object viewData)
{
SelectedView = viewName;
}
}
So far, so good. Now we know the correct view when we call index is going to be Bar, so we set up our test. We will prune off everythign but TestMethod() as an attribute and change the internals to use the FooControllerDouble class. Finally, we will add an assert to guarantee the correct view is going to be rendered.
[TestMethod()]
//[HostType("ASP.NET")]
//[AspNetDevelopmentServerHost("%PathToWebRoot%\MvcApplication1\MvcApplication1", "/")]
//[UrlToTest("http://localhost:64701/")]
public void IndexTest()
{
FooControllerDouble target = new FooControllerDouble(); // TODO: Initialize to an appropriate value
target.Index(); ;
Assert.AreEqual("Bar", target.SelectedView);
}
Running the test, we green light. If you are a TDD purist, you will want to delete the RenderView("Bar") line in the controller and get your red light first. If you are a nazi about it, you will want to delete the method altogether.
From here, we can go into Mocks and Dependency Injection, etc., but I will drop it. It is good enough knowing you can run unit tests without running the web application. And, as you experiment with the MVC Framework, you will find your temptation to stick all of your code in your ASPX pages will go away, as it will not work. Okay, not completely true, but it will stop you from writing all of your application logic in the pages.
MODELS
One final subject in this intro. Models are simply the data you are displaying in your view. Yes, that is an overly simplistic explanation, but let’s role with it, as I do not have time to get any deeper than that. First, let’s create a model. I am going to use AdventureWorks, as it is installed as a sample with SQL 2005 developer when you install samples. You can also download the scripts from CodePlex, although you will need the older ones if you are not playing with SQL Server 2008.
To create the model, I will add a LINQ to SQL class to my Models folder called Contacts. I will then drag the Contact table from the AdventureWorks connection in the server explorer (create one if you have not already) and drop it on the design surface.
To display this data, I am going to create a page called Contacts under the Views/Foo directory (Add >> New Item >> MVC View Page). We will touch this page in a second, but first let’s see how we pass the data. Open the foo controller and add the following:
[ControllerAction]
public void Contacts()
{
ContactsDataContextdataContext = new ContactsDataContext();
RenderView("Contacts", dataContext.Contacts);
}
To link the model to your view, you can simply pass it in when you call the RenderView method, as in the code above (highlighted). This is the ViewData that will be passed to the page.
Now, to the page. First, open the Contacts.aspx.[cs/vb] page (I am using C#, so mine is .cs). Find the class declaration and add the generic type declaration for what ViewData is. We could simply add the generic declartion for Contacts, like so (highlighted portions added).
using System.Web.Mvc;
using MvcApplication1.Models;
namespace MvcApplication1.Views.Foo
{
public partial class Contacts : ViewPage<Contact>
{
}
}
But this is only really good if we are passing a single contact (keep this in mind for your detail page). We are passing in a LINQ list of contacts, so it should be like this (highlighted the additions).
using System.Collections.Generic;
using System.Web.Mvc;
using MvcApplication1.Models;
namespace MvcApplication1.Views.Foo
{
public partial class Contacts : ViewPage<IEnumerable<Contact>>
{
}
}
Now we have the power to run through these contacts. First, add an imports statement to the top of the page (ASPX side):
<body>
<div>
<h1>
Contacts</h1>
<ul>
<% foreach (Contact c in ViewData)
{%>
<li>
<%= c.LastName %>,
<%= c.FirstName %></li>
<%
} %>
</ul>
</div>
</body>
The page is now bound, although to way too much data (not too worried about that now, as this is a sample). I was working with hooking this up to a control, like a GridView or DataList, but I can’t seem to be left alone to concentrate today, so I am going to kabosh it for now. The issue I am having, at present, is getting the code behind to recognize controls on the page. I just found that Scott Guthrie’s blog has an example with a working Repeater, but I am not having luck hooking it up. Probably something simple, but I will have to play with it later as my cell is ringing again. Arrrggghhhh!!!
Hope this helps,
Greg