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

MVC Central launches in November

In December 2009, a new Portal I'm building will be launched.

MVC Central

It's a site that allows developers of all programming languages to learn, and offer solutions for Model/View/Controller applications.  There will be open source solutions available, tutorial articles, video tutorials, Forums and more.

The site will start out a bit lean, but hopefully with the community's help, it will grow into a one-stop shop for everything MVC.

So be sure to stop by when we open.  And if you are an MVC developer and have code snippets or some knowledge on how to solve a problem the community is struggling with, be sure to register and post your article or your video.

It's Free to register and this will allow you to upload complete applications (Kits), write articles (with code samples) and upload video tutorials.

Keep your eye on this site as we will announce when the site opens.

http://www.mvccentral.net

Thanks,

King Wilder

Currently rated 3.0 by 1 people

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

Categories: ASP.NET MVC v1.0
Posted by Admin on Sunday, October 25, 2009 3:07 AM
Permalink | Comments (0) | Post RSSRSS comment feed

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 13 people

  • Currently 4.461539/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

More Starter Kits coming soon!

I know I've been away and neglecting the blog, but I've been busy on paying jobs. 

But I'm working on a larger MVC project using the Release Candidiate...

I will be building it initially using Linq to Sql, but I plan on making an Entity Spaces version also.

Since this is a Starter Kits site, I plan on porting as many standard ASP.NET starter kits over to MVC.  If anyone wants to be a part of the process, and you would like to post your work here, contact me and I can set you up with credentials to post articles on your project.

 

Stay tuned.

 

Currently rated 5.0 by 1 people

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

Posted by Admin on Thursday, September 11, 2008 3:05 AM
Permalink | Comments (57) | Post RSSRSS comment feed

Web site Installer kit

ASP.NET web-based Site Installer


I had the need for this very thing so I thought I would write a small app that I can re-use on my many web-based applications.

Just as a side note... this is not MVC specific.  I know this is a Blog for MVC Starter Kits but this project can be used to install any web site.

What is the Web-based Site Installer?


solution explorer
This is an ASP.NET 2.0 AJAX web site application that you can plug into your web application (if it is meant to be distributed and installed by your customers) and have your customer be able to easily install the application through their browser.


solution explorer

There are more screen shots at the end of this blog post.

 

It's built on a simple ASP.NET Wizard control that gathers information needed to set up the application, usually database information, and then run several methods that do the following:

1)  Modify the web.config to add your connection string
2)  Create the database or use the one you selected
3)  Run scripts that add the database tables (of your choice) to the selected database
4)  Add the ASP.NET Membership tables to the database
5)  Creates Roles (Administrators, Users)
6)  Create an Administrator account based on user input
7)  Assigns that administrator account to the Administrators Role

When the process is completed, you can log on as the account you just created and verify that it all works!

This will work with SQL Server and SQL Server Express.  MySql is not implemented at this time.  Maybe in a future version.

Pre-requesites

1) You must have permission to the SQL Server instance you will be using
2) You must have already created the user for this setup.  In a shared web hosting situation, this would normally be handled through a control panel.  In the control panel you would setup your database and credentials and apply the necessary permissions.
3) The folder that contains the web.config (or external config file if you use the configSource attribute of the connection strings section), needs to have Read and Write permissions from NETWORK SERVICE.  Your control panel should be able to apply these permissions.  If they don't, contact your web host for help.

So assuming you have all the pre-requesites in place, let's continue.

Let's see how this works.  There are essentially 5 steps to the wizard.

1) Collect Server information - assign the server you will use, i.e.: localhost, localhost\SQLEXPRESS, sql.myserver.com, etc.
2) Choose database - you can choose from a list of available (SQL Server) databases, or enter the name of a new database
3) Enter Admin account info - enter the information for the admin account.  This will be entered into the ASP.NET 2.0 Membership tables.
4) Confirm your settings and execute the process.
5) Process completed!

So how does the installer know to go to the /install directory?

There's a small class that probes the web.config file and looks into the connectionStrings section.  If it finds this:

 

<connectionStrings>
<clear />
<add name="appConnection" connectionString="##NOT_IMPLEMENTED##" providerName="System.Data.SqlClient" />
</connectionStrings>

 

 ... then it knows that this is the first time the application is being run.

This is handled by the class, SetupUtility.  It contains a read-only property that returns a boolean value whether the connection string(s) are implemented.  It is called in the Global.asax file.

public class SetupUtility
{
// This string needs to added to the web.config/connectionStrings section on new applications.
// This is what tells the application that it's being used for the first time.
// You can put anything here, as long as it matches what is in the connectionString attribute value
// in the connectionStrings section of web.confg.
private const string NOTIMPLEMENTED = "##NOT_IMPLEMENTED##";

/// <summary>
/// Check all connection strings for ##NOT_IMPLEMENTED##. If it exists
/// in the connections strings section, it is the first time and we need
/// to redirect to the install folder.
/// </summary>
/// <returns></returns>
public static bool isFirstTime
{
get
{
bool firstTime = false;
ConnectionStringSettingsCollection connStrings = WebConfigurationManager.ConnectionStrings;
foreach (ConnectionStringSettings conn in connStrings)
{
if (conn.ConnectionString == NOTIMPLEMENTED)
{
firstTime = true;
break;
}
}
return firstTime;
}
}
}

 

It also contains a little generic helper method that can find controls within a starting control.  I use it in the AttachEventHandlerToFinishButton() method that tries to find the FinishButton in the wizInstall ASP.NET Wizard Control.

/// <summary>
/// You can return a control of a Type based on the ID and the starting Control.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="startingControl"></param>
/// <param name="id"></param>
/// <returns></returns>
public static T FindControl<T>(System.Web.UI.Control startingControl, string id) where T : System.Web.UI.Control
{
T found = null;
foreach (System.Web.UI.Control activeControl in startingControl.Controls)
{
found = activeControl as T;
if (found == null)
{
found = FindControl<T>(activeControl, id);
}
else if (string.Compare(id, found.ID, true) != 0)
{
found = null;
}
if (found != null)
{
break;
}
}
return found;
}

 

In the Global.asax file, this class is called in the Application_Start() event, and the isFirstTime property returns a Boolean value on whether the web.config connection strings section has been set.  If it still has the ##NOT_IMPLEMENTED## place holder, then it has not been set and the application redirects to the install page.

