前面两篇文章我们分别讲了MVC下的视图和控制器,这章我们要讲模型(model),这章由于涉及到基架的使用,还有对模型绑定后数据库相关知识,可能会 很抽象,慢慢来吧,↖(^ω^)↗!在这之前可以先看看老师上课提的几个问题,相信看完了,你就对MVC中的模型有了个初步的了解了!
一 MVC模型相关问题释疑
1 什么是模型,模型有哪几种分类?
在这里我们要讨论的是那些发送信息到数据库,执行业务计算,并在视图中渲染的模型对象。也就是说这些对象代表着应用程序关注的域,模型就是要显示、保持、创建、更新和删除的对象。而模型一般有:面向业务的模型对象和面向视图的模型对象。
2 什么是主键属性,什么是外键属性?
首先数据库中主外键的定义:
主键 | 外键 | |
---|---|---|
定义: | 唯一标识一条记录,不能有重复的,不允许为空 | 表的外键是另一表的主键, 外键可以有重复的, 可以是空值 |
作用: | 用来保证数据完整性 | 用来和其他表建立联系用的 |
个数: | 主键只能有一个 | 一个表可以有多个外键 |
因为这个主外键属性对于理解后面的EF框架(ORM)很有帮助,所以这里我们多讲一些!下面用例子讲解,这个例子也是我们后面需要用的代码:
类Album:专辑类
代码语言:javascript复制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:流派类
代码语言:javascript复制1 public class Genre {
3 public virtual int GenreId { get; set; }
4 public virtual string Name { get; set; }
5 public virtual string Description { get; set; }
6 public virtual List<Album> Albums { get; set; }
7 }
类Artist:艺术家类
代码语言:javascript复制1 public class Artist
2 {
3 public virtual int ArtistId { get; set; }
4 public virtual string Name { get; set; }
5 }
注意:此处的属性都是virtual!这是为了给EF框架提供一个钩子,即方便模型到数据库的映射,不理解就记住,后面的项目会详细讲解。
从上面三个类的代码可以看到,红色标记的是主键,而黄色的就是外键。
解释:在每个Album类中都有Artist和ArtistID两个属性,所以对于一个专辑Album,可以通过点操作符来找到与之相关的艺术家(Album.Artist),称Artist属性为导航属性(navigation property)。而称ArtistID属性为外键属性(foreign key),因为与模型对应的数据库中,专辑表(Album)和艺术家(Artist)表存在对应的外键关系,即ArtistID是Album表的外键!
3 什么是基架,基架的作用是什么?
基架的含义:根据用户自定义的模型(model)生成相应的控制器和视图。
ASP.NET MVC中的基架可以为应用程序的创建、读取、更新和删除(CRUB)功能生成所需要的样板代码。基架模版检测模型类的定义,然后生成控制器以及与该控制器关联的视图,有些情况下还会生成数据访问类。基架知道如何命名控制器、命名视图以及每个组件需要执行什么代码,也知道在应用程序中如何放置这些项以使应用程序正常工作。
下面介绍典型的基架模板:
(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操作的控制器及其需要的所有相关视图,并且还生成了与数据库交互(持久保存数据到数据库或从数据库中读取数据)的代码。
5 什么是实体框架,什么是代码优先和数据上下文?
新建的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语句,使这些改变和数据库保持一致。对于前面Album模型的释疑。
当使用EF的代码优先方法时,需要使用从EF的DbContext类派生出的一个类来访问数据库。该派生类具有一个或多个DbSet<T>类型的属性,类型DbSet<T>中的每一个T代表一个想要持久保存的对象。可以把Db的Set<T>想象成一个特殊的、可以感知数据的泛型列表,它知道如何在父上下文中加载和保存数据。
例如,下面的类(MusicStoreDB 数据上下文类)就可以用来存储和检索Albums、Artist和Genre的信息:
代码语言:javascript复制 1 public class MusicStoreDB : DbContext
2 {
3 public MusicStoreDB() : base("name=MusicStoreDB")
4 {
5 }
7 public System.Data.Entity.DbSet<MvcMusicStore.Models.Album> Albums { get; set; }
9 public System.Data.Entity.DbSet<MvcMusicStore.Models.Artist> Artists { get; set; }
11 public System.Data.Entity.DbSet<MvcMusicStore.Models.Genre> Genres { get; set; }
13 }
使用先前的数据上下文,可以通过使用Linq查询,按字母顺序检索出所有专辑,代码如下:
代码语言:javascript复制1 var db = new MusicStoreDB();
2 var allAlbums = from album in db.Albums
3 orderby album.Title ascending
4 select album;
二 项目实战
1. 为MVC Music Store建模
Models文件夹(右击) --> 添加 --> 类,就是添加文章开头的Album、Genre、Artist三个类,注意是在Models下哦,如图:
此处有个使用vs的小技巧,在创建model类中的属性时候,可以键入prop,然后按tab键两次,可快速创建属性哦!
构建完类之后,需要对整个项目进行编译。点击菜单栏--》生成--》生成解决方案,或者快捷键Ctrl Shift B。注意,如果没有编译项目,则后续的使用模型创建基架的时候会报错!
2. 执行基架模版
(1)右击Controllers文件夹 --> 添加 --> 控制器:
(2)添加基架 --> 包含视图的MVC5 控制器(使用EF) --> 添加:
(3)在“添加控制器”对话框中,选择模型类、数据上下文类,修改控制器名称。
模型类选择Album,我们刚才创建的模型类,基架基于此类,会创建相应的控制器和视图。
数据上下文新建一个名为MvcMusicStoreDB的类。
同时修改控制器名称为:StoreManagerController。
数据上下文会根据选择的模型,自动在models中生成数据上下文类,如下所示。
代码语言:javascript复制 1 public class MusicStoreDB : DbContext
2 {
3 public MusicStoreDB() : base("name=MusicStoreDB")
4 {
5 }
6
7 public System.Data.Entity.DbSet<MvcMusicStore.Models.Album> Albums { get; set; }
8
9 public System.Data.Entity.DbSet<MvcMusicStore.Models.Artist> Artists { get; set; }
10
11 public System.Data.Entity.DbSet<MvcMusicStore.Models.Genre> Genres { get; set; }
12
13 }
注意MusicStoreDB() : base("name=MusicStoreDB")中,MusicStoreDB是配置的数据库连接。
这个MvcMusicStoreDB是继承了DbContext,其作用概括来说:对模型类的修改会反映到数据库中,反之亦然,对数据库的修改也会反映到模型类中。EF实体框架会使用数据迁移来帮我们完成。
基架创建完成后,目录会发生变化,自动创建对应的view,如下:
而在StoreManagerController的Index方法中,有如下代码:
public ActionResult Index() { var albums = db.Albums.Include(a => a.Artist).Include(a => a.Genre); return View(albums.ToList()); }
这段代码的作用是用上下文将数据库中所有专辑加载到一个列表中,并将列表作为模型传递给默认的视图。其中Include是采用预加载策略,尽其所能的使用查询语句加载所有数据。而EF框架的另一种也是默认的策略是延迟加载策略,即只加载主要对象(专辑)的数据,而不填充Artist和Genre。
4.执行基架代码
4.1用实体框架创建数据库--local-DB虚拟数据空间
EF框架的代码优先方法会尽可能地使用约定而非配置(即MVC中的约定优于配置)。如果不配置从模型到数据库中表和列的具体映射,EF将使用约定创建一个数据库模式。
显式的为代码优先数据上下文配置连接很简单,即向web.config文件中添加一个连接字符串。
代码语言:javascript复制1 <connectionStrings>
4 <add name="MusicStoreDB" connectionString="Data Source=(localdb)v11.0; Initial Catalog=MusicStoreDB-20130929160340; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|MusicStoreDB-20130929160340.mdf"
5 providerName="System.Data.SqlClient" />
6 </connectionStrings>
其次,通过修改传递给DbContext的构造函数的name参数可以重写EF给定的数据库名称:
public MvcMusicStoreDB() : base("name=MvcMusicStoreDB") { }
如果不配置具体的连接,EF将尝试连接SQL Server的LocalDB实例,并且查找与DbContext派生类名相同的数据库。如果EF能够连接上数据库服务器,但找不到数据库,那么框架会自动创建一个数据库。
注意自动生成的数据库的名字和数据上下文类同名。这个数据库其实是虚拟的,它在项目的App_Data文件夹下:
具体数据库如下图所示:
注意这里除了三个model类对应的表,还有个__MigrationHistory表,EF框架使用这个表来维护代码优先模型和数据库模式一致!如果删除了这个表,就需要我们自己来维护数据库模式的修改。
4.2使用数据库初始化器--每次插入初始数据-方便项目测试
保持数据库和模型变化同步的一个简单方法是允许实体框架重新创建一个现有的数据库。可以告知EF在应用程序每次启动时重新创建数据库或者仅当检测到模型变化时重建数据库。当调用EF的Database类中的静态方法SetInitializer时,可以选择这两种策略中的任意一个。
框架中带有两个IDatabaseInitializer对象:DropCreateDatabaseAlways(每次启动时重新创建数据库)和DropCreateDatabaseIfModelChanges(仅当检测到模型变化时重建数据库)。可以根据这两个类的名称来辨别每个类所代表的策略。两个初始化器都需要一个泛型类型的参数,并且这个参数必须是DbContext的派生类。
在文件global.asax.cs中,可以在应用程序启动过程中设置一个初始化器:
源代码:
代码语言:javascript复制1 protected void Application_Start()
2 {
3 Database.SetInitializer(new MusicStoreDbInitializer());
4
5 AreaRegistration.RegisterAllAreas();
6 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
7 RouteConfig.RegisterRoutes(RouteTable.Routes);
8 BundleConfig.RegisterBundles(BundleTable.Bundles);
9 }
修改为:
代码语言:javascript复制1 protected void Application_Start()
2 {
3 Database.SetInitializer(new DropCreateDatabaseAlways<MusicStoreDB>());
4
5 AreaRegistration.RegisterAllAreas();6 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
7 RouteConfig.RegisterRoutes(RouteTable.Routes);
8 BundleConfig.RegisterBundles(BundleTable.Bundles);
9 }
4.3播种数据库
很多时候,我们在编写程序的同时需要测试,但此时数据库中没有数据,此时可以创建一个DropCreateDatabaseAlways的派生类并重写其中的Seed方法,Seed方法可以为应用程序创建一些初始化的数据。我们在Models中创建一个新的MusicStoreDbInitializer类:
播种数据:
代码语言:javascript复制 1 public class MusicStoreDbInitializer
2 : System.Data.Entity.DropCreateDatabaseAlways<MusicStoreDB>
3 {
4 protected override void Seed(MusicStoreDB context)
5 {
6 context.Artists.Add(new Artist { Name = "Al Di Meola" });
7 context.Genres.Add(new Genre { Name = "Jazz" });
8 context.Albums.Add(new Album
9 {
10 Artist = new Artist { Name = "Rush" },
11 Genre = new Genre { Name = "Rock" },
12 Price = 9.99m,
13 Title = "Caravan"
14 });
15 base.Seed(context);
16 }
17 }
这样,每次重新生成音乐商店数据库时,都会有两种流派(Jazz和Rock)、两个艺术家(Al Di Meola和Rush)和一个专辑。代码会在程序启动时注册这个初始化器。需要把文件global.asax.cs改为这样:
代码语言:javascript复制1 protected void Application_Start()
2 {
3 Database.SetInitializer(new MusicStoreDbInitializer());
4
5 AreaRegistration.RegisterAllAreas();
6 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
7 RouteConfig.RegisterRoutes(RouteTable.Routes);
8 BundleConfig.RegisterBundles(BundleTable.Bundles);
9 }
现在我们可以重新启动程序,因为我们设置的是DropCreateDatabaseAlways模式,所以如果不重启程序的话,会报错的:
错误为不能删除数据库,因为它正在使用!重启程序后,浏览器中输入URL/MvcMusicStore,可以看到默认的Index视图如下:
5 编辑专辑
5.1 创建编辑专辑的资源
默认的MVC路由规则是将HTTP GET请求中的 /StoreManager/Edit/5 传递到StoreManager控制器的Edit操作中,代码如下
1 public ActionResult Edit(int? id)
2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 Album album = db.Albums.Find(id); 8 if (album == null) 9 { 10 return HttpNotFound(); 11 } 12 ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId); 13 ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId); 14 return View(album); 15 }
黄色代码部分释疑:从数据库中得到所有的流派和艺术家列表,存在ViewBag中。
下面是商店管理器的Edit视图中用来为流派创建下拉列表的代码:
代码语言:javascript复制1 <div class="form-group">
2 @Html.LabelFor(model => model.GenreId, "GenreId",
new { @class = "control-label col-md-2" })
3 <div class="col-md-10">
4 @Html.DropDownList("GenreId", String.Empty)
5 @Html.ValidationMessageFor(model => model.GenreId)
6 </div>
7 </div>
在视图中使用DropDownList辅助方法,Edit中的两行代码就是为了构建从数据库中所有可得到的流派和艺术家的列表,并将这些列表存储在ViewBag中以方便以后让DropDownList辅助方法检索。
代码语言:javascript复制ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
- 第1个参数指定了将要放在列表中的项
- 第2个参数是一个属性名称,该属性包含当用户选择一个指定项时使用的值(键值 ,像52或2)
- 第3个参数是每一项要显示的文本
- 第4个参数包含了最初选定项的值
5.2 模型和视图模型终极版
针对专辑的编辑情形,模型对象(Album对象)并没有包含编辑专辑视图所需要的全部信息,因为另外还需要所有可能的流派和艺术家列表。针对这种问题,有两种解决方案。
基架生成代码展示了第一种解决方案:将额外的信息传递到ViewBag结构中。这个方案完全合理而且还便于实现。
第二种解决方案:强类型模型,创建一个视图特定模型的对象,将专辑信息、流派和艺术家信息传递给一个视图。这个模型可能如下定义:
代码语言:javascript复制1 public class AlbumEditViewModel
2 {
3 public Album AlbumToEdit {get; set;}
4 public SelectList Genres {get; set;}
5 public SelectList Artists {get; set;}
6 }
这样Edit操作就不需要将信息放进ViewBag,而需要实例化AlbumEditViewModel类,设置所有的对象属性,并将视图模型传递给视图。
5.3 Edit视图
当用户单击页面上的Save按钮时,HTML将发送一个HTTP POST请求,请求回到 /StoreManager/Edit/1 页面。这时浏览器会自动收集用户在表单输入中的所有信息并将这些值(及其相关的name属性值)放在请求中一起发送。这里注意input和select元素的name属性,需要和Album模型中的属性匹配。
这是Edit视图,其本质上还是一个form表单,我们后面第4篇教程会介绍HTML辅助方法:
5.4 响应编辑时的POST请求
接受HTTP POST请求来编辑信息的操作的名称也是Edit,但不同于前面看到的Edit操作,因为它有一个HttpPost操作选择器特性:
代码语言:javascript复制 1 [HttpPost]
2 [ValidateAntiForgeryToken]
3 public ActionResult Edit([Bind(Include="AlbumId,GenreId,ArtistId,Title,Price,AlbumArtUrl")] Album album)
4 {
5 if(ModelState.IsValid)
6 {
7 db.Entry(album).State = EntityState.Modified;
8 db.SaveChanges();
9 return RedirectToAction("Index");
10 }
11 ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
12 ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
13 return View(album);
14 }
这个操作的作用就是接收含有用户所有编辑项的Album模型对象,并将这个对象保存到数据库中。
(1)编辑happy path
happy path就是当模型处于有效状态并可以将对象保存到数据库时执行的代码路径。操作通过Model.IsValid属性来检查模型对象的有效性。这个属性可以看作一个信号,来确保用户输入有用的专辑特性值。
如果模型处于有效状态if(ModelState.IsValid) ,则执行以下的代码:
db.Entry(album).State = EntityState.Modified;
这行代码告知数据上下文该对象在数据库中已经存在,所以框架应该对现有的专辑应用数据库中的值而不要再创建一个新的专辑记录。
db.SaveChanges();
^上下文生成一条SQL UPDATE命令更新对应的字段值以保留新值。
2)编辑sad path
sad path就是当模型无效时操作采用的路径。在sad path中,控制器操作需要重新创建Edit视图,以便用户更改自身产生的错误,而ASP.NET MVC5默认提供了客户端校验,如图所示:
我们此时可以分别以Genre:流派类和Artist:艺术家类为模型,使用基架功能,创建他们的CRUD功能:
6 模型绑定
Model Binding(模型绑定) 是 HTTP 请求和 Action 方法之间的桥梁,它根据 Action 方法中的 Model 类型创建 .NET 对象,并将 HTTP 请求数据经过转换赋给该对象。
简单来说,模型绑定的作用:自动从视图的Form集合提取网页的属性值,比如name属性,然后存储到模型类(如Album)中,也就是说,当模型绑定器读取到Album具有Name属性时候,自动在请求中寻找名为Name的参数,然后我们可以直接用name这个变量即可。即自动寻值,直接使用。模型绑定分为隐式模型绑定(DefaultModelBinder)和显式模型绑定(UpdateModel)。
ASP.NET MVC通过模型绑定(Model Binding)机制来解析客户端传送过来的数据,解析的工作由DefaultModelBinder类进行处理。若要自定义ModelBinder类行为,需实现IModelBinder接口。
简单模型绑定:Action的参数在Action被执行时会通过DefaultModelBinder从form或QueryString传送过来的数据进行处理,即将传送过来的字符串型的数据转换成对应的.Net类,并将其输入Action。如图示例:
复杂模型绑定:在ASP.NET MVC中,可以通过DefaultModelBinder类将form数据对应到复杂的.NET类,即模型。该模型可能是一个List<T>类或一个含有多个属性的自定义类。从客户端传送过来的form数据会通过DefaultModelBinder类自动创建Product类对象,将form字段通过.NET的Reflection(反射)机制一一对应到对象的同名属性中。如图示例:
模型绑定数据验证:ASP.NET MVC在处理模型绑定时,会处理Model的数据验证。模型绑定的数据验证失败,则Controller的ModelState.IsValid验证值为false。
可以使用ModelState.AddModelError()方法在Controller中判断更加复杂的业务逻辑,并自定义错误信息至ModelState。
使用Bind属性限制可被更新的Model属性:复杂模型绑定的验证,在默认情况下,不管Model中有多少字段,只要客户端form有数据传送过来就会自动进行绑定。在ASP.NET MVC中可以通过使用Bind属性限制可被更新的Model属性。如绑定多个字段中的部分字段:通过Bind属性来定义Model中需要绑定哪些字段。Exclude:不包括的自动绑定的属性,多个属性,使用逗号(,)分隔:
使用Include指定需要绑定的字段:
如果不希望在每个Action的参数中都应用Bind属性,可以在Model定义中指定:
当绑定引发异常时,使用UpdateModel()方法会直接抛出异常。使用TryUpdateModel()方法,则会在验证成功时返回true,失败或发生异常时返回false:
这里由于作者对此也了解不深,不多讲了,以免误导读者,同时我找了几篇不错的文章,大家可以参考看看:
https://www.cnblogs.com/wyh19941210/p/8320115.html
https://blog.csdn.net/duyelang/article/details/50363659
本系列文章所有实例代码GitEE地址:
https://gitee.com/jahero/mvc
参考文章:
https://www.cnblogs.com/imstrive/p/6518064.html
https://www.cnblogs.com/wyh19941210/p/8320115.html
https://blog.csdn.net/duyelang/article/details/50363659