模型(Model)– ASP.NET MVC 4 系列

       为 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 功能生成所需的样板代码,选择新建数据上下文:

image

image

 

       基架会在 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>

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

image

 

用实体框架创建数据库

       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:

image

 

使用数据库初始化器

       保持数据库和模型变化同步的一个简单方法是允许实体框架重新创建一个现有的数据库。可以告知 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 个专辑。

image

image

       看起来似乎做了很多的工作,但是一旦知道基架能够做的工作,那么实际的工作量是非常小的,仅需要 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 属性上:

image

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

       编辑页面如下:

image

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

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

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

posted on 2015-10-07 14:35  SkySoot  阅读(500)  评论(0编辑  收藏  举报

导航