void Application_Start(object sender, EventArgs e) 
{
// Code that runs on application startup
if (SetupUtility.isFirstTime)
{
System.Web.HttpContext.Current.Response.Redirect("~/install/install.aspx", false);
}
}

 

That's what initiates the process.  Once the application re-starts, it won't go to the /install directory, or if it reads the connection strings section of the web.config file and all connection strings are set, then it will not go to the /install directory.


Install.aspx - code behind

All of the wizard processing occurs in the install.aspx code-behind.  The first few steps of the wizard collects the necessary information and the final button click runs the process.

This is the method that is called in the Finish button click event.

/// <summary>
/// Run all the functions to install the database tables and set the admin account.
/// </summary>
private void RunAll()
{
// If we are using an existing datbase, we don't need to create it.
if (!(bool)ViewState["UseExistingDb"])
{
// Create the Test database
if (!CreateDb()) throw new Exception("Error creating database!");
}

// Add the tables to the database
RunScripts(ViewState["DBName"].ToString());

// Add the ASPNETDB tables to the database using the SqlServices Install method.
// This will add the ASPNETDB tables to the same database as the application.
// NOTE: This method can ONLY be used for SQL Server. To point to MySql,
// you will need to create the database scripts for MySql and add them to
// the RunScripts method.
// Special thanks to the article by Peter Bromberg on the
// System.Web.Management.SqlServices.Install method at http://www.eggheadcafe.com/articles/20060529.asp
if (chkTrustedConnection.Checked)
{
// For SQL Server Trusted connections
System.Web.Management.SqlServices.Install(txtServerName.Text.Trim(), ViewState["DBName"].ToString(), System.Web.Management.SqlFeatures.All);
}
else
{
// For SQL Server
System.Web.Management.SqlServices.Install(txtServerName.Text.Trim(), ViewState["DbUserName"].ToString(), ViewState["DbPassword"].ToString(), ViewState["DBName"].ToString(), System.Web.Management.SqlFeatures.All);
}

// Create the Roles, Administrators, Users
if (!Roles.RoleExists(ADMINISTRATORS_ROLE)) Roles.CreateRole(ADMINISTRATORS_ROLE);
if (!Roles.RoleExists(USERS_ROLE)) Roles.CreateRole(USERS_ROLE);

// Create the Admin User
MembershipCreateStatus status = MembershipCreateStatus.UserRejected;
MembershipUser user = Membership.CreateUser(txtAdminUserName.Text.Trim(), ViewState["AdminPassword"].ToString(), txtAdminEmail.Text.Trim(),
txtSecretQuestion.Text.Trim(), txtSecretAnswer.Text.Trim(), true, out status);

// Assign the Admin user to the Administrators role
if (status == MembershipCreateStatus.Success)
{
Roles.AddUserToRole(txtAdminUserName.Text, ADMINISTRATORS_ROLE);
}

}

 

Let's go over the steps in the RunAll() method.  The first "if" statement checks whether the selected database already exists, if it does, we simply bypass this method.  Otherwise, we create the database.

The RunScripts() method takes the name of the database and runs any number of SQL scripts (that you provide) to create tables in your database, import sample data, drop tables, or whatever you need to do. 

 

/// <summary>
/// Run SQL scripts to create the tables and any other sql scripts.
/// </summary>
/// <param name="dbName"></param>
private void RunScripts(string dbName)
{
//Tables
string[] tableStatements = GetScriptStatements(File.ReadAllText(Server.MapPath(SCRIPT_TABLES), new System.Text.UTF8Encoding()));
ExecuteStatements(tableStatements, dbName);

// Add other sql statements here... such as... !!! Notice the string array variable name is unique
// and it gets passed into the ExecuteStatements method.
//string[] newStatements = GetScriptStatements(File.ReadAllText(Server.MapPath(["some other sql scripts"]), new System.Text.UTF8Encoding()));
//ExecuteStatements(newStatements, dbName);

// Add other sql statements here... such as...
//string[] moreStatements = GetScriptStatements(File.ReadAllText(Server.MapPath(["even more sql scripts"]), new System.Text.UTF8Encoding()));
//ExecuteStatements(moreStatements, dbName);
}

 

Simply by adding more lines of code as shown above, you can run as many SQL scripts as you need.

IMPORTANT!!! If you run more than one sql script, each string array variable needs to be unique!

Here are the three variables in this example:

1) string[] tableStatements = GetScriptStatements(...)
2) string[] newStatements = GetScriptStatements(...)
3) string[] moreStatements = GetScriptStatements(...)

Notice that these are all unique and they get passed into the ExecuteStatements method.  If you just repeat tableStatements, it will generate an error.

The next method call is the interesting one.  System.Web.Management.SqlServices.Install().  What is this?  It's a little known class that does essentially what "aspnet_regsql.exe" does, except you can handle these matters programmatically.  For more information about this, go here, http://msdn2.microsoft.com/en-us/library/system.web.management.sqlservices.install.aspx.

 

if (chkTrustedConnection.Checked)
{
// For SQL Server Trusted connections
System.Web.Management.SqlServices.Install(txtServerName.Text.Trim(), ViewState["DBName"].ToString(), System.Web.Management.SqlFeatures.All);
}
else
{
// For SQL Server
System.Web.Management.SqlServices.Install(txtServerName.Text.Trim(), ViewState["DbUserName"].ToString(), ViewState["DbPassword"].ToString(), ViewState["DBName"].ToString(), System.Web.Management.SqlFeatures.All);
}

 

If you didn't want to use this class, you could simply gather the SQL scripts for the Membership tables and place the sql scripts in the /install/installscripts folder and call them in the RunScripts method.  It will accomplish pretty much the same thing.

You'll notice there is one for SQL Server trusted connections, and one that uses all necessary information.  Obviously the trusted connection overload does not require credentials.

After that, we create the roles, you can add as many roles here that you need, I'm only creating the Administrators and Users roles.

 

// Create the Roles, Administrators, Users
if (!Roles.RoleExists(ADMINISTRATORS_ROLE)) Roles.CreateRole(ADMINISTRATORS_ROLE);
if (!Roles.RoleExists(USERS_ROLE)) Roles.CreateRole(USERS_ROLE);

 

Then we create the Admin user account based on the information collected in the wizard.  And then we assign the newly created admin account to the Administrators role.

 

