为 MVC Music Store 建模

在 Models 目录中为专辑、艺术家、流派建模:

public class Album
{
    public virtual int AlbumId { get; set; }
    public virtual int GenreId { get; set; }
    public virtual int ArtistId { get; set; }
    public virtual string Title { get; set; }
    public virtual decimal Price { get; set; }
    public virtual string AlbumArtUrl { get; set; }
    public virtual Gener Genre { get; set; }
    public virtual Artist Artist { get; set; }
}

public class Artist
{
    public virtual int ArtistId { get; set; }
    public virtual string Name { get; set; }
}

public class Gener
{
    public virtual int GenreId { get; set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual List<Album> Albums { get; set; }
}

可以看到,每一个专辑都有 Artist 和 ArtistId 两个属性来管理与之相关的艺术家。这里的 Artist 属性称为 导航属性(navigational property),对于一个专辑,可以通过点操作符来找到与之相关的艺术家

ArtistId 属性为外键属性(foreign key property),知道一点数据库知识的话,就会知道艺术家和专辑会被保存在两个不同的表中,并且一个艺术家可能与多个专辑有关联

一个专辑也会有一个相关的流派,一种流派也会对应一个相关专辑的列表。

为商店管理器构造基架

新建的 ASP.NET MVC 4 项目会自动包含对实体框架的引用。EF 是一个对象关系映射框架,它不但知道如何在关系型数据库中保存 .NET 对象,而且还可以利用 LINQ 查询语句检索保存在关系型数据库中的 .NET 对象。

还记得模型对象中的所有属性都是虚拟的吗?虚拟属性不是必需的,但是它们给 EF 提供一个指向纯 C# 类集的钩子(hook),并为 EF 启用了一些特性,如高效的修改跟踪机制。EF 需要知道模型属性值的修改时刻,因为它要在这一刻生成并执行一个 SQL UPDATE 语句,使这些改变和数据库保持一致。

当使用 EF 的代码优先方法时,需要从 EF 的 DbContext 类派生出的一个类来访问数据库。该派生类一般具有多个 DbSet<T> 类型的属性。

右击 Controller 文件夹,添加控制器,ASP.NET MVC 中的基架可以为应用程序的 CRUD 功能生成所需的样板代码,选择新建数据上下文:

基架会在 Models 文件夹中添加 MusicStoreDBContext.cs 文件,此类继承了实体框架的 DbContext 类。尽管只告知了基架 Album 类,但是它看到了相关的模型并把它们也包含在了上下文中

public class MusicStoreDBContext : DbContext
{
    // You can add custom code to this file. Changes will not be overwritten.
    // 
    // If you want Entity Framework to drop and regenerate your database
    // automatically whenever you change your model schema, please use data migrations.
    // For more information refer to the documentation:
    // http://msdn.microsoft.com/en-us/data/jj591621.aspx
 
    public MusicStoreDBContext() : base("name=MusicStoreDBContext")
    {
    }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Album> Albums { get; set; }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Artist> Artists { get; set; }
 
    public System.Data.Entity.DbSet<MvcMusicStore.Models.Genre> Genres { get; set; }
 
}

选择的基架模板也会生成 StoreManagerController 类,并拥有选择和编辑专辑信息所需的所有代码。

public class StoreManagerController : Controller
{
    private MusicStoreDBContext db = new MusicStoreDBContext();
 
    // GET: /StoreManager/
    public ActionResult Index()
    {
        var albums = db.Albums.Include(a => a.Artist).Include(a => a.Genre);
        return View(albums.ToList());
    }
 
    // more later ...

Include 方法的调用告知实体框架在加载一个专辑的相关流派和艺术家信息时采用预加载策略(尽其所能使用查询语句加载所有数据)。

实体框架另一种策略是延迟加载(默认)。在这种情况下,EF 在 LINQ 查询中只加载主要对象(专辑)的数据,而不填充 Genre 和 Artist 属性:

var albums = db.Albums;

延迟加载根据需要来加载相关数据,也就是说,只有当需要 Album 的 Genre 和 Artist 属性时,EF 才会通过向数据库发送一个额外的查询来加载这些数据。然而不巧的是,当处理专辑信息时,延迟加载策略会强制框架为列表中的每一个专辑向数据库发送一个额外的查询。对于含有 100 个专辑的列表,如果要加载所有的艺术家数据,延迟加载则总共需要进行 101 个查询,其中,第一个查询是用在 所有 Album 查询上,另外 100 个查询是延迟加载策略用来查询艺术家数据造成的。这就是典型的 “N + 1”问题。延迟加载在带来便利的同时可能也要付出潜在的性能损失代价

基架运行完成,新的视图集也出现在了 Views 目录下。视图的模型是 Album 对象的枚举序列。

@model IEnumerable<MvcMusicStore.Models.Album>
 
@{
    ViewBag.Title = "Index";
}
 
<h2>Index</h2>
 
<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Artist.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Genre.Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.AlbumArtUrl)
        </th>
        <th></th>
    </tr>
 
    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Artist.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.AlbumArtUrl)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.AlbumId }) |
                @Html.ActionLink("Details", "Details", new { id = item.AlbumId }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.AlbumId })
            </td>
        </tr>
    }
