Ok, I'll be the first to admit that it's been a really long time since I've added a new project, but here is the latest.
The Northwind MVC Project
Here are some of the specifics about this project:
- Written using ASP.NET MVC V1.0
- n-Tier application
- Dependency Injection
- Linq-to-Sql DAL
- NUnit Unit tests
- Tests with Rhino Mocks
- Paging

This application is fairly simple to use and understand but I still wanted to implement a number of real world ideas in building it. One of which is not allowing any part of the data access layer to creep into the presentation layer. All the controllers communicate with a Service Layer and the Service Layer communicates with the DAL.
There are many out there who deliver some informative instruction on how to build an MVC application, but their examples always fall short for me since they seem like they are directed toward those who are not going to build a real-world n-Tier application. I like my examples to answer the questions that I would ask, "How does this work in an n-Tier environment?" So I end up having to build it myself.
So let's crack open this puppy and see what's inside.
Application Infrastructure

You can see by the basic structure that I've split up the application into five (5) separate projects:
- Presentation Layer
- Business Objects
- Service Layer
- Data Layer
- Unit Tests
The nice thing about MVC is the separation of concerns, meaning that it is easily testable. When I build an application I usually start from the bottom up. In this case I'll figure out what the projects requirements are, and then begin to realize it.
Requirements
- Display all the categories and allow the user to add, edit and delete a category.
- From the category listing, click a link to display all the products in that category.
- From the list of products for a category, allow the user to view details of the product and edit it.
- Display a list of all the products in a paging manner.
These are pretty simple and straight forward. It shouldn't be too much of a problem. So where do we start?
Get Started
The first thing we need to do is decide what data are we going to access, and how are we going to access it? I'm going to use the little known Northwind database. I'm going to choose three tables:
- Products
- Categories
- Suppliers

And for ease of use, I'm going to use the Linq-to-Sql classes. Now if you've read my earlier blog postings, you probably remember that I was a very knowledgable about Linq at the time I wrote those articles, nor did I think I would ever need to use it or want to use it since I normally use Entity Spaces. But for one reason or another, I've learn a lot about Linq and Linq-to-Sql and I like it a lot.
Don't get me wrong, I still love the simplicity of Entity Spaces, but for certain projects, Linq-to-Sql is just fine. But I digress.
You'll probably notice that there are a couple things that are different than your version of the Northwind database. One is the name of the Entity classes. I've renamed them by appending the word "Entity" at the end of the table name. This help discern the entity classes from the model classes. And two, the ProductEntity and the CategoryEntity have a "rowversion" property. I'll talk more about this later.
So let's access some data! This is the general layout of the Data Layer. It's neatly laid out and nicely structured for understandability.