// Create the Admin User
MembershipCreateStatus status = MembershipCreateStatus.UserRejected;
MembershipUser user = Membership.CreateUser(txtAdminUserName.Text.Trim(), ViewState["AdminPassword"].ToString(), txtAdminEmail.Text.Trim(),
txtSecretQuestion.Text.Trim(), txtSecretAnswer.Text.Trim(), true, out status);

// Assign the Admin user to the Administrators role
if (status == MembershipCreateStatus.Success)
{
Roles.AddUserToRole(txtAdminUserName.Text, ADMINISTRATORS_ROLE);
}

 

 That's it!

It works pretty nicely and I hope you get some value from it.

Ok, one more thing... How do you integrate this into your own web site application?

Simple, just do the following and in seconds you can have an installer for your application:

  1. Copy the SetupUtility.cs file to your web site App_Code folder, or if you have a separate class library project, add the class to that project.
  2. Copy the Install folder in your web site.
  3. Create whatever database table sql scripts you need to run for your application, and add them to the /install/installscripts folder.
  4. Add the if statement to the Global.asax file.
  5. Prepare your web.config/connectionStrings section and change the connectionString attribute value to ##NOT_IMPLEMENTED##.

That should do it!  This should be just a starter example for you.  You can extend this any way you deem necessary for your application.

If you have any questions or find any bugs, please post them here or in my Forum.

You can download the sample web site here: DBInstallSite.zip (22 KB)

Thank you,

King Wilder

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Currently rated 5.0 by 1 people

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

Categories: ASP.NET
Posted by Admin on Tuesday, July 08, 2008 3:28 AM
Permalink | Comments (65) | Post RSSRSS comment feed

ASP.NET MVC Northwind Demo using SubSonic - Part Tres

In Part 1, I built a simple Northwind ASP.NET MVC application using Entity Spaces and maintained the Entity Spaces references throughout the application.  This is not a loosely coupled application, but it does allow for slick and easy Entity Spaces relationship mapping in the View and in other layers of the application.  The downside is that a reference to the Entity Spaces DLL's need to be made in each layer.

In Part Deux, I refactored the application to be loosely coupled.  This helps promote a very flexible and extendable application and maintain the separation of concerns.  In other words, the View or presentation layer, has no idea what kind of data layer is sending the data.

Ok, now what?  Well I stated I was going to try and implement a version of the application for SubSonic, and here it is! 

I'm doing this to make a point.  My goal was to test whether the "loose coupling" pattern holds true and it does, apart from a few changes I needed to make because of changing from Entity Spaces to SubSonic.  These changes are not critical or life changing, but I needed to do it because of the differences in the way SubSonic handles entity naming from Entity Spaces.

And the changes just reflect that you can find out whether your application can stand up to change, by trying to change something and see how much you need to do to handle that change.  In this case, I did need to make some minor changes, but not much.  And in the long run, I feel that the application is even more flexible than version two because of the changes I've made.  I'll explain...

What I mean is this:

In the Northwind database there are table names such as:

  • Categories
  • Territories
  • Employees
  • etc...


These are all plural.  There is nothing wrong with that, and Entity Spaces maintains the naming convention.  It will remove any "underscores" or "dots" that are contained in the table name and just squeeze the words together.  But SubSonic changes the plural names to singular names.

Let me add a disclaimer here, that I am not endorsing SubSonic or Entity Spaces.  But I personally prefer Entity Spaces because it seems to me to be more straight forward in the architecture, and it has saved me hours and hours of work building my application.  I have used SubSonic a little but I am not an expert so the code I will be showing you, may or may not be the most performance enhanced version.  You may know a better way of querying and that's fine.  I just wanted to show that by simply changing the data provider in the Repository, the higher layers need not be affected.

SO WHAT DOES THE TABLE NAME HAVE TO DO WITH ANYTHING?


The reason I brought this up, is that with Entity Spaces, since it keeps the naming convention of the table names intact, I created a separate set of model classes (in the singular) for each table I needed in the application.

So if there was a "Categories" table in the database, I created a "Category" class for the model that would be sent to the View.  This helps promote "loose coupling". 

The reason this had to be address for the SubSonic version is that there was a namespace collision between the generated SubSonic classes and the Model classes.

The SubSonic classes were given the namespace as such:

namespace ESNorthwind.MVC.Data

But the model classes already had that namespace. And when SubSonic makes all plural table names singular, that's where the collision occurred.

solution explorer

 You'll see in this image, the model classes are in the Model folder, and the SubSonic classes are in the Generated folder, and they have the same names.  If they are in the same namespace, they will collide.

So what I did, was refactor a bit so that it would work for either Entity Spaces or SubSonic, or for any other ORM classes that are generated for the application.  I moved the model classes into their own namespace, ESNorthind.MVC.Data.Model.

Of course this will break in many places all over the application, so I did a quick refactor, everywhere so that the model classes now point to the new namespace.

So anywhere that a model class existed in code like this:

Product product = service.GetProductById(id);

... it was changed to this...

ESNorthwind.MVC.Data.Model.Product product = service.GetProductById(id);

I included the full namespace to the class to prevent ambiguous naming.

The View code-behind went from this:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using ESNorthwind.MVC.Data;

namespace ESNorthwind.MVC.Web.Views.Products
{
public partial class Categories : ViewPage< IList<Category> >
{
public void Page_Load()
{

}
}
}

... to this ...

 

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using ESNorthwind.MVC.Data;

namespace ESNorthwind.MVC.Web.Views.Products
{
public partial class Categories : ViewPage< IList<ESNorthwind.MVC.Data.Model.Category> >
{
public void Page_Load()
{

}
}
}

Notice the change in the ViewPage generic base class.

Now if I want to change back to Entity Spaces, I just need to modify the code in the Repository object and I'm done!

I kept the Entity Spaces code in the Repository object so you can see the similarity in their object model, but also the subtle differences.

