1. 为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 Genre Genre { get; set; } public virtual Artist Artist { get; set; } }
类Genre:
public class Genre { 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:
public class Artist { public virtual int ArtistId { get; set; } public virtual string Name { get; set; } }
注意:此处的属性都是virtual
构建完类之后,需要对整个项目进行编译。
2. 基架
ASP.NET MVC中的基架可以为应用程序的创建、读取、更新和删除(CRUB)功能生成所需要的样板代码。基架模版检测模型类的定义,然后生成控制器以及与该控制器关联的视图,有些情况下还会生成数据访问类。基架知道如何命名控制器、命名视图以及每个组件需要执行什么代码,也知道在应用程序中如何放置这些项以使应用程序正常工作。
2.1 基架选项:
像MVC框架的所有其他项一样,如果不喜欢默认的基架,就可以根据自己的需要自定义基架或替换现有基架的代码生成机制。也可以通过NuGet(搜索scaffolding)查找可替代的基架模版。
2.2 常用的基架模版:
(1)MVC5 Controller——Empty
该会向Controllers文件夹中添加一个具有指定名称且派生自Controller的类(控制器)。这个控制器带有的唯一操作就是Index操作,且在内部除了返回一个默认ViewResult实例的代码之外,没有其他任何代码。这个模版不会生成任何视图。
(2)MVC5 Controller with read/write Actions
该模版会向项目中添加一个带有Index、Details、Create、Edit和Delete操作的控制器。虽然控制器内部的操作不是完全空白,但不会执行任何有实际意义的操作,除非向其中添加自己的代码并为他们创建试图。
(3)Web API 2 API Controller Scaffolders
有几个模版向项目中添加一个继承自基类ApiController的控制器。可以使用这些模版为应用程序创建Web API
(4)MVC5 Controller with Views,Using Entity Framework
该模版不仅生成了带有整套Index、Details、Create、Edit和Delete操作的控制器及其需要的所有相关视图,并且还生成了与数据库交互(持久保存数据到数据库或从数据库中读取数据)的代码。
2.3 基架和实体框架:
新建的ASP.NET MVC5项目会自动包含对实体框架(EF)的引用。EF是一个对象关系映射(object-relational mapping,ORM)框架,它不但知道如何在关系型数据库中保存.NET对象,而且还可以利用LINQ查询语句检索那些保存在关系型数据库中的.net对象。
EF支持数据库优先、模型优先和代码优先的开发风格;MVC基架采用代码优先代码优先的风格。
代码优先是指可以在不创建数据库模式、也不打开Visula Studio设计器的情况下,向SQL Server中存储或检索信息。
模型对象中的属性如果设置为虚拟的,可以给EF提供一个指向C#类集的钩子(hook),并未EF启用了一些特性,如高效的修改跟踪机制(efficient change tracking mechanism)。EF需要知道模型属性值的修改时刻,因为需要在这一刻生成并执行一个SQL UPDATE语句,使这些改变和数据库保持一致。
2.4 DbContext类
当使用EF的代码优先方法时,需要使用从EF的DbContext类派生出的一个类来访问数据库。该派生类具有一个或多个DbSet<T>类型的属性,类型DbSet<T>中的每一个T代表一个想要持久保存的对象。可以把DbSet<T>想象成一个特殊的、可以感知数据的泛型列表,它知道如何在父上下文中加载和保存数据。
例如,下面的类(数据上下文类)就可以用来存储和检索Albums、Artist和Genre的信息:
public class MusicStoreDB : DbContext { public MusicStoreDB() : base("name=MusicStoreDB") { } 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; } }
使用先前的数据上下文,可以通过使用Linq查询,按字母顺序检索出所有专辑,代码如下:
var db = new MusicStoreDB(); var allAlbums = from album in db.Albums orderby album.Title ascending select album;
3. 执行基架模版
(1)右击Controllers文件夹 --> 添加 --> 控制器
(2)添加基架 --> 包含视图的MVC5 控制器(使用EF) --> 添加
(3)在“添加控制器”对话框中,选择模型类、数据上下文类,修改控制器名称。
(4)修改数据上下文类,修改为MvcMusicStoreDB:
数据上下文会根据选择的模型,自动生成数据上下文类,如下所示。
public class MusicStoreDB : DbContext { public MusicStoreDB() : base("name=MusicStoreDB") { } 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; } }
注意MusicStoreDB() : base("name=MusicStoreDB")中,MusicStoreDB是配置的数据库连接
<connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\aspnet-MvcMusicStore-20130928021208.mdf;Initial Catalog=aspnet-MvcMusicStore-20130928021208;Integrated Security=True" providerName="System.Data.SqlClient" /> <add name="MusicStoreDB" connectionString="Data Source=(localdb)\v11.0; Initial Catalog=MusicStoreDB-20130929160340; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|MusicStoreDB-20130929160340.mdf" providerName="System.Data.SqlClient" /> </connectionStrings>
如果不配置具体的连接,EF将尝试连接SQL Server的LocalDB实例,并且查找与DbContext派生类名相同的数据库。如果EF能够连接上数据库服务器,但找不到数据库,那么框架会自动创建一个数据库。
(5)视图
一旦基架运行完成,就将在新的视图文件夹Views/Album中出现一个视图集。视图提供了罗列、编辑和删除专辑的功能。
4.执行基架代码
4.1用实体框架创建数据库
4.2使用数据库初始化器
保持数据库和模型变化同步的一个简单方法是允许实体框架重新创建一个现有的数据库。可以告知EF在应用程序每次启动时重新创建数据库或者仅当检测到模型变化时重建数据库。当调用EF的Database类中的静态方法SetInitializer时,可以选择这两种策略中的任意一个。
框架中带有两个IDatabaseInitializer对象:DropCreateDatabaseAlways和DropCreateDatabaseIfModelChanges。可以根据这两个类的名称来辨别每个类所代表的策略。两个初始化器都需要一个泛型类型的参数,并且这个参数必须是DbContext的派生类。
在文件global.asax.cs中,可以在应用程序启动过程中设置一个初始化器:
源代码:
protected void Application_Start() { Database.SetInitializer(new MusicStoreDbInitializer()); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
修改为:
protected void Application_Start() { Database.SetInitializer(new DropCreateDatabaseAlways<MusicStoreDB>()); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
4.3播种数据库
Seed方法可以为应用程序创建一些初始化的数据。
protected void Application_Start() { Database.SetInitializer(new MusicStoreDbInitializer()); AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); }
播种数据:
public class MusicStoreDbInitializer : System.Data.Entity.DropCreateDatabaseAlways<MusicStoreDB> { protected override void Seed(MusicStoreDB 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); } }
这样,每次重新生成音乐商店数据库时,都会有两种流派(Jazz和Rock)、两个艺术家(Al Di Meola和Rush)和一个专辑。代码会在程序启动时注册这个初始化器。
5 编辑专辑
5.1 创建编辑专辑的资源
默认的MVC路由规则是将HTTP GET请求中的 /StoreManager/Edit/5 传递到StoreManager控制器的Edit操作中,代码如下:
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); }
下面是商店管理器的Edit视图中用来为流派创建下拉列表的代码:
<div class="form-group"> @Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.DropDownList("GenreId", String.Empty) @Html.ValidationMessageFor(model => model.GenreId) </div> </div>
在视图中使用DropDownList辅助方法,Edit中的两行代码就是为了构建从数据库中所有可得到的流派和艺术家的列表,并将这些列表
存储在ViewBag中以方便以后让DropDownList辅助方法检索。
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
- 第1个参数指定了将要放在列表中的项
- 第2个参数是一个属性名称,该属性包含当用户选择一个指定项时使用的值(键值 ,像52或2)
- 第3个参数是每一项要显示的文本
- 第4个参数包含了最初选定项的值
5.2 模型和视图模型终极版
针对专辑的编辑情形,模型对象(Album对象)并没有包含编辑专辑视图所需要的全部信息,因为另外还需要所有可能的流派和艺术家列表。针对这种问题,有两种解决方案。
基架生成代码展示了第一种解决方案:将额外的信息传递到ViewBag结构中。这个方案完全合理而且还便于实现。
第二种解决方案:强类型模型,创建一个视图特定模型的对象,将专辑信息、流派和艺术家信息传递给一个视图。这个模型可能如下定义:
public class AlbumEditViewModel { public Album AlbumToEdit {get; set;} public SelectList Genres {get; set;} public SelectList Artists {get; set;} }
这样Edit操作就不需要将信息放进ViewBag,而需要实例化AlbumEditViewModel类,设置所有的对象属性,并将视图模型传递给视图。
5.3 Edit视图
当用户单击页面上的Save按钮时,HTML将发送一个HTTP POST请求,请求回到 /StoreManager/Edit/1 页面。这时浏览器会自动收集用户在表单输入中的所有信息并将这些值(及其相关的name属性值)放在请求中一起发送。这里注意input和select元素的name属性,需要和Album模型中的属性匹配。
5.4 响应编辑时的POST请求
接受HTTP POST请求来编辑信息的操作的名称也是Edit,但不同于前面看到的Edit操作,因为它有一个HttpPost操作选择器特性:
[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); }
这个操作的作用就是接收含有用户所有编辑项的Album模型对象,并将这个对象保存到数据库中。
(1)编辑happy path
happy path就是当模型处于有效状态并可以将对象保存到数据库时执行的代码路径。操作通过Model.IsValid属性来检查模型对象的有效性。这个属性可以看作一个信号,来确保用户输入有用的专辑特性值。
如果模型处于有效状态,则执行以下的代码:
db.Entry(album).State = EntityState.Modified;
这行代码告知数据上下文该对象在数据库中已经存在,所以框架应该对现有的专辑应用数据库中的值而不要再创建一个新的专辑记录。
db.SaveChanges();
上下文生成一条SQL UPDATE命令更新对应的字段值以保留新值。
(2)编辑sad path
sad path就是当模型无效时操作采用的路径。在sad path中,控制器操作需要重新创建Edit视图,以便用户更改自身产生的错误