MVC Starter Kits

MVC Starter Kits for ASP.NET

About the author

King Wilder, I'm an ASP.NET developer and I run and own a small web hosting company called Gizmo Beach.
E-mail me Send mail

Pages

Recent comments

Authors

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010

New Northwind Linq Project built using ASP.NET MVC 1.0

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

browser

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

solution explorer

You can see by the basic structure that I've split up the application into five (5) separate projects:

  1. Presentation Layer
  2. Business Objects
  3. Service Layer
  4. Data Layer
  5. 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

three tables

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.

data layer

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.

business objects

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.

service 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.

presentation layer

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.

add controller

And views...

add view

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.

add view2

 

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.

category index

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.

products by 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.

edit product

 

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.

product paging

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)

MVC It

 

Currently rated 4.5 by 14 people

  • Currently 4.5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: ASP.NET MVC v1.0
Posted by Admin on Sunday, April 26, 2009 11:24 AM
Permalink | Comments (60) | Post RSSRSS comment feed

Related posts

Comments

le vin us

Friday, May 08, 2009 11:24 PM

le vin

nice post! thanks for sharing

Roy Du cn

Friday, May 15, 2009 3:47 AM

Roy Du

Thanks !

but when I run the MVC,throws a exception about ' 列名 'rowversion' 无效。'

King Wilder us

Friday, May 15, 2009 3:56 AM

King Wilder

Roy,

This is explained in the paragraph toward the beginning, under the image of the three database tables. You need to add the "rowversion" column to the tables in order for this to work. Read that paragraph and it should be clear. If not, let me know what confuses you and I'll answer your questions.

Thanks,

King Wilder

King Wilder us

Friday, May 15, 2009 4:00 AM

King Wilder

Roy,

I added a title to the paragraph that talks about the Rowversion property. Take a look at that.

King

Roy Du cn

Friday, May 15, 2009 1:17 PM

Roy Du

King Wilder,

I see ,thank you!

Roy Du cn

Friday, May 15, 2009 1:49 PM

Roy Du

King Wilder,

I see that Northwind.MVC.Services is not a web service (or WCF). is it fact a facade layer now?

and I see the dataAccess respository which in Service layer is throughts as a argument to introduction,I think this is not well Coupling between every layer,i see some one use a provider factory to privoder the data Access object,why don't use like that?

King Wilder us

Friday, May 15, 2009 4:47 PM

King Wilder

Roy,

The Services layer can be considered a facade layer. I thought about making it WCF, but I thought it would make it too complicated for most users to understand to get up and running quickly.

Regarding the DataAccess Layer, I'm not sure I follow your question. If you are talking about the dependency injection in the constructor, this is a poor man's version of dependency injection but it is very acceptable. This provides good loosely-coupled layers. If the application was any larger, or had more than one possible constructor injection, then this wouldn't be the best possible solution. I talk about this in this article also. That's when I would use something like Microsoft's Unity, or StructureMap, or something like that for better control over dependency injection.

A provider factory could be used here, but I chose this method. I've found that sometimes, provider factory structures can become complicated the larger the application gets.

You are free to use whatever method you feel most comfortable with.

King

Roy Du cn

Friday, May 15, 2009 10:44 PM

Roy Du

OK,I see

ha

Roy Du cn

Friday, May 15, 2009 10:47 PM

Roy Du

Do you know what different between the ADO.NET Entity Data Model and the LINQ TO SQL? I can't found some words about it.

King Wilder us

Saturday, May 16, 2009 7:08 AM

King Wilder

Roy,

No I don't know completely what the differences are. I've heard that Microsoft is pushing Linq-to-Entities more than Linq-to-Sql these days. They are pretty similar but as far as I know there are subtle differences in the way you work with it.

There is plenty of documentation on it at http://msdn.microsoft.com/en-us/library/aa697427(VS.80).aspx.

King

Roy Du cn

Saturday, May 16, 2009 5:13 PM

Roy Du

Thanks,I havn't heard anything in China,ha

capsule us

Thursday, May 21, 2009 10:09 PM

capsule

Great article? but it incredible long, so I need more time to understand all this information.

Cervical Cancer us

Tuesday, May 26, 2009 1:49 AM

Cervical Cancer

Wow..it is a usefull hint..thanks for sharing..

yazılım gb

Sunday, May 31, 2009 1:44 AM

yazılım

Nice article

Cosmetic dentistry kent gb

Tuesday, June 02, 2009 8:38 PM

Cosmetic dentistry kent

I think asp.net is the best choice to create this projects.

tukang nggame us

Thursday, June 04, 2009 2:09 PM

tukang nggame

wow..fantastic. good job

Israel institute uz

Sunday, June 07, 2009 5:09 AM

Israel institute

great posting
we need more like u

Israel human rights vn

Sunday, June 07, 2009 5:10 AM

Israel human rights