</table>

注意,基架是如何选择所有“重要的”字段显示给用户的。换句话说,视图的表中没有显示任何外键属性的值(因为它们对用户是无意义的),但显示了相关的艺术家姓名和流派名称。

用实体框架创建数据库

EF 的代码优先方法会尽可能的使用约定而非配置。如果在运行时不配置一个具体的数据库连接,EF 将按照约定创建一个连接。EF 会尝试连接 SQL Server Express 的本地实例。

建议在生产环境下还是手动配置数据库连接!在 web.config 文件中添加一个连接字符串,该字符串名称必须与数据上下文类的名称一致!

<connectionStrings>
  <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-MvcMusicStore-20151006055357.mdf;Initial Catalog=aspnet-MvcMusicStore-20151006055357;Integrated Security=True"
    providerName="System.Data.SqlClient" />
  <add name="MusicStoreDBContext" connectionString="Data Source=.; 
    Initial Catalog=MusicStoreDB; Integrated Security=True; MultipleActiveResultSets=True;"
    providerName="System.Data.SqlClient" />
</connectionStrings>

路由到 Index 操作上,此时会发现本机安装的 SQL Server 数据库已创建了新的 DataBase:

使用数据库初始化器

保持数据库和模型变化同步的一个简单方法是允许实体框架重新创建一个现有的数据库。可以告知 EF 在程序每次启动时重新创建数据库或者仅当检测到模型变化时重建数据库。当调用 EF 的 Database 类中的静态方法 SetInitializer 时,可以选择这 2 种策略中的任意一个。

当使用 SetInitializer 方法时,需要向其传递一个 IDatabaseInitializer 对象,而框架中有 2 个此对象:DropCreateDatabaseAlways 和 DropCreateDatabaseIfModelChanges。这两个初始化器都需要一个泛型类型的参数,并且这个参数必须是 DbContext 的派生类。

假如要在应用程序每次启动时都重新创建音乐商店的数据库,那么在 global.asax.cs 内部,可在应用程序启动过程中设置一个初始化器:

protected void Application_Start()
{
    System.Data.Entity.Database.SetInitializer(
        new System.Data.Entity.DropCreateDatabaseAlways<MvcMusicStore.Models.MusicStoreDBContext>() );
 
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

现在可能极想知道为什么有人想在每次应用程序重新启动时都要重建数据库,尽管模型改变了,但是难道不想保留其中的数据吗?

       这些都是很合理的问题。必须记住,代码优先方法(如数据库的初始化器)的特征是为应用程序生命周期早期阶段的迭代和快速变化提供便利的。一旦发布一个实际网站并且采用真实的客户数据,就不能在每次改变模型时重新创建数据库了。

播种数据库

假设每次应用程序都会重建数据库,然而,想让新建的数据库中带有一些流派、艺术家甚至一些专辑,以便在开发应用程序时不必输入数据就可以使其进入可用状态。这样的情形下,可以创建一个 DropCreateDatabaseAlways 的派生类并重写其中的 Seed 方法:

public class MusicStoreDbInitializer : DropCreateDatabaseAlways<MusicStoreDBContext>
{
    protected override void Seed(MusicStoreDBContext context)
    {
        context.Artists.Add(new Artist { Name = "Al Di Meola" });
        context.Genres.Add(new Genre { Name = "Jazz" });
        context.Albums.Add(new Album
        {
            Artist = new Artist { Name = "Rush" },
            Genre = new Genre { Name = "Rock" },
            Price = 9.99m,
            Title = "Caravan"
        });
        base.Seed(context);
    }
}

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        System.Data.Entity.Database.SetInitializer(
            new MvcMusicStore.Models.MusicStoreDbInitializer());
 
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

此时,每次应用程序重启之后,在数据库中都会有 2 种流派,2 个艺术家以及 1 个专辑。

       看起来似乎做了很多的工作,但是一旦知道基架能够做的工作,那么实际的工作量是非常小的,仅需要 3 步:

  1. 实现模型类
  2. 为控制器和视图构建基架
  3. 选择数据库初始化策略

编辑专辑

点击 Edit 链接,默认的 MVC 路由规则将 HTTP GET 请求传递到相应的操作上:

// GET: /StoreManager/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Album album = db.Albums.Find(id);
    if (album == null)
    {
        return HttpNotFound();
    }
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    return View(album);
}

