Introduction
There are many ways to create ASP.NET web applications; however, one common problem with them all is the difficulty in separating business logic from presentation. Separating these layers allows for more modular development, code reuse, and ease of testing. A pattern that supports these features is the Model-View-Controller, or MVC. The ASP.NET team has recognized the benefits of this pattern, and is working to incorporate it into ASP.NET. This article will focus on building an ASP.NET MVC web application from the Preview 2 release of ASP.NET 3.5 Extensions.
Prerequisites
Model-View-Controller Pattern
The first thing that must be discussed, of course, is what the MVC pattern is. MVC is a pattern that divides an application into separate areas of responsibility: Model, View, and Controller.
- Model: Models are responsible for maintaining the state of the application, often by using a database.
- View: These components are strictly for displaying data; they provide no functionality beyond formatting it for display.
- Controller: Controllers are the central communication mechanism in an MVC application. They communicate actions from the views to the model and back.
One of the main points with the MVC pattern is that there is no direct communication between the model and view. This allows for reuse of model and controller code in different types of applications. The logic for a Web application could easily be applied to a Windows application by changing the view components. The controller components could also be exposed as Web Services for SOA without affecting the view. Of course, the model could also be changed without affecting the controller; for instance, if the database itself or the scheme were changed. Another benefit to separating the functions is to allow for better testing. Since the view is only concerned with display, all of the logic that needs tested is in the controller. Unit tests can easily be incorporated to test these functions.
ASP.NET Extensions
ASP.NET 3.5 Extensions Preview is a set of enhancements to ASP.NET and ADO.NET that are expected to be included in future releases but are available now as previews. These enhancements include ASP.NET Silverlight controls, ADO.NET Entity Framework, ASP.NET Dynamic Data, and ASP.NET MVC. The last is, of course, what we will be focusing on here. For more information about the others, please see the references at the end of this article.
All of the functionality for ASP.NET MVC projects is provided in three assemblies:
- System.Web.Mvc
- System.Web.Routing
- System.Web.Abstractions
Creating an ASP.NET MVC project
After installing the extensions, you should see a new project type under File -> New Project -> Visual C# -> Web. This project is only available in C#. There are no plans at this time to support VB.
The first thing this project wizard asks is if you want to create a unit test project. As stated earlier, one of the benefits of the MVC pattern is the separation of logic for easy testing. Although unit testing will be covered in a future article, we'll create the test project along with the main Web Application project.
As we can see below, the project creation wizard will create the basic shell of the Web Application, including a Master page and two views, About
and Index
, and the HomeController
. The test project has also been created and added to the solution, with unit test for the HomeController
.
Music Catalog Application
We will use this basic shell to build a simple demo app that displays artists, albums associated with the artist, and the songs associated with the album. Later in this series, we will also create forms for editing and inserting data. The database script provided with this article includes all the data we will be working with.
Applying the MVC Pattern
The Model
Since the application isn't very useful without data, we will start by creating a model for the MusicCatalog. To simplify development, we will be using LINQ to SQL.
Add New Item -> Data -> LINQ to SQL Classes. We'll name the class Music
.
After clicking the Add button, a designer will be displayed with two panels. The right panel allows you to drag Stored Procedures from a database onto this surface to create methods in the generated class. We'll skip this for the time being and concentrate on the left panel. This panel allows you to drag database tables from the Server Explorer onto the surface and create classes, with access methods from them. For this demo, open the Server Explorer and navigate to the MusicCatalog database. Drag the three tables, Artist, Album, and Song onto the designer surface.
A Brief Look at LINQToSQL Classes
As you can see, after the tables have been dragged to the design surface, the foreign key relationships established in the database are also represented in the designer and generated classes. If we open the Music.designer.cs file, we can see the classes that have been created.
[System.Data.Linq.Mapping.DatabaseAttribute(Name="MusicCatalog")]
public partial class MusicDataContext : System.Data.Linq.DataContext
Notice here that DataContext
has automatically been appended to the name we provided, Music
. It is the convention to name LINQ to SQL classes this way. We can also see that LINQ to SQL uses the DatabaseAttribute
to identify the database this class represents.
Partial Methods
The generated class also contains definitions for methods that have been declared as Partial.
#region Extensibility Method Definitions
partial void OnCreated();
partial void InsertArtist(Artist instance);
partial void UpdateArtist(Artist instance);
partial void DeleteArtist(Artist instance);
partial void InsertAlbum(Album instance);
partial void UpdateAlbum(Album instance);
partial void DeleteAlbum(Album instance);
partial void InsertSong(Song instance);
partial void UpdateSong(Song instance);
partial void DeleteSong(Song instance);
#endregion
This is a new feature added in .NET 3.0 that is similar to the partial class that was introduced in .NET 2.0. Partial classes allow code to be separated into multiple classes and complied into a single unit. An example is the separation of ASP.NET code-behind files. Partial methods allow a designer to implement, define, and stub in methods that may be implemented by developers using the class. One difference between partial methods and virtual methods is that if the method is not implemented, the compiler just ignores it, as can be seen below.
However, when the method is implemented in a partial class, it is used and compiled into the assembly.
A usage for this technique is for simple lightweight messaging capability. It is also useful for providing hooks into your code that other developers can use to provide additional functionality, such as auditing, without the need to drive additional classes.
Adding accessor methods
The DataContext
class isn't very useful as is, since it doesn't really provide methods to access the data we need, such as getting a list of the albums associated with a given artist. So we'll add some methods now. These could be provided by Stored Procedures in the database and dragged to the design surface to be automatically generated. However, by implementing them ourselves, we can look at some LINQ methods and techniques.
public Artist GetArtistById(int id)
{
return Artists.Single(a => a.id == id);
}
public List<Album> GetAlbumsForArtist(int id)
{
return Albums.Where(a => a.artist_id == id).ToList();
}
These two methods are only a sample; see the MusicDataContext
class for the full implementation. They show using the Single
and Where
LINQ extension methods to return an artist matching the given ID and a list of albums for the given artist ID. Since this article isn't about LINQ, we'll move on and leave the details to other articles.
The Controller
All of the controllers in this application are located under the appropriately named Controllers folder. In an ASP.NET MVC application, users don't make requests for pages or resources, instead they request actions. Each public method in a controller is an action that can be requested.
The default implementation provided by the project template includes one controller, HomeController
, with two actions, Index
and About
. When creating a new controller class, it must have the suffix Controller. MVC uses Reflection to locate controllers based on the names, as we will see shortly, with this suffix. The reason for this requirement is unknown.
public class HomeController : Controller
{
public void Index()
{
RenderView("Index");
}
public void About()
{
RenderView("About");
}
}
We'll cover the RenderView
method shortly, so for now, just ignore it.
MusicController
To create the controller for the MusicCatalog application, right click on the Controllers folder in the MVC project and select Add -> New Item or Class and select MVC Controller Class in the Add New Item dialog. Notice the description stating that the class must use the Controller suffix as mentioned earlier.
The class that is created is derived from System.Web.Mvc.Controller
and already contains a method, Index
. This is the default action for any controller.
public class MusicController : Controller
{
public void Index()
{
}
}
We will add an instance of the previously created Model, MusicDataContext
, and methods for the actions necessary to retrieve and display artists, albums, and songs.
public class MusicController : Controller
{
private MusicDataContext m_Music = new MusicDataContext();
public void Index()
{
}
public void Artists(string letter)
{
RenderView("Artists", DataContext.GetArtists(letter));
}
public void Albums(int id)
{
RenderView("Albums", DataContext.GetAlbumsForArtist(id));
}
public void Songs(int id)
{
RenderView("Songs", DataContext.GetSongsForAlbum(id));
}
#region Properties
private MusicDataContext DataContext
{
get { return m_Music; }
}
#endregion
}
Rendering the View
The controller initiates the process of having a page displayed to the user by calling RenderView
, which has a few overloads. In the overload version we are using, the first parameter is the name of the view page to be rendered. Notice we do not need to specify a full path, only the name. The MVC framework will look for this page in a folder matching the name of the controller under the views folder. The second parameter to the RenderView
method is an object that will be passed to the view. This object is assigned to the ViewData
member, which is an implementation of IDictionary<string, object>
. You can assign ViewData
directly and use an alternate override of RenderView
, as below.
public void Artists(string letter)
{
ViewData["Artists"] = DataContext.GetArtists(letter);
RenderView("Artists");
}
Of course, since ViewData
is an IDictionary<string, object>
, we can assign multiple values to be passed to the View
.
public void Index()
{
ViewData["TheAnswer"] = 42;
ViewData["Data"] = DataContext.GetArtists("A");
RenderView("Artists");
}
If the second RenderView
method, commented out above, is used, it will overwrite any previous assignments of ViewData
with the List<Artists>
returned from DataContext.GetArtists("A")
.
The View
Now that we have the Model and Controller, it's time to move on to the Views. We'll start by adding the Artist view to display all of the music artists in the database.
First, add a new folder under Views called Music. The name of this folder matches the name of the controller it relates to. Notice the Home folder with views matching the actions in the HomeController
that was created by the project template. Next, right click the folder you just created and select Add -> New Item.
As you can see, there are four items that relate to MVC projects; Master Page and User Control should be obvious. The difference between MVC View Page and MVC View Content Page is the latter is for use with a MasterPage, while the former is a stand-alone page. We will select MVC View Content Page and name it Artists.aspx. Select Site.Master
under the Shared folder when asked for a MasterPage to use.
One thing to keep in mind with MVC View pages is that they are not ASP.NET pages. Although they have the aspx extension for convenience, they have no form, as the below MVC View Page sample shows. All interactions are processed via controllers, not by forms, as in a traditional ASP.NET application. Though they are not ASP.NET forms, server controls can still be used, as we will see shortly.
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<div>
</div>
</body>
</html>
ViewPage Code-behind
Opening up Music.aspx.cs, you can see the class is a Partial class, just like an ASP.NET page, but derives from ViewPage
rather than Page
.
public partial class Artists : ViewPage
Within this class, you can use the same methods and events you would in a normal ASP.NET page, such as the Load
or DataBind
events. In our case, we'll override the Load
event.
protected override void OnLoad(EventArgs e)
{
AddMenu();
ArtistList.DataSource = (List<Artist>)ViewData["Artists"];
ArtistList.DataBind();
}
We start by adding a simple alphabetic menu to the top of the page for navigation; more on that in a moment. Next, we bind the ViewData
, which was passed from the controller, to the ListView
control. Remember that the ViewData
was also a member of the controller class. Keeping the same name between the classes is a convenience. Since ViewData
is an IDictionary<string, object>
, we need to cast it from an object to a generic List<Artists>
before using it. This loose coupling is good, but what if we needed more strongly typed data? ViewPage
also has a generic constructor that allows you to specify the type ViewData
will be, making any casting, and the inherent performance penalties from possible boxing/unboxing operations, unnecessary. We also gain Intellisence support and compile time error checking.
public partial class Artists : ViewPage< List<Artist> >
Now ViewData
can be used as a List
rather than as a generic object, as in the first image.
Creating a Menu
To be somewhat useful, this application needs a way to list the artists in the database so users can select them. A simple alphabetic menu should serve our needs.
We construct the menu by simply iterating from A to Z, creating an HTML link for each letter and adding it to a container. Nothing special here, except for creating the link.
private void AddMenu()
{
for(char c = 'A'; c <= 'Z'; c++)
{
string link = Html.ActionLink(c.ToString(), "Artists",
new RouteValueDictionary( new { controller = "Music",
letter = c.ToString() }) );
Alphabet.Controls.Add(new LiteralControl(link));
if(c != 'Z')
Alphabet.Controls.Add(new LiteralControl(" | "));
}
}
ViewPage
has an HTML
property that exposes an HtmlHelper
class. This class, as the name implies, provides helper methods for creating HTML links: ActionLink
and RouteLink
. Remember that in an MVC application, everything is routed through controllers that take action as necessary, hence the method ActionLink
. We don't create HTML links that point to a URI, but rather links that tell what action to request from a controller.
In the above example, the first parameter is the text that will appear on the link. The next parameter is the name of the action to request. The third parameter, as we can see, is a RouteValueDictionary
object that uses object initialization to assign a value to the controller
and letter
properties. This generates an HTML anchor tag that matches the route format, {controller}/{action}/{letter}
, and looks like this: <a href="/Music/Artists/A">A</a>.
URL Routing
Now that we have the basic architecture of the application laid out, and, hopefully, an understanding of the major pieces, the question is, How does the application know how and when to display a particular page? The answer is URL Routing.
A route is the path, or URL in traditional web applications, to an action in a controller. An ASP.NET MVC application adds a RouteTable
object to the application scope for this purpose. RouteTable
, defined in the System.Web.Routing
namespace, contains a single property, Routes
, which is a RouteCollection
.
The ASP.NET MVC project template adds this implementation to the Gloabal.asax.cs file.
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { action = "Index", id = "" }),
});
routes.Add(new Route("Default.aspx", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home",
action = "Index", id = "" }),
});
}
protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
}
URL Routing uses pattern matching to direct the request to the appropriate controller and action, and routes are evaluated in the order in which they have been registered. Just like exception handling, you should register routes from most specific to general.
The first route added to the collection is for requests using the format, {controller}/{action}/{id},
such as, Home/About/1
. The second route is a default catch all. Typically in a web application, a request with no resource, only the application name, such as, http://www.mymvcapp, will be routed to a default page, usually default.aspx for ASP.NET applications.
In both routes, you can see that a Defaults
property is also being assigned. This property is of type RouteValueDictionary
, which is an IDictionary<string, object>
, and as you can guess by the name, stores a collection of defaults to be used for the route. In the second route, since no value has been provided for a controller or action, the values specified for each in the Defaults
property are used, Home
and Index
, respectively, in this case. The first route registered above will only match if a controller has been specified in the request, but if an action or ID has not been included, the defaults will be used for those values.
To be Continued...
ASP.NET MVC is a very rich and detailed technology that can't be covered in a single article. Hopefully this article has illustrated the basic concepts and can be used to evaluate the great potential of this technology. Future articles in this series will cover a more in depth look at URL routing, posting data to a database, or processing input from a page, and unit testing.
References
Caution: This series of articles is based on an earlier release, many things have changed with the preview 2 release.
Partial methods
History