thank u ppl
nice seeing u

robert us

Monday, June 08, 2009 8:06 PM

robert

Thanks a lot sir, very usefull script. thanks...

buying property gb

Monday, June 08, 2009 8:08 PM

buying property

Great work I like it so much.

paul us

Monday, June 08, 2009 8:10 PM

paul

thansk for ASP.NET MVC article, very interesting

steve us

Monday, June 08, 2009 8:14 PM

steve

good job, very great article. thanks..

micr toner ca

Tuesday, June 09, 2009 5:50 AM

micr toner

Very thorough! Heavy stuff so I appreciate the diagrams and coding. Thanks!

Franchises for sale us

Tuesday, June 09, 2009 8:09 AM

Franchises for sale

It's interesting, the blog engine platform seems very variable in form. My design skills are not so good as my C coding though, I would be interested in seeing what additional skins you can get for it. Nice blog btw, best wishes for it and keep up the posts. Smile Kind regards, Peter sims.

natural treatment for gout us

Tuesday, June 09, 2009 5:27 PM

natural treatment for gout

That was lots of steps! However it seems easy to understand based on the pictures given.

p shaped conservatories gb

Tuesday, June 09, 2009 11:38 PM

p shaped conservatories

The platform which you have use to build this project is great and their is no any bugs I think so.

Ahlln

Monday, June 15, 2009 2:19 PM

Ahlln

Very complete ... Great Job!

Stop Dreaming Start Action us

Thursday, June 18, 2009 4:33 AM

Stop Dreaming Start Action

Someday I will write something that I hope to be as good as your writing. You have an excellent idea and thought anyone has never had in mind. Congratulations!

Rusli Zainal Sang Visioner us

Thursday, June 18, 2009 4:35 AM

Rusli Zainal Sang Visioner

Great article. I was actually looking at entity spaces articles. Cause i wanted to test MVC with it and came across this article. I feel very lucky. Thanks for writing.

King Wilder us

Thursday, June 18, 2009 5:08 AM

King Wilder

Rusli, I hope to complete some other apps using Entity Spaces, as soon as I get some paying jobs out of the way. :^)

Stop Dreaming Start Action us

Friday, June 19, 2009 2:55 AM

Stop Dreaming Start Action

good job, very great article. thanks.

franchises for sale us

Friday, June 19, 2009 8:53 AM

franchises for sale

hmmmm i'm hungry. Ooops, focus matthew focus matthew focus. Just browsing, thinking of food and browsing. Post some more Smile MunchMunch

oz

Saturday, June 20, 2009 7:25 AM

oz

interesting explanation

Liverpool dentist gb

Sunday, June 21, 2009 10:23 PM

Liverpool dentist

I learn from you this post how to arrange the huge codes.

Internet keno sites us

Sunday, June 21, 2009 10:44 PM

Internet keno sites

Hi, interesting post. I have been wondering about this issue,so thanks for posting. I’ll likely be coming back to your blog. Keep up great writing....

Thanks.

Todde Li cn

Tuesday, June 23, 2009 9:46 PM

Todde Li

Hi King, it's really a beautiful job, and this is what I've been looking for. One quick question, how do you do Log and Audit in your architecture? Particularly how can you log any updated items when user completes his response. Any advice?
Many thanks, Todde.

Admin us

Wednesday, June 24, 2009 2:35 AM

Admin

Todde,

I'm not quite sure what you are asking, but there are different ways you can log or audit in your application. You can log to a log file (text file), or to the database.

If you want to audit, meaning trace the movement of every action, you can write a class that handle the auditing to update a log file or database, and if you have it reside in your PageBase class, then it's easy to reference it and call a method on it in every page and page method. Of course every page in your application must inherit from your PageBase class. Does that make sense?

I hope this answers your question.

King Wilder

Todde Li cn

Thursday, June 25, 2009 1:03 AM

Todde Li

Hi King, thanks for your rapid response. Actually, I mean a little further detailed. Currently we're using [Filter] to monitor any ActionResult method and keep the log. However, we're not able to log the updated items. For example, two items phone number and address on the page have been updated by the user, then how to catch up these two items and how to log the original contents and the updated items of these two items. Hope you understand better this time.
Thanks, Todde.

King Wilder us

Thursday, June 25, 2009 3:00 AM

King Wilder

Todde, it sounds like you want to log changes that have been made to database records. I'm pretty sure there isn't a built-in MVC way of doing this so you would have to build your own functionality.

One way that might be possible, thinking off the top of my head, would be when a user accesses a page and the data is retrieved to display on the page, store the data in a temporary object in a session in the controller. Then when the page is updated, capture the new values and store them both in a new database table with both the original values and the new values.

Since I don't know why you want to do this, it's hard for me to say the best way of handling this. You would probably want to include some other columns in the database table to capture more helpful info, like the date of the update, what entity this record is reflecting, who updated it, etc.