/// <summary>
/// Get all suppliers.
/// </summary>
/// <returns></returns>
public IList<ESNorthwind.MVC.Data.Model.Supplier> GetSuppliers()
{
/********************************************************************
* * Begin Entity Spaces Code
* *****************************************************************/

//SuppliersCollection suppColl = new SuppliersCollection();
//suppColl.LoadAll();

//Supplier supplier = null;

//List<Supplier> suppList = new List<Supplier>();
//foreach (Suppliers supp in suppColl)
//{
// supplier = new Supplier();
// supplier.SupplierID = (int)supp.SupplierID;
// supplier.CompanyName = supp.CompanyName;
// suppList.Add(supplier);
//}
//return suppList;

/********************************************************************
* * End Entity Spaces Code
* *****************************************************************/

/********************************************************************
* * Begin SubSonic Code
* *****************************************************************/

SupplierCollection suppColl = new SupplierCollection();
suppColl.Load();

ESNorthwind.MVC.Data.Model.Supplier supplier = null;

List<ESNorthwind.MVC.Data.Model.Supplier> suppList = new List<ESNorthwind.MVC.Data.Model.Supplier>();
foreach (Supplier supp in suppColl)
{
supplier = new ESNorthwind.MVC.Data.Model.Supplier();
supplier.SupplierID = (int)supp.SupplierID;
supplier.CompanyName = supp.CompanyName;
suppList.Add(supplier);
}
return suppList;

/********************************************************************
* * End SubSonic Code
* *****************************************************************/
}

To those developers interested in ASP.NET MVC and SubSonic, I hope this helps. Rob Conery (the creator of SubSonic) has more information on ASP.NET MVC using SubSonic, so check out his Blog.

Thanks for watching!

You can download the file here, ESNorthwind.MVC_Pt3.zip (6.77 mb)

King Wilder.

 

 

 

Currently rated 5.0 by 2 people

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

Posted by Admin on Tuesday, June 17, 2008 6:57 AM
Permalink | Comments (48) | Post RSSRSS comment feed

ASP.NET MVC Northwind Demo using Entity Spaces - Part Deux

In Part 1 of this short series, I discussed how to build a simple ASP.NET MVC application using Entity Spaces.  If you have any Object Oriented background you will have noticed that that application was what is called, "tightly coupled".  That means that one set of objects depends on another.  This is generally a bad idea as it makes your application less flexible and extendable.

In this article, I will discuss what changes are necessary to make the application "loosely coupled", which in turn will make it easier to extend.

"Coding to an interface, rather than to an implementation, makes your software easier to extend."

 

"By encapsulating what varies, you make your application more flexible, and easier to change."

 

"The best way to get good requirements is to understand what a system is supposed to do."

 

"Great software is easy to change and extend, and does what the customer wants it to do."

 

"Analysis helps you ensure your system works in a real-world context."

 

"A loosely coupled application means that your objects are independent of each other, in other words, changes to one object don't require you to make a bunch of changes to other objects."

 

I just got finished saying that I read the Head First book on Object Oriented Analysis and Design, and one of the mantra's in it was the last quote above.  And I violated it by creating the INorthwindRepository interface as such:

 

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public interface INorthwindRepository
{
CategoriesCollection GetCategories();
ProductsCollection GetProducts();
ProductsCollection GetProductsByCategoryName(string categoryName);
Products GetProductById(int id);
SuppliersCollection GetSuppliers();
void SubmitChanges(Products product);
}
}

The CategoriesCollection is an Entity Spaces specific collection!  So right off the bat I've limited the extendability of this application to... er... Entity Spaces.  Which isn't really a limitation to me, since Entity Spaces is all I would use, but for anyone else who would want to download this application and is not an Entity Spaces user, this would be very restricting and hard to adapt.

This instantly prevents the developer from easily plugging in their own data provider and having it work. 

So to make it easier for non-Entity Spaces users, this is what I need to do:

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public interface INorthwindRepository
{
IList<Category> GetCategories();
IList<Product> GetProducts();
IList<Product> GetProductsByCategoryName(string categoryName);
Product GetProductById(int id);
IList<Supplier> GetSuppliers();
void SubmitChanges(Product product);
}
}

"Code to an interface, not an implementation".

By using IList<Category>, it now allows the application to be provider agnostic.  It could care less who the data provider is.  This did, though, require a bit more coding to make it work as it should.

For those who don't know Entity Spaces, built-into the Entity Spaces framework is the option to build all classes and maintain hierarchical relationships between database tables.  In a normal application, not a loosely coupled application, this type of framework is the developers dream.

Let's say we are returning a Products entity and we need to get the CategoryName from the Categories entity.  Using Entity Spaces we could do something like this:

Products product = new Products();
product.LoadByPrimaryKey(4);

string categoryName = product.UpToCategoriesByCategoryID.CategoryName;

Entity Spaces can easily create those relationships for the developer.  But to have a loosely coupled application, where any layer above the Data layer is NOT dependent on the data provider, this is not a good thing.

So what do we do?

When generating the Entity Spaces classes, I can just not select the option to "Generate Hierarchical Model" so each entity is independent.  So now there would be no more, UpToCategoriesByCategoryID, property to the class.  This means of course, that we need to manually create or maintain that relationship in code.

 



This isn't necessarily a bad thing, it's just a slightly more time consuming task, but in the long run it makes the application more flexible.

HOW DOES THIS IMPACT THE ENTIRE APPLICATION?

It impacts it a little, but nothing drastic, especially for an application as small as this one.  I was able to refactor the entire application in an afternoon.  But this also means that you can remove the Entity Spaces references from the Services Project since there are no dependencies to Entity Spaces in this layer.

What I did have to add were classes that would directly act as the Model:

  • Category
  • Product
  • Supplier

I built these classes manually to store the data from the repository that would then be passed to the Controller and then the View.  Notice that they are a singular, where the database tables are plural.

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public string Picture { get; set; }
}
}


using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
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 int UnitsInStock { get; set; }
public Category Category { get; set; }
public Supplier Supplier { get; set; }
}
}


using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public class Supplier
{
public int SupplierID { get; set; }
public string CompanyName { get; set; }
}
}

What else had to change?  The NorthwindRepository class had to change a bit to store the data into the model classes instead of passing those Entity Spaces object up to the view.  So I had to do some relationship mapping. 

This is the GetProductById() in part one of the application:

/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Products GetProductById(int id)
{
Products product = new Products();
product.LoadByPrimaryKey(id);
return product;
}

And here's the newly refactored method:

/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Product GetProductById(int id)
{
// Get the Products data from the database using Entity Spaces.
Products prod = new Products();
prod.LoadByPrimaryKey(id);

// Instantiate the model Product object to store the
// data from the Entity Spaces objects.
Product product = new Product();
product.ProductID = (int)prod.ProductID;
product.ProductName = prod.ProductName;
product.SupplierID = (int)prod.SupplierID;
product.CategoryID = (int)prod.CategoryID;
product.QuantityPerUnit = prod.QuantityPerUnit;
product.UnitPrice = (decimal)prod.UnitPrice;
product.UnitsInStock = (int)prod.UnitsInStock;

// Load the Entity Spaces object to get the CompanyName.
Suppliers supp = new Suppliers();
supp.LoadByPrimaryKey(product.SupplierID);

// New up a model Supplier object to store the CompanyName.
product.Supplier = new Supplier() { CompanyName = supp.CompanyName };

// Load the Entity Spaces Categories object to get the CategoryName.
Categories cat = new Categories();
cat.LoadByPrimaryKey(product.CategoryID);

// New up the model Category object to store the CategoryName.
product.Category = new Category() { CategoryName = cat.CategoryName };

// Return the model Product object.
return product;
}