Find 方法接受的参数是表的主键值,如上图中的 id,真正的表的主键 AlbumId 值在 Index 视图中被赋予了超链接的 id 属性上:

HttpNotFound() 返回一个 404 响应码的错误。

编辑页面如下:

编辑页面提供给用户下拉菜单以选择流派和艺术家:

<div class="col-md-10">
    @Html.DropDownList("GenreId", string.Empty)
    @Html.ValidationMessageFor(model => model.GenreId)
</div>

控制器是这样提供数据给视图以检索下拉菜单的:

// 参数列表:数据源、valueField、textField、选中项
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);

模型和视图模型终极版

有 2 种解决方案。基架生成代码将额外的信息传递到 ViewBag 结构中,在模型较为简单和紧凑的情况下,这是一种合理且便于实现的方式。而一些程序员更喜欢通过一个强类型的模型对象得到所有的模型数据。

强类型模型的拥护者会选择第 2 种方案,这个模型可能需要这样定义:

namespace MvcMusicStore.ViewModel
{
    public class AlbumEditViewModel
    {
        public Album AlbumToEdit { get; set; }
        public SelectList Genres { get; set; }
        public SelectList Artists { get; set; }
    }
}

响应编辑时的 POST 请求

方法名称仍为 Edit,不同的是它有一个 HttpPost 特性,它接受一个 Album 对象,并保存至数据库。