I hope this helps.

King Wilder

Todde Li cn

Thursday, June 25, 2009 11:40 PM

Todde Li

Thanks King! The reason that we do this is for tracing. We want to capture all the changes that have been made to database records so that we can compare the two versions of data. Actually, we want find out "who, where, do exact what from where" when the system was hacked or something strange happened.
Today, we've got a solution, compare the data from data entity with changed data from controller. We've been able to capture any changes in this way. I may put some key code here if you're interest.

Poker & the Law kr

Friday, June 26, 2009 3:57 PM

Poker & the Law

Nice article.The code works fine and is a good starting point for me making a custom module.Thanks for the module..

rusli zainal sang visioner us

Sunday, June 28, 2009 6:05 PM

rusli zainal sang visioner

Someday I will write something that I hope to be as good as your writing. You have an excellent idea and thought anyone has never had in mind. Congratulations!

Babu gb

Monday, June 29, 2009 1:16 AM

Babu

Hello King,

This is very good and detailed. I have a problem though. Could you please clarify this.

I am not using MVC but I am using ordinary ASP.net with UI, Business and DAL layers.

I have used your Repository as DAL and Service as Business. But I am not sure what I should do with controller Layer.

But in my page code behind in the UI, I am using like this.


ProductService pService = new ProductService(); // this is my business class and in your app this Service class

Product prod = new Product();

prod.Name = txtProdName.Text;
prod.Desc = txtProdDesc.Text;

pService.Insert(prod)

Am I doing right. Please advise me.

Cheers,
Babu.

King Wilder us

Monday, June 29, 2009 3:00 AM

King Wilder

Babu,

What you are doing now by calling the service layer in your code-behind, is ok, not perfect, but ok. This still helps promote the separation of concerns in that you aren't directly calling your DAL from your code-behind.

In my standard ASP.NET applications, I've sometimes created a Controller class (which is just a class, not inheriting from MVC ConrollerBase) just to continue a pseudo-MVC pattern. The important thing is to have your business rules, that is, a place where you validate all your business logic in one place, such as the Service layer. This way in the case of a violation, the process is stopped at the service layer and your application can immediately return a message to the UI of the violation and the DAL is never contacted.

Ultimately, I like the business objects coming into the DAL to be trusted! The DAL should be stupid, in the way that it only needs to do it's job which is processing or returning a new set of data, and not to do further validation. That should be done in the Service layer.

So there aren't any 100% rights and wrongs, but there are best practices and what you are doing is ok. As long as you have a layer that does proper validation before continuing to the DAL, then you should be fine.

I hope this helps.

King Wilder

Akulah Tukang Nggame us

Tuesday, June 30, 2009 12:31 AM

Akulah Tukang Nggame

Smart idea's. thank's

RUSLI zainal sang Visioner us

Friday, July 03, 2009 11:23 AM

RUSLI zainal sang Visioner

nice Code friend

club penguin us

Sunday, July 05, 2009 6:06 PM

club penguin

Great and inspirational stuff!
Thanks for sharing.

Online Poker us

Monday, July 06, 2009 5:14 PM

Online Poker

Wow... Great man. Well written code and the template looks good as well

life experience degrees us

Monday, July 06, 2009 9:29 PM

life experience degrees

Smart idea's. thank's

stop dreaming start action

Tuesday, July 07, 2009 11:18 PM

stop dreaming start action

Nice post

Make a Legal Will gb

Thursday, July 09, 2009 4:41 PM

Make a Legal Will

You have done a marvellous job by exploring this subject with such an honesty and depth. Thanks for sharing it with us!

goodsneeds us

Thursday, July 09, 2009 7:04 PM

goodsneeds

yes it is very nice post

tenapril us

Thursday, July 09, 2009 7:05 PM

tenapril

I think this is wonderful article

eporchshop us

Thursday, July 09, 2009 7:08 PM

eporchshop

Great man. Well written code and the template looks good as well

traveltea us

Thursday, July 09, 2009 7:08 PM

traveltea

Someday I will write something that I hope to be as good as your writing. You have an excellent idea and thought anyone has never had in mind. Congratulations!

narutoindo us

Thursday, July 09, 2009 7:09 PM

narutoindo

Thanks ever so much, very usefull article.

lirikmusik us

Thursday, July 09, 2009 7:09 PM

lirikmusik

good job, very great article. thanks.

stop dreaming start action id

Tuesday, July 14, 2009 4:09 PM

stop dreaming start action

thanks for post i really like it

web development us

Monday, July 20, 2009 12:34 AM

web development

Very nice post, really helpful
Thanks !

topsy.com

Wednesday, December 02, 2009 1:30 AM

pingback

Pingback from topsy.com

Twitter Trackbacks for

New Northwind Linq Project built using ASP.NET MVC 1.0
[mvcstarterkits.net]
on Topsy.com