You'll see that there's a bit more code, but this has to be done in order to de-couple the model from the data provider. 

Here's the SubmitChanges method in the NorthwindRepository class:

Entity Spaces specific method:

public void SubmitChanges(Products product)
{
product.Save();
}

And the loosely coupled method:

/// <summary>
/// Submit the changes to the database.
/// </summary>
/// <param name="product"></param>
public void SubmitChanges(Product product)
{
// Load the Entity Spaces Products object based
// on the ProductID and get the new values
// from the Product model passed in.
Products prod = new Products();
prod.LoadByPrimaryKey(product.ProductID);
prod.ProductName = product.ProductName;
prod.SupplierID = product.SupplierID;
prod.CategoryID = product.CategoryID;
prod.UnitPrice = product.UnitPrice;
prod.Save();
}

Again, as I stated in the first article, I'm learning this as I go and as elated as I was to have the application work where I could pass along the Entity Spaces objects to the Views, when I thought about hooking up SubSonic as another test, I quickly realize that I couldn't with the application in it's present state.  Hence the need to refactor to make the application extendable and more flexible.


WHAT ABOUT THE SERVICES LAYER?

That's what's so nice about this pattern, the Service layer was the quickest fix of them all.  I just needed to change the return object types from the Entity Spaces specific objects or collections to model references, ex: IList<model>.

This is the GetProductsByCategoryName() method originally:

/// <summary>
/// Return the ProductsCollection based on the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public ProductsCollection GetProductsByCategoryName(string categoryName)
{
ProductsCollection prodColl = _repository.GetProductsByCategoryName(categoryName);
return prodColl;
}

And this is the new method:

/// <summary>
/// Return the ProductsCollection based on the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public IList<Product> GetProductsByCategoryName(string categoryName)
{
IList<Product> prodColl = _repository.GetProductsByCategoryName(categoryName);
return prodColl;
}

The only thing changed was the return types, ProductsCollection to IList<Product>.  Now other than maybe adding a few new methods in this class or applying any business logic, this class doesn't need to be modified if we change data providers.

SO WHAT HAPPENS TO THE WEB LAYER?

Let's see how this refactoring impacted the Web site layer.

First understand that the way Entity Spaces works, a reference to Entity Spaces needs to occur in the web site, since the Loader needs to be set in the Global.asax file.

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace ESNorthwind.MVC
{
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
//routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

//routes.MapRoute(
// "Default", // Route name
// "{controller}/{action}/{id}", // URL with parameters
// new { controller = "Home", action = "Index", id = "" } // Parameter defaults
//);

routes.MapRoute("mvcroute", "{controller}/{action}/{id}"
, new { controller = "products", action = "Index", id = "" }
, new { controller = @"[^\.]*" });

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
EntitySpaces.Interfaces.esProviderFactory.Factory = new EntitySpaces.LoaderMT.esDataProviderFactory();
}
}
}

So essentially, nothing has changed in this file.

WHAT ABOUT THE CONTROLLER CLASS?


The controller came out pretty much unscathed, only a couple things needed to be changed.  Edit and Update were the two methods that changed.

Here's the original methods:

public object Edit(int id)
{

ProductsEditViewData viewData = new ProductsEditViewData();
Products product = service.GetProductById(id);
viewData.Product = product;

if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}

ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);

return View("Edit", viewData);
}

public object Update(int id)
{
Products product = service.GetProductById(id);

if (!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}

BindingHelperExtensions.UpdateFrom(product, Request.Form);
service.SubmitChanges(product);

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.UpToCategoriesByCategoryID.CategoryName }));
}

And here are the methods from the newly refactored controller class.

public object Edit(int id)
{

//ProductsEditViewData viewData = new ProductsEditViewData();
Product product = service.GetProductById(id);
//viewData.Product = product;

if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}

ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);

//return View("Edit", viewData);
return View("Edit", product);
}

public object Update(int id)
{
Product product = service.GetProductById(id);

if (!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}

BindingHelperExtensions.UpdateFrom(product, Request.Form);
service.SubmitChanges(product);

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.Category.CategoryName }));
}

Notice that in the Edit method, I moved away from the ProductsEditViewData class to store the data and simply loaded the Product model class from the service.GetProductById(id) method.  Then I passed the product model to the View directly.

In the Update method, I had to change the RedirectToRoute line where the anonymous object was called using an Entity Spaces specific hierarchical jump to the CategoryName:

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.UpToCategoriesByCategoryID.CategoryName

I simply added a property to the Product model object that returns a Category model object and then I'm good to go.

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.Category.CategoryName }));

If you remember, the Product model class contains a property called Category, which returns a Category object.

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
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 int UnitsInStock { get; set; }
public Category Category { get; set; }
public Supplier Supplier { get; set; }
}
}

This Category property is then populated way down in the NorthwindRepository class and the method, GetProductById(id).

/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Product GetProductById(int id)
{
// Get the Products data from the database using Entity Spaces.
Products prod = new Products();
prod.LoadByPrimaryKey(id);

// Instantiate the model Product object to store the
// data from the Entity Spaces objects.
Product product = new Product();
product.ProductID = (int)prod.ProductID;
product.ProductName = prod.ProductName;
product.SupplierID = (int)prod.SupplierID;
product.CategoryID = (int)prod.CategoryID;
product.QuantityPerUnit = prod.QuantityPerUnit;
product.UnitPrice = (decimal)prod.UnitPrice;
product.UnitsInStock = (int)prod.UnitsInStock;

// Load the Entity Spaces object to get the CompanyName.
Suppliers supp = new Suppliers();
supp.LoadByPrimaryKey(product.SupplierID);

// New up a model Supplier object to store the CompanyName.
product.Supplier = new Supplier() { CompanyName = supp.CompanyName };

// Load the Entity Spaces Categories object to get the CategoryName.
Categories cat = new Categories();
cat.LoadByPrimaryKey(product.CategoryID);

// New up the model Category object to store the CategoryName.
product.Category = new Category() { CategoryName = cat.CategoryName }; // Category property populated here.

// Return the model Product object.
return product;
}