// POST: /StoreManager/Edit/5
// 为了防止“过多发布”攻击,请启用要绑定到的特定属性,有关 
// 详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkId=317598。
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="AlbumId,GenreId,ArtistId,Title,Price,AlbumArtUrl")] Album album)
{
    if (ModelState.IsValid)
    {
        db.Entry(album).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
    ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
    return View(album);
}

模型绑定

Edit 视图认真的命名了每一个表单元素的 name 名称,例如 Price 值的 input 元素的名称是 Price 等。

当操作带有参数时,MVC 运行环境会使用一个默认的模型绑定器(DefaultModelBinder)来构造这个参数。当然,也可以为不同的模型注册多个模型绑定器。在本例中,默认的模型绑定器检查 Album 类,并查找能用于绑定的所有 Album 属性。换句话说,当模型绑定器看到 Album 类中具有 Title 属性时,它就在请求中查找名为“Title”的参数。注意,这里说的是请求中,而不是表单集合中。

模型绑定器使用称为值提供器(value provide)的组件在请求的不同区域中查找参数值。模型绑定器可以查看路由数据、查询字符串、表单集合。另外,也可以添加自定义的值提供器。

模型绑定器有点像搜救犬,运行时告知模型绑定器想要知道某个参数值或复合对象的一些属性值,然后,模型绑定器开始导出查找这些参数。

显式模型绑定

当操作中有参数时,模型绑定器会隐式的工作。但也可以使用控制器中的 UpdateModel 和 TryUpdateModel 方法显式的调用模型绑定:

[HttpPost]
public ActionResult Edit()
{
    var album = new Album();
    try
    {
        UpdateModel(album);
        db.Entry(album).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch 
    {
        ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
        ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
        return View(album);               
    }
}

TryUpdateModel(album) 不会抛出异常,因此也可以使用 if else 结构替换上述的 try catch 结构:

if (TryUpdateModel(album))
{
    // Do something...
}
else
{
    // Do something...
}

模型绑定的副产品就是模型状态。模型绑定器移进模型中的每一个值在模型状态中都有相应的一条记录,并在绑定后,可以查看模型状态以确定绑定是否成功:

TryUpdateModel(album);
if (ModelState.IsValid)
{
    // Do something...
}
else
{
    // Do something...
}

如果在模型绑定过程中出现错误,那么模型状态将会包含导致绑定失败的属性名、尝试的值、以及错误消息。

模型(Model)– ASP.NET MVC 4 系列相关推荐

  1. ASP.NET MVC实践系列6-Grid实现(上)

    ASP.NET MVC中不推荐使用webform的控件了,也就是说当希望列表显示数据时不能使用GridView了,很多开源软件为ASP.NET MVC实现了列表的解决方案,这些具体的解决方案我们放到下 ...

  2. ASP.NET MVC实践系列9-filter原理与实践

    filter实际上是一个特性(attribute),它提供了一种向controller 或 action中添加某些任务的方法,当controller 或 action被调用时,会触发filter中定义 ...

  3. ASP.NET MVC实践系列11-FCKEditor和CKEditor的使用

    FCKEditor是一款强大的在线编辑器,简单实用,多浏览器兼容,免费开源,应用十分广泛,据他的官方网站上称有三百多万的下载量,而且无数的知名大公司正在使用它.所以FCKEditor是很值得信赖的,现 ...

  4. Scott的ASP.net MVC框架系列文章之四:处理表单数据(2)

    前几周我发表了一系列文章介绍我们正在研究的ASP.NET MVC框架.ASP.NET MVC框架为你提供了一种新的开发Web应用程序的途径,这种途径可以让应用程序变得更加层次清晰,而且更加有利于对代码 ...

  5. ASP.NET MVC Framework 系列

    序言 做为设计模式的王者,MVC在众多领域都成为良好的模型的代名词,从前在ASP.NET下我们只能依靠Monorail来实现ASP.NET下无控件的MVC,但是现在ASP.NET 下的MVC已经成为现 ...

  6. ASP.NET MVC学习系列(一)-WebAPI初探

    由于即将要接手的新项目计划用ASP.NET MVC3来开发,所以最近一段时间一直在看相关的书或文章.因为之前在大学里也曾学习过MVC2开发,也做过几个简单的MVC2的小型测试项目,不过在后来工作以后主 ...

  7. ASP.NET MVC实践系列5-结合jQuery

    现在做web开发肯定都听说过jQuery,jQuery在ASP.NET MVC被支持的很好,而且据说vs2010中也会集成进去,所以使用ASP.NET MVC了解jQuery肯定有莫大的好处,所以这里 ...

  8. Spring.Net+NHibenate+Asp.Net mvc +ExtJs 系列 3 ----数据访问层

    在上一篇中,我们已经搭建起了整个解决方案的项目,并且建好了数据库,完成了实体类和Nhibernate映射文件.在本文中,将定义数据访问接口,并利用Nhibernate实现接口,利用Spring.net ...

  9. ASP.NET MVC 入门系列教程

    一个居于ASP.NET MVC Beta的系列入门文章,有朋友提议说写一个示例程序来同步讲解,那样更加容易学习.所以就写选择了写一个Blog程序来作为示例程序.(原来是居于ASP.NET MVC Pr ...

最新文章

  1. 反转给定区间的链表C语言,寻找素数对(C++)
  2. JQuery实现动态插入HTML模块
  3. DWZ与百度编辑器的IE8以下上传图片兼容问题
  4. netif_start_queue/netif_wake_queue/netif_stop_queue
  5. C# XML字符串与DataTable相互转换
  6. 多方计算时,每次结果都存在着巨大隐患,如何解决
  7. 用博文中的方法-r -d \t试了下conlleval测试crf++的输出
  8. tiledmap 图块属性_TiledMap 组件参考
  9. 信杂比公式_信噪比公式
  10. 吉林大学 校园网 认证相关 (SUSPEND)
  11. 腾讯云大学实验室(153个学习案例)免费学服务器技术
  12. AVC编码中的规格 :High、Baseline、Main什么意思?还有High@L3.0、High@L4.0、High@L5.1等
  13. 如何平衡老师任务和自学之间的关系
  14. 使用ngrok 二 -- 微信公众号笔记---本地调试微信接口
  15. Poco C++库简介
  16. 组合数学4-全排列生成算法
  17. 鸿蒙系统教程,麒麟9000+鸿蒙操作系统,华为新平板有点牛
  18. EBC-B10电池容量测试仪之通信协议分析
  19. 【云计算】基于VMware Vsphere云平台的设计与实现
  20. 高中计算机竞赛在哪学,信息学奥赛获奖学生,都去哪高就了?

热门文章

  1. MD5加密 登录用户名密码
  2. 如何利用客户端在CU发博客
  3. Mysql 基本命令
  4. java ftp下载文件源码_java实现ftp文件下载的源代码
  5. ntrip获取源列表_Ntrip通讯协议怎么样?
  6. eplise怎么连接数据库_如何通过eclipse连接到mysql数据库
  7. DataGridView设置单元格的提示内容ToolTip详解
  8. 主角有智能芯片的种田小说_推荐3本克苏鲁类小说,压抑邪恶与搞笑逗乐并存,看看是你的菜吗...
  9. Flask-Email实现发送大量的电子邮件(可以限制发送的邮件数)
  10. Codeforces Global Round 13 E. Fib-tree