翻译:ASP.NET MVC Example Application over Northwind with Entity Framework

首先,谢谢TerryLee推荐系列,本文就是在他的推荐系列看到的.
原文地址:ASP.NET MVC Example Application over Northwind with the Entity Framework
by  BradA

在上个月的假期,Lance Olson和我花了一些时间使用Entity Framework来移植ScottGu's 优秀的 MVC 实例.我想在这里分享我的成果并能得到你的观点和反馈.

对于这个例子,你将需要:

如果你是喜欢先吃甜品的类型,你可以跳过并 下载完整的示例代码

开始

File/New Project - 选择 ASP.NET MVC Web Application and Test

image_2

这创建了一个我们可以使用单元测试(unit testing)的WEB应用程序(web application)的项目.这两个项目是我们继续往下的原料.

创建Routes

ASP.NET MVC 带来的一个强大的新特性是能够自定义URL来访问你的应用程序.URL routing特性打破了URL和磁盘上物理文件之间的连接,而URL被用来访问一个约定的功能块.这对搜索引擎优化(SEO)很重要,对于一般的实用性网站也一样.例如,访问http://localhost/Products/ItemDetails.aspx?item=42的时候,你现在可以通过一个非常标致的URL来访问,就如http://localhost/Products/CodFishOil

这是通过在MVC程序的global.asax文件里面创建路由表来实现的.幸运地,默认的(路由表)已经包含在模板中,并在程序中完美的工作.

RouteTable.Routes.Add(new Route
{
    Url 
= "[controller]/[action]/[id]",
    Defaults 
= new { action = "Index", id = (string)null },
    RouteHandler 
= typeof(MvcRouteHandler)
});

这段代码阐明了我们希望在我们的网站使用的URL格式.特别的,一个URL格式为

http://localhost/Products/Details/CodFishOil

将会定位到ProductsController类(注意到我们添加"Controller"后缀到类名中,使这些类在对象模型设计的时候显得突出),而Action是这个类中被命名为Details的一个方法,最后Details方法的参数是CodFishOil.

它当然也可以有其他的格式,通过简单地改变URL模式字符串的正则表达式.

创建 Model

Model代表了你打算在程序中使用的数据.而我们的情况,最好就是开始程序的代码.

复制Northwind.mdf 文件到MVC程序的App_Data目录中.Northwind可能就是最普遍的SQL示例数据库.你可以从这里下载,或者只是原始文件,可以从这里下载

image_4

接下来,我们需要在NorthWind上创建一个LINQ Model来使它更容易工作.你可以使用NHibernate, LinqToSql, Entity Framework 来做这个,或者其他的.NET OR-Mapping 技术.只要它是.NET的对象,ASP.NET MVC框架能用它来工作.在这里我将会使用Entity Framework.

Models目录上点右键,并选择add new item

image_6

在对话框中选择 ADO.NET Entity Data Model.

在向导里,选择"Generate from Database" 并使用默认的"Northwnd" 连接字符串继续往下进行.

在我们的Demo中,我们将只使用Categories, Products 和 Suppliers表,当然你可以扩展这个Demo来包括一个富功能集.但是现在,除了那三个表,其他的Views 和Stored Procedures 我们都不选择.

image_8

当你点完成的时候,VS将会创建一个定制的用来访问这个数据库的.NET类集.而我们获得一个漂亮的设计器来设计他们的数据关系.

image_10

请注意,这些类的默认名字还是从数据库来的复数的命名,但在我们的OR-Mapping里他代表的是单个的实例,所以为了代码更加易读,将所有的表的名字改为合适的单数形式:Category, Product 和 Supplier. 然后Product里面的Navigation properties也需要是单数的,代表每个Product只有一个Category 和 Suppler.

image_28

下一步我们需要整理命名空间是我们代码里面的东西看起来更漂亮.右击设计视图然后设置属性,将namespace设为"NorthwindModels"而Entity Container name 设为 "NorthWindEntities"

image_18

 

创建 Controller