Ok, so that's what I did for this refactoring of the application.  I'm going to try a couple more things to this application, again as an exercise for myself. 

One is to try and implement Extension Methods (MSDN article msdn.microsoft.com/en-us/library/bb383977.aspx  and an article by Scott Guthrie weblogs.asp.net/scottgu/archive/2007/03/13/new-orcas-language-feature-extension-methods.aspx)  which can help make the application even more flexible and try to hook up SubSonic and see if this application above the Data layer holds up without making any changes to it.  That is the real test of extendibility and flexibility.

You can download the new version here. ESNorthwind.MVC_Pt2.zip (2.37 mb)

As always, if you have any questions or comments, good or bad, primarily good, then feel free to post a comment here or in my Forums.

Thanks.

 

 

 

Currently rated 3.7 by 3 people

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

Posted by Admin on Wednesday, June 04, 2008 3:49 AM
Permalink | Comments (118) | Post RSSRSS comment feed

Looking for active members and leads

Brand New Site about MVC

This site isn't about any specific MVC application, it's about any MVC application and to make them open source and available to developers.

I'm looking for interested participants to be a lead in the ongoing development of ASP.NET MVC applications.  If you are interested in leading a project, email me and I can give you access to the site to run your project.

Thanks,

 

King Wilder 

Currently rated 4.5 by 2 people

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

Posted by Admin on Monday, June 02, 2008 9:04 AM
Permalink | Comments (36) | Post RSSRSS comment feed

ASP.NET MVC Northwind Demo using Entity Spaces - Part 1

WHAT IS THIS?

This is an article about an ASP.NET MVC application I ported to use Entity Spaces as the data access layer, or model. 

Requirements:

  • Northwind database - you need to have the Northwind database installed
  • Entity Spaces - you'll need the trial version at least, of Entity Spaces to have this application work as is.

This doesn't mean that you can't learn something if you don't use Entity Spaces.  This exercise is to demonstrate how to build an ASP.NET MVC application without using Linq.

Update 6/26/2009 - this application was built against the Preview 3 MVC Build

INSTALLATION

Installation instructions are at the end of this article.

ENTITY SPACES

Ok a brief word about Entity Spaces.  It is a really cool ORM tool but much more than that.  It reads your database and builds entity classes.  Now you might say that there are many tools that do that like NHibernate, SubSonic, etc., and you would be correct.  Entity Spaces actually costs money where SubSonic is free.  But I was using SubSonic before I trialed Entity Spaces.  After about a half a day playing with it, I purchased it and I'm going on my second year (2008) using it and I couldn't be happier.  Go to their web site to find out more.  But this article isn't about Entity Spaces, it's about building an ASP.NET MVC application without using Linq.

UPDATE to post  (6/3/2008):

Part Deux of this series will show you how to build the application to use any data layer provider instead of Entity Spaces.  Stay tuned...



SO...

I've been interested in MVC for ASP.NET ever since I heard about it about six months ago, primarily for the ease of testing and the separation of concerns.  So I decided to build (or port) an application using the ASP.NET MVC Preview 3 framework that was released at the end of May 2008.

I found a simple Northwind demo that Phil Haack put together and downloaded it so I could dissect it.  And this application like others I've downloaded all use the same data access method for their model, which is Linq, or more accurately, Linq to Sql.

Now I don't have anything against Linq, I just don't know it and I'm not sure I want to or need to.  I have Entity Spaces!  So after looking at the Northwind demo, I decided to take on the task of porting it over to use Entity Spaces to see if I have the right idea of what the model (and data access layer) does.  And to also see if I could do it.

<DISCLAIMER>
I am presenting this application to all those developers who do not presently know, or plan to know or use Linq.  I wanted to see how easy an ASP.NET MVC application can be built using a different data access provider and I built this in such a way so that developers can easily swap out my Entity Spaces provider with their own without ever touching the higher layers.

Also, I may have my head up my okole (Hawaiian for buttocks), so cut me some slack.
</DISCLAIMER>

The way I understand MVC is that the Model loosely translates to the data access layer of the application.  If I'm correct in this assumption, since no documentation seems to actually say "the Model is your data access layer", I just assume it is based on the description of what it does.  (For some reason everyone knowledgable of MVC assumes newbies will know what a Model is.  Unfortunately, I don't, or didn't.  I've never heard the term prior to hearing about MVC.  Vent!)

So based on my assumption, I rebuilt the application to not only try and integrate Entity Spaces as the Model part of this framework, but also build it using a psuedo-repository pattern in the data layer.  Let me be clear about this, I AM NO EXPERT in MVC or Design Patterns, although I did just finish the Head First Design Patterns book and I like what I read.

The repository pattern I'm following is one that is employed by Rob Conery in his new MVC Storefront real-world application.  So I jumped in the deep end and I decided to take on much more than I probably should have, but in the end it works and I was able to port the entire application in an afternoon.  (June 1, 2008 to be exact)

WHAT HAVE I DONE?

The original application was built using the default web site structure as created by Visual Studio, primarily one project.

