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

Categories


Disclaimer

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

© Copyright 2008

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 in the beginning stages of porting the ASP.NET Personal website Starter Kit to MVC using the recently released Preview 5.  I'll be working on it on the weekends and hope to have it done in a week or so.

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.

 

Be the first to rate this post

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

Posted by Admin on Thursday, September 11, 2008 3:05 AM
Permalink | Comments (0) | 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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Be the first to rate this post

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

Categories: ASP.NET
Posted by Admin on Tuesday, July 08, 2008 3:28 AM
Permalink | Comments (2) | 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 (3) | 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.

 

 

 

Be the first to rate this post

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

Posted by Admin on Wednesday, June 04, 2008 3:49 AM
Permalink | Comments (13) | 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 (0) | 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.

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.