右击Controller目录选择"Add new Item".在对话框中找到MVC Controller Class并确定它的命名以Controller结尾.在这里我们将它命名为ProductsController.

image_14

OK,我们准备好了在ProductsController.cs里面开始工作.

controller的目的是为view准备好model.我们想将尽可能多的逻辑从view中分离出来,因为它很难在view里面测试.所以在controller里面,我们将访问Model并将他们建立起来,使view所要做的是显示一些数据.

首先我们要做的,是访问我们的数据库.

1. 添加合适的命名空间:Linq和我们OR Mapping的引用

using System.Linq;
using NorthwindModel;

2. 然后,我们创建一个NorthwindEntities容器类的实例.几乎所有我们的actions将会访问这个类.

    public class ProductsController : Controller
    {
        NorthwindEntities Northwind 
= new NorthwindEntities(); 
    }

OK,现在我们已经准备好来创建我们第一个action:列出所有的categories.记住controller的工作是为view准备model.当定义一个新的action,我喜欢写一个注释来提醒我什么样的URL会访问这个函数.

下一步我们从model中取得Categories表.我将结果放到一个泛型的集合类中(你可能需要添加一个System.Collections.Generic的引用),然后我们将结果传递给命名为"Categories"的视图(view).这是一个非常简单的例子,稍后我们会在这里见到更多的复杂的逻辑.

 