I've built this application in four (4) projects:

  • ESNorthwind.MVC.Web - the web site, controllers, and views, no models.
  • ESNorthwind.MVC.Data - this is the Model project that contains the /Custom and /Generated folders for the Entity Spaces classes, Interfaces (INorthwindRepository) and Repository classes.
  • ESNorthwind.MVC.Services - this contains the service layer that sits on top of the Data layer and is directly called from the Controller class.  This just adds another layer of abstraction.  (Does it sound like I'm talking out of my ass yet?)
  • ESNorthwind.MVC.TestProjects - this contains unit tests that turns the only color that was available, which is green!



Also a note about NUnit, I used NUnit instead of the integrated Testing framework in Visual Studio 2008, for one specific reason... I have VS 2008 Standard which does not have the Unit Testing framework.  Ok, so I got the application for free from one of the Microsoft 2008 launches.  I can deal with that for a while.  Otherwise, I'm getting the same functionality I need for unit testing.

 



So back to my projects... I abstracted the application out like this for a few reasons:

  1. I'm a glutton for punishment
  2. I want to build a real-world MVC application using a pattern similar to this by not having the model in the web site project
  3. I want to later see if I can easily change the data store to MySql instead of SQL Server, without affecting the rest of the application

WHAT DID I CHANGE?

Most of what I had to change, besides the overall structure of the application (four projects instead of one), was just the model or probably more precisely, the data access layer of the model.  (Again, my terminology might not be correct, so try not to flame me.  Just post a nice response indicating how wrong I am.)

I also removed all using statements that pointed to System.Linq.  I wanted to make sure that this application could live on it's own without any dependencies on Linq.

ALSO, this is the change that affected the biggest impact on whether it would work or not.  I had to change every reference from Products to Product, except of course the actual entity classes created by Entity Spaces.  The original demo application had a partial class called Product that it used as the reference for the model.  But my application was to use the ES Classes as the model which has a class called Products, which I found out was ambiguous to the compiler.

I wanted to try and build the application with out using any manufactured classes that would partial-ize an entity class.

So the Controller name is called ProductController (singular) not ProductsController.  It really was a pretty easy fix to implement site-wide, so it isn't really a problem.

VIEWS

The Views for the most part are as-is.  I did have to change a few things relationship-wise to take advantage of the Entity Spaces built-in relational mapping.

This is the Edit.aspx view in the original application:

<input type="submit" value="Save" /> <%= Html.ActionLink("cancel", "List", new { id = ViewData.Model.Product.Category.CategoryName })%>

And this is the same line of code in my version:

<input type="submit" value="Save" /> <%= Html.ActionLink("cancel", "List", new { id = ViewData.Model.Product.UpToCategoriesByCategoryID.CategoryName })%>

CONTROLLER

I also tried to move all or most of the data querying out of the Controller and move it into the Model where it belongs.

Here is some ProductsController code from the original application using Linq:

public class ProductsController : Controller {
public ProductsController()
: this(new NorthwindRepository(new NorthwindDataContext()))
{ }

public ProductsController(NorthwindRepository context)
{
this.repository = context;
}

NorthwindRepository repository;

public object Index()
{
return Categories();
}

public object Categories()
{
return View("Categories", repository.Categories.ToList());
}

public object Detail(int id)
{
Product product = this.repository.Products.SingleOrDefault(p => p.ProductID == id);
return View(product);
}

public object List(string id)
{
var category = repository.Categories.SingleOrDefault(c => c.CategoryName == id);

var products = from p in repository.Products
where p.CategoryID == category.CategoryID
select p;

ViewData["Title"] = "Hello World!";
ViewData["CategoryName"] = id;

//this.ViewEngine = new MvcContrib.NHamlViewEngine.NHamlViewFactory();
return View("ListingByCategory", products.ToList());
}

public object Category(int id)
{
Category category = repository.Categories.SingleOrDefault(c => c.CategoryID == id);
return View("List", category);
}

public object Edit(int id)
{
ProductsEditViewData viewData = new ProductsEditViewData();

Product product = repository.Products.SingleOrDefault(p => p.ProductID == id);
viewData.Product = product;

if (TempData.ContainsKey("ErrorMessage")) {
foreach (var item in TempData) {
ViewData[item.Key] = item.Value;
}
}

ViewData["CategoryID"] = new SelectList(repository.Categories.ToList(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"]= new SelectList(repository.Suppliers.ToList(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);

return View("Edit", viewData);
}

public object Update(int id)
{
Product product = repository.Products.SingleOrDefault(p => p.ProductID == id);
if(!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}

BindingHelperExtensions.UpdateFrom(product, Request.Form);
repository.SubmitChanges();

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.Category.CategoryName }));
}
....

And here is my new Controller code:

public class ProductController : Controller
{
NorthwindService service;

#region Constructors

public ProductController()
{
service = new NorthwindService();
}

#endregion

#region View Methods

public object Index()
{
return Categories();
}

public object Categories()
{
return View("Categories", service.GetCategories());
}

public object List(string id)
{
ViewData["CategoryName"] = id;

return View("ListingByCategory", service.GetProductsByCategoryName(id));
}

public object Detail(int id)
{
return View("Detail", service.GetProductById(id));
}

public object Edit(int id)
{

ProductsEditViewData viewData = new ProductsEditViewData();
Products product = service.GetProductById(id);
viewData.Product = product;

if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}

ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);

return View("Edit", viewData);
}

public object Update(int id)
{
Products product = service.GetProductById(id);

if (!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}

BindingHelperExtensions.UpdateFrom(product, Request.Form);
service.SubmitChanges(product);

return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.UpToCategoriesByCategoryID.CategoryName }));
}


#endregion

...
}

Notice in my use of the Update method I have a SubmitChanges method on the service object, it's not part of Linq.  I just created it to maintain a similar feel to Linq, but I had to pass in the Products object to the method as an argument in order for it to get updated correctly.  Now bear in mind that there is most likely a better way of doing this, but this is what I did.  You can change it to anything you feel works as good or better.  Let me know what you did though, I'd be interested in hearing what you came up with.

But you'll notice that most of my service calls provide the same functionality as those from the original demo without having any querying in the controller itself.

Now as the application gets more complex, I don't know how far I would be able to take this, but at least I was able to do it this far.
   
GLOBAL ASAX

This is the original application:

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

And this is my version:

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
EntitySpaces.Interfaces.esProviderFactory.Factory = new EntitySpaces.LoaderMT.esDataProviderFactory();
}

DATABASE CHANGES

There was a small change I had to make to the Northwind database itself in order for the routing to work.  There are two categories, Grains/Cereals and Meat/Poultry.  If you know about routing, the forward slashes will present a problem, so I had to change the fields in the database to use dashes instead, Grains-Cereals, etc.  Then it works without a problem.

Other than that, no changes to the database.


SERVICE LAYER

The service layer is essentially a relay object to the data layer.  It allows for more of a separation of concerns, meaning it is easier this way, to snap-in a different provider without affecting the Web layer.

The code for the service layer is pretty straight forward:

using System;
using System.Collections.Generic;
using System.Text;
using ESNorthwind.MVC.Data;