You can see from the close-up of the Data Layer project, that I'm using a Repository pattern. In the Linq folder I've created the Linq-to-Sql classes called Northwind.dbml, and in the DataAccess folder I've created a INorthwindRepository interface and a NorthwindRepository class. This is where most of the work takes place.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Data.Linq;
namespace Northwind.MVC.Data
{
public interface INorthwindRepository
{
IList<Category> GetCategories();
Category GetCategoryById(int id);
void Update(Category category);
void Insert(Category category);
void Delete(Category category);
IList<Product> GetProducts();
IList<Product> GetPagableProducts(int startRowIndex, int maximumRows, out int totalCount);
IList<Product> GetProductsByCategoryId(int id);
IList<Product> GetProductsByCategoryName(string categoryName);
Product GetProductById(int id);
void Update(Product product);
void Insert(Product product);
IList<Supplier> GetSuppliers();
Supplier GetSupplierById(int id);
}
}
The interface contains all the methods needed to handle all necessary data access and manipulation for this project.
The concrete repository class contains the implementations that carry out the work. You'll notice in the code in the repository class that there's no model-to-entity mapping in any of the methods. I've learned a neat trick that is very helpful, but using "Entity Mapper" classes. Below is just a partial render of the repository class, but it should give you an idea of what I'm doing.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Data;
using Northwind.MVC.Data.Linq;
using Northwind.MVC.Data.EntityMappers;
namespace Northwind.MVC.Data
{
public class NorthwindRepository : INorthwindRepository
{
#region INorthwindRepository Members
public IList<Category> GetCategories()
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
IQueryable<CategoryEntity> categories = db.CategoryEntities;
return categories.Select(c => CategoryMapper.ToBusinessObject(c)).ToList();
}
}
public IList<Product> GetProducts()
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
IQueryable<ProductEntity> query = db.ProductEntities;
return query.Select(p => ProductMapper.ToBusinessObject(p)).ToList();
}
}
public IList<Product> GetProductsByCategoryName(string categoryName)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
CategoryEntity category = db.CategoryEntities.Where(cat => cat.CategoryName == categoryName).Select(c => c).SingleOrDefault();
int categoryId = category.CategoryID;
IQueryable<ProductEntity> products = db.ProductEntities.Where(p => p.CategoryID == categoryId).Select(prod => prod);
return products.Select(p => ProductMapper.ToBusinessObject(p)).ToList();
}
}
public Product GetProductById(int id)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
return ProductMapper.ToBusinessObject(db.ProductEntities
.SingleOrDefault(p => p.ProductID == id));
}
}
public IList<Supplier> GetSuppliers()
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
IQueryable<SupplierEntity> suppliers = db.SupplierEntities;
return suppliers.Select(s => SupplierMapper.ToBusinessObject(s)).ToList();
}
}
public void Update(Product product)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
ProductEntity entity = ProductMapper.ToEntity(new ProductEntity(), product);
try
{
db.ProductEntities.Attach(entity, true);
db.SubmitChanges();
}
catch (ChangeConflictException)
{
// A possible concurrency exception occurred. Let's see if
// we can resolve it.
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
conflict.Resolve(RefreshMode.KeepCurrentValues);
}
try
{
// Try saving it again.
db.SubmitChanges();
}
catch (ChangeConflictException)
{
// It didn't work, so throw a new exception.
throw new Exception("A concurrency error occurred!");
}
}
catch (Exception ex)
{
throw new Exception("There was an error saving this record! " + ex.Message);
}
}
}
public IList<Product> GetProductsByCategoryId(int id)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
CategoryEntity category = db.CategoryEntities.Where(cat => cat.CategoryID == id).Select(c => c).SingleOrDefault();
int categoryId = category.CategoryID;
IQueryable<ProductEntity> products = db.ProductEntities.Where(p => p.CategoryID == categoryId).Select(prod => prod);
return products.Select(p => ProductMapper.ToBusinessObject(p)).ToList();
}
}
I leave it up to the mapper classes to handle the mapping of entity properties to business objects and back. This way I don't have to repeat mapping code in each method in the repository class. I can just point to the mapper class and delegate the work to it. It also helps when I need to refactor the database by either adding or removing a column in the database table. All I need to do is modify the single mapper class and I'm done!
Here's an example of one of the mapper classes. It's the mapper class for the Product (model) / ProductEntity mapping. You can see it's a class with two static methods. One method maps from Linq-to-Sql entity class to the Product model class, and the other one does the opposite.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Data.Linq;
namespace Northwind.MVC.Data.EntityMappers
{
public class ProductMapper
{
public static Product ToBusinessObject(ProductEntity entity)
{
return new Product
{
ProductID = entity.ProductID,
ProductName = entity.ProductName,
CategoryID = entity.CategoryID,
SupplierID = entity.SupplierID,
QuantityPerUnit = entity.QuantityPerUnit,
UnitPrice = entity.UnitPrice,
UnitsInStock = entity.UnitsInStock,
Category = new Category(){ CategoryID = (int)entity.CategoryID, CategoryName = entity.CategoryEntity.CategoryName, Description = entity.CategoryEntity.Description},
Supplier = new Supplier(){ SupplierID = (int)entity.SupplierID, CompanyName = entity.SupplierEntity.CompanyName },
rowversion = VersionConverter.ToString(entity.rowversion)
};
}
public static ProductEntity ToEntity(ProductEntity entity, Product model)
{
entity.ProductName = model.ProductName;
entity.ProductID = model.ProductID;
entity.CategoryID = model.CategoryID;
entity.SupplierID = model.SupplierID;
entity.UnitPrice = model.UnitPrice;
entity.QuantityPerUnit = model.QuantityPerUnit;
entity.UnitsInStock = model.UnitsInStock;
entity.rowversion = VersionConverter.ToBinary(model.rowversion);
return entity;
}
}
}
What is this "rowversion" property?
If you've been paying attention, you should be wondering right now, "what is the rowversion property?" If you look in your own copy of the Northwind database, you'll notice that there is no "rowversion" column in either the Products or Categories tables. I added these, and if you want to run this application against your own Northwind database, you'll have to add these columns also. Here's why...
The "rowversion" column is used for concurrency, meaning that Linq uses it to check whether the data has been altered since you've requested it. You might be saying that you've never had to use it before, and you would be correct. But you were most likely not using Linq-to-Sql in an n-Tier fashion.
When you abstract out the repository like I've done here, where the Data Layer is a separate project, then the DataContext isn't called directly from the Presentation or Service Layer. The data is passed around via BusinessObjects. When you Update or Insert or Delete data this way, you need to Attach a new entity object (which is created by the Mapper Classes) back to the DataContext. Since this new entity class is disconnected from the DataContext, the DataContext has no idea of its relationship to the existing record in the database or if it's been modified by someone else.
When you attach the new entity, the DataContext compares the rowversion which contains the timestamp when it was called, to the existing timestamp for this record in the database. If they are equal, then it can complete the transaction without a problem. Otherwise, the data has been changed since it was called by this context and there is a concurrency issue that needs to be resolved.
Without the "rowversion" column in the database, using the abstracted Data Layer this way would never work, since the DataContext would have no way of comparing the records for concurrency. I hope that helps.
The VersionConverter Class
Here's a quick glance at the VersionConverter class. It simply helps to convert the entity binary type to string for the model, and the other method converts it back.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
namespace Northwind.MVC.Data
{
public static class VersionConverter
{
public static string ToString(Binary input)
{
if (input == null) return null;
return Convert.ToBase64String(input.ToArray());
}
public static Binary ToBinary(string input)
{
if (string.IsNullOrEmpty(input)) return null;
return new Binary(Convert.FromBase64String(input));
}
}
}
Look at this code block that shows the Update method in the repository class.
public void Update(Product product)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
ProductEntity entity = ProductMapper.ToEntity(new ProductEntity(), product);
try
{
db.ProductEntities.Attach(entity, true);
db.SubmitChanges();
}
catch (ChangeConflictException)
{
// A possible concurrency exception occurred. Let's see if
// we can resolve it.
foreach (ObjectChangeConflict conflict in db.ChangeConflicts)
{
conflict.Resolve(RefreshMode.KeepCurrentValues);
}
try
{
// Try saving it again.
db.SubmitChanges();
}
catch (ChangeConflictException)
{
// It didn't work, so throw a new exception.
throw new Exception("A concurrency error occurred!");
}
}
catch (Exception ex)
{
throw new Exception("There was an error saving this record! " + ex.Message);
}
}
}
If this line looks strange to you...
db.ProductEntities.Attach(entity, true);
... then that means you haven't been using Linq-to-Sql in an n-Tier application. You've probably been accessing the DataContext from the controller (in MVC) or the code-behind in ASP.NET.
By building the application this way, by mapping entity classes to business objects (the model), you loosely couple your application and allow it to change easily. Other layers are not dependant on the data access layer. All you need to do it shuttle the model around and everything is great. This is where the "db.ProductEntities.Attach(entity, true)" feature comes into play.
When you access your Linq-to-Sql DataContext in your presentation layer, there is no need for this, because you are always working with the data context entities themselves. But once you de-couple them from the rest of the application, you need to attach entities back into the context in order to do Inserts, Updates and Deletes.
So the Update method above first maps the incoming model (Product business object) to the Linq-to-Sql ProductEntity. Then it gets attached to the ProductEntities class to get ready for the Update. Then the normal db.SubmitChanges() method is called and everything is hunky dorey!
You've probably noticed that there's lots more code in this method than the other "Get" methods. That's because we are checking for an concurrency issues. We do this by catching the ChangeConflictException, and trying to resolve it. We loop through any conflicts and then give the db.SubmitChanges another go. If it works, then great, and we're saved! If not, then we throw our hands up and look for another line of work.
What about tests?
At this point, you can start some tests. I've integrated NUnit into this project since I have VS 2008 Standard which doesn't have the built-in debugger and this way anyone can run the tests. Here are some of the Product tests.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Linq;
using System.Web.Mvc;
using NUnit.Framework;
using Rhino.Mocks;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Data;
using Northwind.MVC.Data.Linq;
using Northwind.MVC.Services;
using Northwind.MVC.Controllers;
namespace Northwind.MVC.UnitTests.ProductTests
{
[TestFixture]
public class ProductTests
{
IList<Product> productList = new List<Product>();
IList<Category> categoryList = new List<Category>();
IList<Supplier> supplierList = new List<Supplier>();
[TestFixtureSetUp]
public void FixtureSetup()
{
productList.Add(new Product() { ProductID = 1, ProductName = "Product 1", CategoryID = 1, SupplierID = 10, UnitPrice = 9.95m });
productList.Add(new Product() { ProductID = 2, ProductName = "Product 2", CategoryID = 2, SupplierID = 11, UnitPrice = 19.95m });
productList.Add(new Product() { ProductID = 3, ProductName = "Product 3", CategoryID = 3, SupplierID = 12, UnitPrice = 29.95m });
productList.Add(new Product() { ProductID = 4, ProductName = "Product 4", CategoryID = 3, SupplierID = 12, UnitPrice = 129.95m });
productList.Add(new Product() { ProductID = 5, ProductName = "Product 5", CategoryID = 4, SupplierID = 13, UnitPrice = 292.95m });
productList.Add(new Product() { ProductID = 6, ProductName = "Product 6", CategoryID = 5, SupplierID = 13, UnitPrice = 39.95m });
productList.Add(new Product() { ProductID = 7, ProductName = "Product 7", CategoryID = 5, SupplierID = 12, UnitPrice = 24.95m });
productList.Add(new Product() { ProductID = 8, ProductName = "Product 8", CategoryID = 4, SupplierID = 12, UnitPrice = 14.95m });
productList.Add(new Product() { ProductID = 9, ProductName = "Product 9", CategoryID = 3, SupplierID = 12, UnitPrice = 18.95m });
categoryList.Add(new Category() { CategoryID = 1, CategoryName = "Category 1", Description = " Category Description 1" });
categoryList.Add(new Category() { CategoryID = 2, CategoryName = "Category 2", Description = " Category Description 2" });
categoryList.Add(new Category() { CategoryID = 3, CategoryName = "Category 3", Description = " Category Description 3" });
categoryList.Add(new Category() { CategoryID = 4, CategoryName = "Category 4", Description = " Category Description 4" });
supplierList.Add(new Supplier() { SupplierID = 10, CompanyName = "Supplier 10" });
supplierList.Add(new Supplier() { SupplierID = 11, CompanyName = "Supplier 11" });
supplierList.Add(new Supplier() { SupplierID = 12, CompanyName = "Supplier 12" });
supplierList.Add(new Supplier() { SupplierID = 13, CompanyName = "Supplier 13" });
}
[Test]
public void GetProducts()
{
var mock = MockRepository.GenerateMock<INorthwindRepository>();
mock.Expect(p => p.GetProducts()).Return(productList);
IList<Product> prodList = mock.GetProducts();
Assert.AreEqual(9, prodList.Count);
Assert.AreEqual(19.95m, prodList[1].UnitPrice);
Assert.AreEqual("Product 3", prodList[2].ProductName);
}
[Test]
public void GetProductsByCategoryName()
{
string categoryName = "Category 3";
Category category = (from c in categoryList.Where(cat => cat.CategoryName == categoryName) select c).SingleOrDefault();
Assert.AreEqual("Category 3", category.CategoryName);
int categoryId = category.CategoryID;
Assert.AreEqual(3, categoryId);
IList<Product> prodList = (from p in productList.Where(prod => prod.CategoryID == categoryId) select p).ToList();
Assert.AreEqual(3, prodList.Count);
}
[Test]
public void GetProductById()
{
int productId = 5;
Product product = (from p in productList.Where(prod => prod.ProductID == productId) select p).SingleOrDefault();
Assert.AreEqual(5, product.ProductID);
Assert.AreEqual("Product 5", product.ProductName);
Assert.AreEqual(4, product.CategoryID);
Assert.AreEqual(13, product.SupplierID);
Assert.AreEqual(292.95m, product.UnitPrice);
}
I wrote a test for each method in the INorthwindRepository interface. I wrote a few additional tests, but this project is fairly simple and I didn't need tests any more elaborate than this at the moment.
Most of the test use Rhino Mocks for testing, but some actually call the DataContext to return real data just to make sure everything returns as it should.
Business Objects
Before I go too far, I want to jump ahead a little to the Business Objects that are used to transfer data throughout the application.

This also is fairly simple. I single class for each database table in the repository. Plus I've included a helper class, "ProductsViewData", that will combine data for certain views.
Let's take a look at the Product class.
using System;
using System.Collections.Generic;
using System.Text;
namespace Northwind.MVC.BusinessObjects
{
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int? SupplierID { get; set; }
public int? CategoryID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal? UnitPrice { get; set; }
public short? UnitsInStock { get; set; }
public Category Category { get; set; }
public Supplier Supplier { get; set; }
public string rowversion { get; set; }
}
}
Nothing earth shattering here. I just holds data from the entity up to the View, and back. You'll notice there are two extra properties, "Category" and "Supplier". These are simply properties that reference the other business object classes so the Product class can bring along associated data from those classes. Here are both the Category and the Supplier classes.
using System;
using System.Collections.Generic;
using System.Text;
namespace Northwind.MVC.BusinessObjects
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public string Picture { get; set; }
public string rowversion { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace Northwind.MVC.BusinessObjects
{
public class Supplier
{
public int SupplierID { get; set; }
public string CompanyName { get; set; }
}
}
And here's the ProductsViewData class.
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Mvc;
namespace Northwind.MVC.BusinessObjects
{
public class ProductsEditViewData
{
public Product Product { get; set; }
public SelectList Suppliers { get; set; }
public SelectList Categories { get; set; }
}
public class ProductsNewViewData
{
public IList<Supplier> Suppliers { get; set; }
public IList<Category> Categories { get; set; }
}
}
You may be wondering what the "SelectList" class is. This is a built-in MVC helper class to be used with DropDown lists on the view. I'll show how this is used later.
Now that the model is built and the data access layer is built, I can move onto building the Service Layer.
Service Layer
This is mostly a transport layer that allows data to move back and forth from the presentation layer and the data access layer.

Below is just part of the Service layer methods, but they are the same as are in the repository class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Data;
using Northwind.MVC.Data.Linq;
namespace Northwind.MVC.Services
{
public class NorthwindService : INorthwindService
{
private INorthwindRepository _repository;
#region ctors
public NorthwindService()
: this(new NorthwindRepository())
{
}
public NorthwindService(INorthwindRepository repository)
{
this._repository = repository;
}
#endregion
#region INorthwindService Members
public IList<Category> GetCategories()
{
return _repository.GetCategories();
}
public IList<Product> GetProducts()
{
return _repository.GetProducts();
}
public IList<Product> GetProductsByCategoryName(string categoryName)
{
return _repository.GetProductsByCategoryName(categoryName);
}
public Product GetProductById(int id)
{
return _repository.GetProductById(id);
}
public IList<Supplier> GetSuppliers()
{
return _repository.GetSuppliers();
}
public void Update(Product product)
{
_repository.Update(product);
}
public IList<Product> GetProductsByCategoryId(int id)
{
return _repository.GetProductsByCategoryId(id);
}
#endregion
You'll notice the constructors use a pseudo-constructor injection pattern, or Dependency Injection. This is fine for use in a small application, but maintaining all the possible different constructor injected classes on a larger project could be a bit overwhelming. In that case it's better to use any one of the freely available Dependency Injection frameworks, such as Unity, Ninject, AutoFac, Spring.Net, etc.
From here, next stop is the Presentation Layer,
Presentation Layer
The nice thing about this overall project framework is that I could have an ASP.NET MVC application, or a WPF application or a normal ASP.NET application. It really doesn't matter.

With this latest release of the ASP.NET MVC framework, it's really easy to create controllers and views. By right-clicking in the Solution Explorer on the Controllers folder, you can select to add a new Controller and it shows you the possibilities for creating a new controller.

And views...

If you check the "Create a strongly typed view" checkbox, you will see a list of the classes available that you can base your View against. This means it will create a view with labels or text boxes created based on the properties of the selected business object class.

But as they say, "the controller is King!" So let's take a look at the controller and see what's happening.
Category Controller
The category controller handles a lot less work than the Product controller, but it's no less important. It contains all the basic CRUD methods.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using Northwind.MVC.BusinessObjects;
using Northwind.MVC.Services;
namespace Northwind.MVC.Controllers
{
public class CategoryController : Controller
{
private INorthwindService _service;
#region ctors
public CategoryController()
: this(new NorthwindService())
{
}
public CategoryController(INorthwindService service)
{
this._service = service;
}
#endregion
//
// GET: /Category/
public ActionResult Index()
{
return View("Index", _service.GetCategories());
}
public ActionResult Details(int id)
{
return RedirectToRoute(new { controller="Product", action = "List", id = id });
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
Category category = _service.GetCategoryById(id);
return View(category);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection)
{
Category category = _service.GetCategoryById(id);
try
{
UpdateModel(category);
_service.Update(category);
return RedirectToAction("Index");
}
catch (Exception ex)
{
ModelState.AddModelError("Category", ex.Message);
return View(category);
}
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Create()
{
Category category = new Category();
return View(category);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
{
Category category = new Category();
try
{
UpdateModel(category);
if (category.CategoryName == string.Empty)
this.ModelState.AddModelError("CategoryName", "The Category name cannot be empty!");
if (!this.ModelState.IsValid)
throw new InvalidOperationException();
_service.Insert(category);
return RedirectToAction("Index");
}
catch (Exception ex)
{
ModelState.AddModelError("Category", ex.Message);
return View(category);
}
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Delete(int id)
{
Category category = _service.GetCategoryById(id);
return View(category);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmDeleteButton)
{
Category category = _service.GetCategoryById(id);
try
{
_service.Delete(category);
return RedirectToAction("Index");
}
catch (Exception)
{
throw;
}
}
}
}
This too has Dependency Injection in the constructors so it can communicate with the Service Layer. And you'll notice this class makes use of the [AcceptVerbs] method attributes. This helps identify which actions will perform what work.
"Get" verbs are used when the controller is first called and the method is not expecting any real data. "Post" verbs are used when the controller action IS expecting data from a form collection. A good example of this is the Delete action methods. The "Get" action method is the one that is called to display the Delete Category page for confirmation. The "Post" action method is called when the "Delete" button is clicked on that Delete page. It carries out the deletion process, then it redirects you to the Index action which will display the remaining Categories.
With only some slight modification, this is the Category Index view.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<Northwind.MVC.BusinessObjects.Category>>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Categories
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Categories</h2>
<table cellpadding="3" cellspacing="3">
<tr>
<th></th>
<th>
CategoryName
</th>
<th>
Description
</th>
<th></th>
</tr>
<% foreach (var item in Model) { %>
<tr>
<td>
<%= Html.ActionLink("Edit", "Edit", new { id = item.CategoryID }) %> |
<%= Html.ActionLink("View Products", "Details", new { id = item.CategoryID })%>
</td>
<td>
<%= Html.Encode(item.CategoryName) %>
</td>
<td>
<%= Html.Encode(item.Description) %>
</td>
<td>
<%= Html.ActionLink("Delete", "Delete", new { id = item.CategoryID}) %>
</td>
</tr>
<% } %>
</table>
<p>
<%= Html.ActionLink("Add New Category", "Create") %>
</p>
</asp:Content>
It renders a page that looks like this.

Let's take a look at some Product views.
Product View
If we click on the "View Products" link, it will take us to a page that contains a list of all the Products in the selected Category.

Again, nothing earth shattering here. The images are kind of a hack. Each image's filename corresponds to the product id, so for ProductID 24, we have 24.jpg. You can display images in a different manner but these were already available from Phil Haack's original example (which this project is very loosely based on).
Here's the ProductController Index action that produces this page.
public ActionResult List(int id)
{
Category category = _service.GetCategoryById(id);
ViewData["CategoryName"] = category.CategoryName;
IList<Product> products = _service.GetProductsByCategoryId(id);
return View(products);
}
The "id" parameter of the List action is the CategoryID from the "View Products" link on the index view in this format, "Category/Details/2". Then the GetCategoryById service method is called with the category id and returns a Category model class. I retrieve the name of the Category so I can display it in the View.
Then I call the GetProductsByCategoryId Service method to retrieve a list of all the Products for the selected Category. Then I return it to the View.
Here's the Repository code for the GetCategoryById.
public Category GetCategoryById(int id)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
return CategoryMapper.ToBusinessObject(db.CategoryEntities.SingleOrDefault(c => c.CategoryID == id));
}
}
And here is the GetProductsByCategoryID repository method.
public IList<Product> GetProductsByCategoryId(int id)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
CategoryEntity category = db.CategoryEntities.Where(cat => cat.CategoryID == id).Select(c => c).SingleOrDefault();
int categoryId = category.CategoryID;
IQueryable<ProductEntity> products = db.ProductEntities.Where(p => p.CategoryID == categoryId).Select(prod => prod);
return products.Select(p => ProductMapper.ToBusinessObject(p)).ToList();
}
}
Are you starting to see how sweet the use of the Entity Mappers makes the code. Remember, if I need to modify any of the columns in the database, THIS CODE DOESN'T CHANGE! The entity mapper classes handle all the mapping of entity properties to model classes. Now that's a real time saver. It helps refactoring immensely! Let's continue...
If we click on the "Edit" link, we'll see how the "SelectList' classes come into play.

Here's the View code that renders this.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Northwind.MVC.BusinessObjects.ProductsEditViewData>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit Product
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Edit Product</h2>
<%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
<% using (Html.BeginForm()) {%>
<fieldset>
<legend>Product</legend>
<p>
<label for="ProductName">Product Name:</label>
<%= Html.TextBox("ProductName", Model.Product.ProductName) %>
<%= Html.ValidationMessage("ProductName", "*") %>
</p>
<p>
<label for="SupplierID">Supplier:</label>
<%= Html.DropDownList("SupplierID", Model.Suppliers) %>
</p>
<p>
<label for="CategoryID">Category:</label>
<%= Html.DropDownList("CategoryID", Model.Categories) %>
</p>
<p>
<label for="QuantityPerUnit">Quantity Per Unit:</label>
<%= Html.TextBox("QuantityPerUnit", Model.Product.QuantityPerUnit) %>
<%= Html.ValidationMessage("QuantityPerUnit", "*") %>
</p>
<p>
<label for="UnitPrice">Unit Price:</label>
<%= Html.TextBox("UnitPrice", String.Format("{0:F}", Model.Product.UnitPrice)) %>
<%= Html.ValidationMessage("UnitPrice", "*") %>
</p>
<p>
<label for="UnitsInStock">Units In Stock:</label>
<%= Html.TextBox("UnitsInStock", Model.Product.UnitsInStock) %>
<%= Html.ValidationMessage("UnitsInStock") %>
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
<div>
<%=Html.ActionLink("Back to List", "List", new { id = Model.Product.CategoryID })%>
</div>
</asp:Content>
Here's the ProductController Edit ("Get") action to render this view.
// GET: /Product/Edit/5
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
Product product = _service.GetProductById(id);
IList<Category> categories = _service.GetCategories();
IList<Supplier> suppliers = _service.GetSuppliers();
ProductsEditViewData viewData = new ProductsEditViewData();
viewData.Product = product;
viewData.Suppliers = new SelectList(suppliers, "SupplierID", "CompanyName", product.SupplierID.ToString());
viewData.Categories = new SelectList(categories, "CategoryID", "CategoryName", product.CategoryID.ToString());
return View(viewData);
}
The Edit action method takes a Product ID as the single method parameter. We use this to get the Product model, this has all the information we need for the view. But this view has two DropDown lists that need to be fully populated with all the Categories and all the Suppliers. So we return a list of both the Categories and the Suppliers and store them in some local variables.
Then we new up the ProductsEditViewData class. If you remember, this was from the ProductsViewData class in the BusinessObjects project. You can add more of these types of classes that help pass any type of data to the view.
This class has three properties, a Product property, and two SelectList properties. These properties will be used to populate the DropDown lists. We set a new SelectList class to the Suppliers property and pass in the suppliers list, set the value and text settings and the selected value. To make sure this works, the name of the Html helper needs to be the name of the value of the select list. For example, you'll notice the name of the Supplier DropDown list is "SupplierID", not something like "Suppliers". It needs to be the name of the "Value" of the select list, other wise the list will simply display all the records, but no selected item will be selected. I hope that was clear.
Paging
One last thing I'd like to talk about it paging of data. Unless you have some cool tools like Telerik controls for MVC, you'll have to figure out other ways of paging through data. I've built an example that expands on the paging model used in the NerdDinner application.

Clicking on the "Previous" and "Next" links page you through the data 10 rows at a time. You might want to allow the number of rows to be in a DropDown list and enhance the paging navigation with "jump to page" links, but I've kept it relatively simple for this application.
Here's how it works. First I want to modify my Routes to handle the paging.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"ProductPaging",
"Product/Page/{page}",
new { controller = "Product", action = "Index" }
);
routes.MapRoute(
"mvcroute",
"{controller}/{action}/{id}",
new { controller = "Category", action = "Index", id = "" },
new { controller = @"[^\.]*" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
The first route named "ProductPaging" will be called when the "Previous" or "Next" links are clicked. Here's the View code that renders this page.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Northwind.MVC.Helpers.PaginatedList<Northwind.MVC.BusinessObjects.Product>>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
All Products
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>All Products</h2>
<table>
<tr>
<th></th>
<th>
ProductName
</th>
<th>
Category
</th>
<th>
Supplier
</th>
<th>
UnitPrice
</th>
<th>
UnitsInStock
</th>
</tr>
<% foreach (var item in Model) { %>
<tr>
<td>
<%= Html.ActionLink("Edit", "Edit", new { id = item.ProductID }) %> |
<%= Html.ActionLink("Details", "Details", new { id=item.ProductID })%>
</td>
<td>
<%= Html.Encode(item.ProductName) %>
</td>
<td>
<%= Html.Encode(item.Category.CategoryName) %>
</td>
<td>
<%= Html.Encode(item.Supplier.CompanyName) %>
</td>
<td>
<%= Html.Encode(String.Format("{0:C}", item.UnitPrice)) %>
</td>
<td>
<%= Html.Encode(item.UnitsInStock) %>
</td>
</tr>
<% } %>
</table>
<br />
<div class="pagination">
<% if (Model.HasPreviousPage)
{ %>
<%= Html.RouteLink("Previous", "ProductPaging", new { page = (Model.PageIndex - 1) })%>
<% }
else
{%>
Previous
<%} %>
<% if (Model.HasNextPage)
{ %>
<%= Html.RouteLink("Next", "ProductPaging", new { page = (Model.PageIndex + 1) })%>
<%}
else
{ %>
Next
<%} %>
Page <%= (Model.PageIndex + 1).ToString() %> of <%= Model.TotalPages.ToString() %>
</div>
</asp:Content>
Let's analyze this first. You'll notice the Html helper method "RouteLink". This is similar to the "ActionLink" helper method for a normal link, but it points back to a specific Route we've created. Remember the "ProductPaging" Route in the Global.asax? This is where it gets called. This particular overload of the RouteLink method says, call this link "Previous", but only use the Route named as "ProductPaging", and I'm passing into the "page" variable, the next index.
So the generated HTML source for the paging links look like this:
<div class="pagination">
<a href="/Product/Page/5">Previous</a>
<a href="/Product/Page/7">Next</a>
Page 7 of 8
</div>
Let's see how this works. Let's take a look at the first line of code, the Page register declaration.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<Northwind.MVC.Helpers.PaginatedList<Northwind.MVC.BusinessObjects.Product>>" %>
You'll notice that the ViewPage generic class contains a reference to "Northwind.MVC.Helpers.PaginatedList<Northwind.MVC.BusinessObjects.Product>> class. It's this PaginatedList class that manages the paging, along with the repository.
The difference between my PaginatedList class and the one in the NerdDinner application, is they used the Linq-to-Sql DataContext in the controller. Mine is separated out in a Data Layer so I had to go about moving the data back and forth appropriately.
Here's the PaginatedList class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Northwind.MVC.Helpers
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IList<T> source, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = count;
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
//this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
this.AddRange(source);
}
public bool HasPreviousPage
{
get { return (PageIndex > 0); }
}
public bool HasNextPage
{
get { return (PageIndex+1 < TotalPages); }
}
}
}
You'll see that it's just a class that does some calculations in order to return the correct set of data. In the NerdDinner application this class did the processing of the data. The IList<T> source parameter was IQueryable<T> source, that's why there's the commented line of code for the AddRange method. I didn't want to bring the DataContext up to the presentation layer so I had to refactor this a little.
I'll describe it a little more, but first let's take a look at the Index action of the ProductController that handles the paging.
public ActionResult Index(int? page)
{
int pageSize = 10;
int totalCount = 0;
var products = _service.GetPagableProducts(page ?? 0, pageSize, out totalCount);
var paginateProducts = new PaginatedList<Product>(products, totalCount, page ?? 0, pageSize);
return View(paginateProducts);
}
The Index action method takes a single nullable int as a parameter and it will be the page index. Then I created a GetPagableProducts repository method that actually handles the paging calculation and then returns just the set of data needed. Here's the GetPagableProducts() method.
public IList<Product> GetPagableProducts(int startRowIndex, int maximumRows, out int totalCount)
{
using (NorthwindDataContext db = new NorthwindDataContext())
{
IQueryable<ProductEntity> query = db.ProductEntities;
totalCount = query.Count();
query = query.Skip(startRowIndex).Take(maximumRows);
return query.Select(p => ProductMapper.ToBusinessObject(p)).ToList();
}
}
It first returns all the Products with db.ProductEntities, then we get the number of records with "query.Count()". The third line is the beauty of the paging. It uses the Skip() and Take() extension methods to Skip to a specific record in the set, then Takes just the set of records we want. The last line simply maps the entity records to the model and returns it as a list.
I should also point out that the totalCount needs to be returned to the controller, and the easiest, most convenient way of doing this is to store it in a variable with the "out" indicator. This tells the method that the variable can be read when the method returns a value. So we pass all this information to the PaginatedList class to manage the properties that will be used by the View, such as the "HasPreviousPage" and "HasNextPage" properties and other properties that help determine what page to return out of how many total records.
That's It!
Well I don't know about you, but I'm tired!
It took almost as long to complete this blog as it did to write the application. I'm pooped! But I hope this helps in your adventures with ASP.NET MVC. You can click the link below to download the entire source code solution.
Requirements:
- VS 2008 SP1
- ASP.NET MVC V1.0
- Northwind database (I do not include it with this application)
- Rhino Mocks (optional)
Download the solution and check it out. It's got lots of helpful hints and some best practices. I hope this helps.
Northwind.MVC.zip (1.76 mb)