//URL: http://localhost/Products/Categories
[ControllerAction]
public void Categories()
{
    List
<Category> categories = Northwind.Categories.ToList();
    RenderView(
"Categories", categories);


下一步,我们需要创建"Categories"视图...

创建 View

右击Views文件夹,添加新的目录"Products".这样我们可以清晰的组织我们所有的视图.右击Views/Products文件夹添加新项MVC View Content Page.我们将会使用默认项目里面的模板页来使这个看起来更好看一些.

image_20

将它命名为Categories.aspx...使视图名字匹配上面的RenderView方法的第一个参数是很重要的.

默认项目将模板页放在 Views/Shared/Site.Master

image_22

为了获得强类型来访问我们从controller传过来的ViewData,我们需要告诉试图页我们期望得到的类型.这需要打开codebehind (Categories.aspx.cs) 并改变所继承的类型:

    public partial class Categories : ViewPage
    {
    }
to:
public partial class Categories : ViewPage< List<Category> > 
{
}

然后你可以很简单的写清楚的,简单的,可设计的HTML.注意到这里我循环ViewData里面所有的项并将他们作为一个链接输出.我使用MVC helper方法Html.ActionLink 使用合适的Product ID来创建List action的URL.

    <h2>Browse Products</h2>

    
<ul class="categorylisting">
    
      
<% foreach (var category in ViewData) { %>
    
        
<li>
            
<%= Html.ActionLink(category.CategoryName, new { action="List", id=category.CategoryName }) %>            
        
</li>
    
      
<% } %>
        
    
</ul>

OK,我们做好了最后的准备来运行他!

按F5,导航到我们写在 http://localhost:64701/products/Categories 的controller action

image_24

点击列表的任何一个链接你将得到一个错误,因为我们还没有写List action...我们将在下面做.

顺便说下,就好像我,如果你习惯在你的aspx页上用"View in Browser"来浏览,你将看到下面的错误.要想再现这错误,在Categories.aspx 右击并选择"View in Browser".

image_26

你得到了一个错误.为什么?记住在MVC模式中,所有的执行过程都通过controller,视图本身是不可运行的(not runnable).将来的工具会使这个更好,但在现在,使用F5或者你可以使用default.aspx来"run in browser".当然要确认你已经生成解决方案.

The List Action, View

OK,让我们回到前面加上缺少的List Action.我们在这里需要做的是找出给定的Category的所有products.首先我需要从model中获得所有的products,然后我需要保证Category references被加载了.Entity framework默认提供了一个Explicit Loading Model,这是你Explicit Load任何表所需要的.最后我们呈现view.

//example URL:http://localhost:64701/products/List/Confections
[ControllerAction]
public void List(string id)
{
    List
<Product> products = Northwind.GetProductsByCategory(id);

    
//prepare the view by explicitly loading the categories  
    products.FindAll(p => p.Category == null).ForEach(p => p.CategoryReference.Load());

    RenderView(
"ListingByCategory", products);

}

注意,我在NorthwindDataContext类中调用了一个自定义的方法...我个人比较喜欢将所有的数据存取逻辑封装到这个类中.下面来定义这个方法,在Model上右击,添加新项,选择CodeFile并将他命名为NorthwindDataContext.cs,然后添加实现.

 

using System;
using System.Collections.Generic;
using System.Linq;

namespace NorthwindModel
{
    
public partial class NorthwindEntities
    {

    }
}

现在你可以很容易地添加数据存取方法到这个类中,例如我们上面用到的GetProductsByCategory()方法.

public List<Product> GetProductsByCategory(string category)
{
    
return Products.Where(p => p.Category.CategoryName == category).ToList();
}

接下来,我们需要添加ListingByCategory视图.我们根据和上面一样的步骤来添加一个ListingByCategory.aspx页在Views/Products/目录下.

现在,我们要使ViewData为List<Product>类型.

public partial class ListingByCategory : ViewPage< List<Product> > 
 {
 }

接下来是实现视图,我们简单的循环view data并将它以正确的格式显示出来.

<%--Print out the catagory name--%>  
  
<% foreach (var product in ViewData) { %>
  
<% if (product.Category.CategoryName != null) {  %> 
    
<h2>  <%=product.Category.CategoryName  %></h2> 
    
<% break; %>
 
<%//end if %>
 
<%}//end foreach %>
        
    
<ul class="productlist">
    
        
<% foreach (var product in ViewData) { %>
        
            
<li>
                
<img src="/Content/Images/<%=product.ProductID%>.jpg" alt="<%=product.ProductName %>" /> 
                
<br/> 
                
<href="/Products/Detail/<%=product.ProductID %>"> <%=product.ProductName %> </a>
                
<br />
                Price: 
<%=String.Format("{0:C2}", product.UnitPrice)%> 
                  
<span class="editlink"">
                    (
<%= Html.ActionLink("Edit"new { Action="Edit", ID=product.ProductID })%>)
                
</span>
           
            
</li>
    
        
<% } %>
        
    
</ul>

一旦你从示例工程中添加/Content/Images 你将得到这个:

image_30

单元测试

使用MVC Model的一个重要原因是启用测试驱动开发(TDD)和更多一般的单元测试.UI的单元测试一直是个难题.Automation是脆弱的,而聘请一支军队的测试员来点击按钮是不划算的.MVC模式是为了分离出更多的代码(因此包括很多潜在性的BUG)到你可以进行测试的Model和Controller中.

因此,我们接下来要要在方法中为刚刚创建的那两个控制方法写单元测试和一致的逻辑.

首先我们需要配置我们的测试环境.默认的模板已经创建一个我们可以自定义开始的测试项目.

1.在MvcApplicationTest项目中,创建一个命名为ProductControllerTest.cs 的新类.

2.添加一个方法来测试我们在上面定义的Categories action.

namespace MvcApplicationTest.Controllers
{

    [TestClass]
    
public class ProductsControllerTest
    
{

        [TestMethod]
        
public void Categories()
        
{

        }

    }

}

3. 我们想测试我们写的controller,但我们并不想运行IIS和所有的ASP.NET.我们想要有一个更简单的系统来避免测试断点等等.为此目的,我为上面创建的ProductsController创建了一个test特性.在这个子类中我重写了RenderView方法.代替调用ASP.NET,我的测试特殊的实现了简单的捕获参数,使它们可以在测试中显示.

 

class TestProductsController : ProductsController
{
    
public string ViewName { getset; }
    
public string MasterName { getset; }
    
public new object ViewData { getset; }
    
protected override void RenderView(string viewName, string masterName, object viewData)
    {
        ViewData 
= viewData;
        MasterName 
= masterName;
        ViewName 
= viewName;
    }
  }

4. 现在我们来实现测试.首先创建一个我们test controller的实例,然后我创建一些模型数据.这儿的想法是我们并不想在我们的测试中连接生产数据库(或其他类似的数据库).我们想要确切的数据给我们的测试,既不多也不少.然后我们配置NorthwindDataContext来使用我们的数据,而不是从数据库中取得.然后调用我们想要测试的controller action.最后,我们assert一些东西...那ViewData的类型是对的,那返回的items的数目是对的,等等.注意,你需要添加一个EntityFramework (System.Data.Entity.dll)的引用到你的测试项目来使用这些EF产生的类型,例如Product,Category等等.

[TestMethod]
public void Categories()
{
    
// Create Products Controller
    TestProductsController controller = new TestProductsController();

    List
<Category> l = new List<Category>();
    l.Add(
new Category() { CategoryName = "Drink" });
    l.Add(
new Category() { CategoryName = "Food" });
    l.Add(
new Category() { CategoryName = "Cheese" });

    controller.Northwind.TheCategories 
= l.AsQueryable();

    
// Invoke Controller Action
    controller.Categories();

    
// Verify Detail Action Behavior
    Assert.AreEqual(typeof(List<Category>),
                     controller.ViewData.GetType(),
                     
"Product object Passed to View");

    Assert.AreEqual(
3,
                     ((List
<Category>)controller.ViewData).Count,
                     
"Correct Number of Catagories returned");

    Assert.AreEqual(
"Food",
                     ((List
<Category>)controller.ViewData)[1].CategoryName,
                     
"Correct Product Object Passed to View");

    Assert.AreEqual(
"Categories",
                     controller.ViewName,
                     
"Correct View Rendered");

}

5. 设置测试项目中的connection string使他从EF模型中获得元数据,但不要打开数据库.在MVCApplicationTest添加一个使用该connection string的app.config文件到项目的根目录下.

<?xml version="1.0"?>
<configuration>
  
<connectionStrings>
    
<add name="NorthwindEntities"
         connectionString
="metadata=res://*/;provider=System.Data.SqlClient;"
         providerName
="System.Data.EntityClient" />
  
</connectionStrings>
</configuration>

现在我们需要回到EDM设计器中来,设置嵌入元数据到model中以使它成为程序集的一部分.这使我们上面的connection string 更简单.在EDMX文件上双击,然后在模型浏览器(model browser)中选择概念模型(conceptual model),然后在属性面板中修改Metadata Artifact Processing属性“Copy to Output Directory”为“Embed in  Output Assembly”.

image_33

6. 现在我们要在MVCApplication做的只是一点设置是启动一个伟大的测试体验...这儿耍了点把戏,而我所演示的并不是确切的模式(还没有确切的模式).但是我喜欢他,所以管他呢!

MVCApplication/Model/NorthwindDataContext.cs 为Categories添加下面的属性.

private IQueryable<Product> savedProducts = null;
public IQueryable<Product> TheProducts
{
    
get
    {
        
if (savedProducts == null) savedProducts = this.Products;
        
return savedProducts;
    }
    
set
    {
        savedProducts 
= value;
    }
}

然后,保证所有的访问都通过这个抽象容器,例如:

public List<Product> GetProductsByCategory(string category)
{
    
return TheProducts.Where(p => p.Category.CategoryName == category).ToList();
}

最后,我们需要保证NorthwindEntities能够在测试类中访问,所以在ProductsController.cs中添加"public"修饰.

public NorthwindEntities Northwind = new NorthwindEntities();

现在,我们可以运行我们的测试.你可以设置测试项目为"startup project" (右击测试项目)或者选择Test/Run选项来运行它.

image_35

好了,这就是MVC+EF...还有很多和这示例有关的我发表的文章.如果有兴趣而我又有时间,我会在将来的博客中发表更多文章.下面有所有的源代码,你可以自己下来玩.

Enjoy!

你可以从 这儿下载完整的示例.

posted on 2008-02-23 11:23  Q.Lee.lulu  阅读(4187)  评论(12编辑  收藏  举报