namespace ESNorthwind.MVC.Services
{
public class NorthwindService
{
INorthwindRepository _repository = null;

#region Constructors
public NorthwindService(INorthwindRepository repository)
{
this._repository = repository;
if (_repository == null)
throw new InvalidOperationException("Repository cannot be null");
}
public NorthwindService()
{
this._repository = new NorthwindRepository();
}
#endregion

#region Public Methods

/// <summary>
/// Get all the categories.
/// </summary>
/// <returns></returns>
public CategoriesCollection GetCategories()
{
CategoriesCollection catColl = _repository.GetCategories();
return catColl;
}

/// <summary>
/// Get all products.
/// </summary>
/// <returns></returns>
public ProductsCollection GetProducts()
{
ProductsCollection prodColl = _repository.GetProducts();
return prodColl;
}

/// <summary>
/// Return the ProductsCollection based on the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public ProductsCollection GetProductsByCategoryName(string categoryName)
{
ProductsCollection prodColl = _repository.GetProductsByCategoryName(categoryName);
return prodColl;
}

/// <summary>
/// Return the product based on the product id.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Products GetProductById(int id)
{
Products product = _repository.GetProductById(id);
return product;
}

/// <summary>
/// Get all suppliers.
/// </summary>
/// <returns></returns>
public SuppliersCollection GetSuppliers()
{
return _repository.GetSuppliers();
}

public void SubmitChanges(Products product)
{
_repository.SubmitChanges(product);
}
#endregion
}
}

Another thing I wanted to see if I could do with this application, is to avoid using IQueryable, which is a Linq interface.  Knowing as little as I did (and still do) about ASP.NET MVC, I really didn't know whether I could build and MVC application without using IQueryable. 

I really wanted to use the Collection classes created by Entity Spaces instead of IQueryable, so I wrote a few tests and saw that it worked.  Needless to say I was jazzed.

DATA LAYER

The first thing I did was create an interface the repository was to implement, INorthwindRepository.

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public interface INorthwindRepository
{
CategoriesCollection GetCategories();
ProductsCollection GetProducts();
ProductsCollection GetProductsByCategoryName(string categoryName);
Products GetProductById(int id);
SuppliersCollection GetSuppliers();
void SubmitChanges(Products product);
}
}

A quick aside... I've been reading everything I can about OOP and Design Patterns, etc., to better my knowledge of making scalable and reusable applications.  I've always known about interfaces, but I have never been really clear about why they are in existence and why I should use them.

But after reading books like the Head First Design Patterns, and Head First Object Oriented Analysis and Design, I feel I have a better understanding of what their purpose in life is.

Now back to our story...

By creating a repository that implements INorthwindRepository, the service layer will accept the repository without any complaints whether it's using MySql, SQL Server, Oracle, etc.

"Code to an Interface, not an implementation!"

So the NorthwindRepository class implements this interface and does all the heavy lifting.

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public class NorthwindRepository : INorthwindRepository
{
#region INorthwindRepository Members

/// <summary>
/// Get all categories.
/// </summary>
/// <returns></returns>
public CategoriesCollection GetCategories()
{
CategoriesCollection catColl = new CategoriesCollection();
catColl.LoadAll();
return catColl;
}

/// <summary>
/// Get all products.
/// </summary>
/// <returns></returns>
public ProductsCollection GetProducts()
{
ProductsCollection prodColl = new ProductsCollection();
prodColl.LoadAll();
return prodColl;
}

/// <summary>
/// Return the ProductsCollection by the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public ProductsCollection GetProductsByCategoryName(string categoryName)
{
int catId = 0;

Categories cat = new Categories();
cat.Query.Where(cat.Query.CategoryName.Equal(categoryName));
cat.Query.Load();
catId = (int)cat.CategoryID;

ProductsCollection prodColl = new ProductsCollection();
prodColl.Query.Where(prodColl.Query.CategoryID.Equal(catId));
prodColl.Query.Load();
return prodColl;
}

/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Products GetProductById(int id)
{
Products product = new Products();
product.LoadByPrimaryKey(id);
return product;
}

/// <summary>
/// Get all suppliers.
/// </summary>
/// <returns></returns>
public SuppliersCollection GetSuppliers()
{
SuppliersCollection suppColl = new SuppliersCollection();
suppColl.LoadAll();
return suppColl;
}

public void SubmitChanges(Products product)
{
product.Save();
}

#endregion
}
}

Again, it's not all that complex since this is a simple application, but you get the idea of how this works.  This is the class that creates the concrete Entity Spaces classes that will return data to the service layer.

The only other class I have in this layer is a helper class for Views that need data from more than one entity.

using System;
using System.Collections.Generic;
using System.Text;

namespace ESNorthwind.MVC.Data
{
public class ProductsEditViewData
{
public Products Product { get; set; }
public SuppliersCollection Suppliers { get; set; }
public CategoriesCollection Categories { get; set; }
}

public class ProductsNewViewData
{
public SuppliersCollection Suppliers { get; set; }
public CategoriesCollection Categories { get; set; }
}
}

This is shoved into the code-behind of the Edit.aspx view to transport data to the view.

using System;
using System.Web;
using System.Web.Mvc;
using ESNorthwind.MVC.Data;

namespace ESNorthwind.MVC.Web.Views.Products
{
public partial class Edit : ViewPage<ProductsEditViewData>
{
}
}

In the Controller class, the Edit method uses this class that passes it along to the aspx page.

public object Edit(int id)
{

ProductsEditViewData viewData = new ProductsEditViewData();
Products product = service.GetProductById(id);
viewData.Product = product;

if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}

ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);

return View("Edit", viewData);
}

So that's about it.  Feel free to comb through the code to see what I did and remember, I don't really know what I'm doing.  So if you like this application, my name is King Wilder.  If you don't, my name is John Smith.

I've been watching every video I can on the subject from Rob Conery, Scott Guthrie and Scott Hanselman, and I'm amazed at how lost I can get watching them sometimes.  But other times I have a clarity that is overwhelming.  I'd like to think this is one of those moments.

Thanks and if you have any comments, you can post them here, or in the Forums.

INSTALLATION

Click this link to download the application. ESNorthwind.MVC.zip (2.27 mb)

For Entity Spaces Users:

  • simply unzip the application then add a reference to all the Entity Spaces DLL's to all four projects and generate the Custom and Generated classes to the Data project.  Compile.

For non-Entity Spaces users:

  • You must download the trial version of Entity Spaces and create the files necessary to run the application.  You can find these instructions on the Entity Spaces web site.  www.entityspaces.net

If you have any questions or comments you can post them here on the Blog or post your comment in the Forums, click on the Forums link at the top of this page.

Thanks.

Currently rated 4.8 by 5 people

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

Posted by Admin on Monday, June 02, 2008 8:55 AM
Permalink | Comments (269) | Post RSSRSS comment feed