视图
3.1视图的作用
视图的职责是向用户提供用户界面,向它提供对模型的引用后,它会将模型转换为准备提供给用户的格式。
在ASP.NET MVC中这个过程由两部分组成:
检查控制器提交的ViewDataDictionary(通过ViewData属性访问),另一部分是将其内容转换为HTML格式
从ASP.NET MVC 3开始,视图数据可以通过ViewBag属性访问,该属性是动态的,语法简单,可以访问通过ViewData属性访问的相同数据。封装了ViewData,因此可以用类似访问属性的语法来检索字典中的值,如:
ViewBag.Message等同于ViewData["Message"]
两者的差异:1、只有当要访问的关键字是一个有效的C#标识符时,ViewBag才其作用
如:在ViewBag["Key With Spaces"]中存放一个值,那么将不能使用ViewBag访问改值
2、该动态值不能作为一个参数传递给扩展的方法,因为C#编译器为了选择正确的扩展方法,在编译时必须知道每个参数的类型。
如:@Html.TextBox("name",ViewBag.Name)会编译失败,如果想编译通过有两种方法1、使用ViewData["Name"],2、(string)ViewBag.Name
在强类型视图中,ViewDataDictionary拥有一个视图渲染的强类型模型对象,该模型可能代表了实际的域对象,为方便起见,该模型对象可以通过视图的Model属性进行引用。
如:@{
Layout=null;
}
控制器中的控制行为:
Public ActionResult Sample(){
ViewBag.Message="Hello World";
Return View("Sample");
}
注意:该控制器将ViewBag.Message属性设置为一个字符串,然后返回一个名为Sample的视图,该视图将显示传递给ViewBag.Message属性的值
3.2指定视图
默认情况下在控制器中的操作方法返回的View()视图即与控制器名去掉Controller同名的视图,如果需要向其指定不同的视图则需在View(“OtherView")中指定要返回的视图名称即可
在一些情况下需要指定完全位于不同的目录结构中的视图,这时可以用带有~符号的语法来提供视图的完整路径,如:
Public ActionResult Index()
{
ViewBag.Message="Hello World";
Return View("~/Views/Example/Index.cshtml");
}
3.3强类型视图
例:编写一个显示Album列表的视图,实现如下:
Public ActionResult List()
{
Var ablums=new List<Album>();
For(int i=0;i<5;i++)
{
albums.Add(new Album{ Title="Olive"+i});
}
ViewBag.Albums=albums;
Return View(albums);
}
在视图中显示如下:
<ul>
@foreach(var a in (ViewBag.Albums as IEnumerable<Album>))
{
<li>@a.Title</li>
}
</ul>
但是在强类型视图中,可以这样实现:
Public ActionResult List()
{
Var ablums=new List<Album>();
For(int i=0;i<5;i++)
{
albums.Add(new Album{ Title="Olive"+i});
}
Return View(albums);
}
视图中:
@model IEnumerable<MvcMusicStore.Models.Album>
<ul>
@foreach(var p in Model)
{
<li>@p.Title</li>
}
</ul>
3.4视图模型
视图通常需要显示各种没有直接映射到域模型的数据,可以通过编写自定义的视图模型类来实现
3.5添加视图
l Scaffold Template
① Empty 创建一个空视图,使用@model语法指定模型类型
② Create 创建一个视图,其中带有创建模型新实例的表单,并为模型类型的每一个属性生成一个标签和编辑器
③ Delete 创建一个是视图,其中带有删除现有模型实例的表单,并为模型的每一个属性显示一个标签以及当前该属性的值
④ Details 创建一个视图,显示了模型类型的每一个属性的标签及相应值
⑤ Edit 创建一个视图,并带有编辑现有模型实例的表单,并未模型类型的每一个属性生成一个标签和编辑器
⑥ List 创建一个带有模型实例的表单,为模型 类型的每一个属性生成一列,确保操作方法向视图传递的是IEnumerable<OliveType>类型,同时为执行创建/编辑/删除操作,视图还包含了指向操作的链接
l Reference script libiaries:指示创建的视图是否包含指向Javascript的文件集,当创建Create或Edit视图时,就需要选中这些项,如果实现客户端的验证也必须选中这些项
3.6Razor视图引擎
3.6.3代码表达式
例:@{ string root="Olive";}
想要输出结果为<span>Olive.Models</span>
但是在实际的运行中会报错,提示string没有Models的属性,可以通过将表达式用圆括号括起来的方式进行解决,即:
期望的输出结果为<li>Item_3</li>
但是实际输出的是<li>Item_@item.Length</li>
出现这样的情况是因为Razor将其识别为邮箱地址了,
我们只需加上圆括号即可,如:<li>Item_@(item.Length)</li>
此外如下想要输出@号,可以使用两个@@用来转义
3.6.4Html编码
在许多情况下需要用视图显示用户的输入,但是这样存在潜在的跨站脚本攻击,但是Razor表达式默认用HTML编码
如下:@{string message=“<scritp>alert('Olive')</script>";}
<span>message</span>
这段代码则不会弹出提示框,显示如下:
<span><script>alert('Olive');<script></span>
如果想展示HTML标记,则需返回System.Web.IHtmlString对象的实例,Razor不对它进行编码,也可用Html.Row来显示
@{
String message=“<scritp>alert('Olive')</script>";}
<span>@Html.Row(message)</span>
这样就可以显示弹框了
与此同时,在Javascript中将用户提供的值赋给变量时,要使用Javascript字符串编码而不是HTML编码,也就是用@Ajax.JavaScriptStringEncode方法来对用户的输入进行编码的
这样就可以有效的避免跨站脚本的攻击,如下:
<script type="text/javascript">
$(function(){
Var message='Hello @Ajax.JavaScriptStringEncode(ViewBag.UserName)';
$("#message").html(message).show('slow');
});
</script>
3.6.5 Razor语法示例
1、隐式代码表达式
Razor中隐式表达式总是采用HTML编码方式
2、显示代码表达式
<span>ISBN@(isbn)</span>
3、无编码代码表达式
使用Html.Row方法来确定内容不被编码
<span>@Html.Row(model.Message)</span>
4、代码块
@{
Int x=123;
String y=”because";
}
5、文本和标记组合
@foreach(var item in items)
{ <span>Item @item.Name.</span>}
6、混合代码和纯文本
@if(show)
{
<text>This is Olive</text>
}
或者
@if(show)
{
@This is Olive
}
3.6.6布局
ASP.NET MVC 3中的布局相当于WebForm中的母版页
如下布局页中部分代码:
<div id=“main-content">@RenderBody()</div>
其中的@RenderBody()相当于WebFrom中的placeholder占位符
布局使用示例:
@{
Layout="~</Views/Shared/Layout.cshtml";
}
<span>This is main content!</span>
此外,布局中还可能有多个节,如下:部分布局页代码示:
<div id="main-conten">@RenderBoday()</div>
<footer>@RenderSeciton("Footer")</footer>
如果对视图页不做任何改变则会报错,这里需要对视图页做如下修改
@{
Layout="~</Views/Shared/Layout.cshtml";
}
<p>This is a main content!</p>
@section Footer{
This is the <strong>footer</strong>
}
@section语法为布局中定义的一个节指定了内容,在默认的情况下视图必须为布局中定义的节指定内容,但是RenderSection方法有一个重载版本,允许指定不需要的节,即为required参数传递一个false值来标记Footer节是可选的,如下:
<footer>@RenderSection("Footer",false)</footer>
也可以定义一些薯条中没有定义节时的默认内容,方法如下:
<footer>
@if(IsSectionDefined("Footer"))
{
RenderSection("Footer");
}
Else
{
<span>This is the default footer.</span>
}
</footer>
3.6.7 ViewStart
创建默认的ASP.NET MVC 3时,会自动生成_ViewStart.cshtml文件,指向了一个默认的布局,如果一组视图有共同的设置,则_ViewStart.cshtml文件便很有用,但是如果需要另选择布局,则需要重新指定视图的Layout属性如下:
@{
Layout="~/Views/Share/others.cshtml";
}
3.7指定分部视图
除了返回视图外,操作方法也通过PartialView方法以PartialViewResult的形式返回分部视图:
如:
Public ActionResult Message()
{
ViewBag.Message="This is Olive";
Retrun PartialView();
}
如果布局是由_Viewstart.cshtml页面指定的,则布局无法渲染。
分部视图多用于AJAX技术中的部分更新情形
如:使用JQuery将一个分部视图内容加载到使用AJAX调用当前视图中:
<div id="result></div>
<script type="text/javascript">
$(function(){
$('#result').load('/home/message');
});
</script>
3.8视图引擎
首先来了解下ASP.NET MVC 3的生存周期,
控制器本身并不渲染视图,它仅准备暑假,并返回一个ViewResult实例来决定显示哪个视图,控制器基类包含一个名为View的简单方法来返回一个ViewResult实例,在视图引擎后台ViewResult被调用到当前的视图引擎中来渲染该视图
第四章 模型
本章要讨论的是那些发送信息到数据库,执行业务计算并在视图中渲染的模型对象。这些对像代表着应用程序的关注的域,模型就是想要保存、创建、更新和删除对象
4.2.1基架的概念
ASP.NET MVC中基架可以为应用程序的创建、读取、更新和删除(CRUD)提供所需的样板代码。基架模板检测模型类的定义,然后生成控制器以及该控制器控制的相关视图。
ASP.NET MVC3共有三个模板可供选择:
l Empty Controller:该模板会向Controller文件夹中添加一个具有指定名称且派生自Controller类的控制器,该控制器仅带有Index操作,且在内部仅返回一个默认的ViewResult实例,不会生成任何视图
l Controller with Empty Read/Write Actions:该模板会向项目中添加一个带有Index、Details、Create、Edit和Delete操作控制器,但是还需自己为其添加代码,实现操作,并为其创建视图
l Controller with Read/Write Actions and Views,Using Entity Framework:该模板生成整套的带有Index、Details、Create、Edit和Delete操作控制器,以及相关的所有视图,还生成了与数据库交互的代码。该模板需要指定选择合适的类模型(基架检测会检测所选择的模型的所有属性,然后根据这些信息来创建控制器、视图、数据库操作等代码)和数据上下文对象名称
4.2.2基架和实体框架
EF(Entity Framework)是一个对象关系映射框架,可以在关系型数据库中保存对象,也可以利用LINQ查询语句检索那些保存关系型数据库中的.NET对象
我们之前所建的模型类中所有的属性都是虚属性,意思就是该属性不是必须的,但是它们给EF提供一个指向纯C#类集的钩子,并为EF启用了一些特性,EF需要知道模型属性值的修改时刻,在这一刻生成UPDate语句,使这些改变和数据库保持一致。
l 代码优先约定
EF对于外键关系、数据库名称等有约定,这些约定取代了以前需要提供给一个关系对象映射框架的所有映射和配置
l DbContext
在使用代码优先约定时,需要使用从EF的DbContext类派生出一个类来访问数据库。该类可以有一个以上的的DbSet<T>类型的字段,类型DbSet<T>的每一个T代表一个想要持久保存的对象
4.2.3执行基架模板
1、数据上下文
上文提到的从DbContext类继承而来的类,如下:
public class MusicStoreDB : DbContext { /// <summary> /// 用来存储Album类型的对象,相当于数据库中的Album表 /// </summary> public DbSet<Album> Albums { get; set; } /// <summary> /// 用来存储Artist类型的对象,相当于数据库中的Artist /// </summary> public DbSet<Artist> Artists { get; set; } /// <summary> /// 用来存储Genre类型的对象,相当于数据库中的Genre表 /// </summary> public DbSet<Genre> Genres { get; set; } }
如果需要访问数据库,只需要实例化这个数据上下文类即可。
2、加载相关对象
① 预加载:尽其所能的使用查询语句加载所有数据
② 延迟加载:根据需求来 加载相关数据
4.2.4执行基架代码
1、用实体框架创建数据库
EF的代码优先尽可能的使用约定而非配置,如果不配置从模型到数据库表的映射,EF将用约定创建一个数据库模式,如果不配置具体数据库连接,则EF按约定自己创建一个连接
EF连接到服务器,如果找不到数据库则会创建一个数据库,该数据库中EdmMetadata表是EF用来确保模型类和数据库模式同步的。如果修改模型类如添加或删除模型类的一个属性,那么EF要不根据模型类重新创建数据库,要不报出就抛出异常。
2、使用数据库初始化器
保持数据库和模型变化同步的一个简单方法是允许实体框架重新创建一个现有的数据库可以告知EF在应用程序每次重启或者检测到模型变化时重建数据库。
调用EF的Database类静态的SetInitializer方法时,需要为其传递一个IDataBaseInitializer对象,而框架中带有两个IDatabaseInitializer对象:DropCreateDatabaseAlways和DropCreateDatabaseIfModelChanges,两个初始化器都需要一个泛型类的参数,并且这个参数必须是DbContext的派生类:
示例:在每次启动程序时设置一个初始化器
在global.asax.cs内部Application_Star()方法中添加一句代码
Database.SetInitializer(new DropCreateDatabaseAlways<MusicStoreDB>)());
3、播种数据库
如果需要在程序运行时有一些初始数据,可以创建DropCreateDatabaseAlways类并重写Seed方法
如下:
public class MusicStoreDbInitializer : DropCreateDatabaseAlways<MusicStoreDB> { /// <summary> /// 重写Seed方法为程序提供原始数据 /// </summary> /// <param name="context"></param> protected override void Seed(MusicStoreDB context) { context.Artists.Add(new Artist { Name = "Olive" }); context.Genres.Add(new Genre { Name = "Classic" }); context.Albums.Add(new Album { Artist = new Artist { Name = "Only" }, Genre = new Genre { Name = "Pop" }, Price = 9.99m, Title = "Only for you" }); base.Seed(context); }
这个时候我们还需要在Application_Start事件中添加如下代码:
Database.SetInitializer(new MusicStoreDbInitializer());
总的来说我们只需要做三步的工作即可:
1、实现模型类
2、为控制器和视图构建基架
3、选择数据库初始化策略
基架只是为应用程序的特定部分提供了起点,在此基础上可以根据自己的喜好做修改。
4.3编辑专辑
4.3.1创建编辑专辑的资源
在public ActionResult Edit(int id)
{
Album album = db.Albums.Find(id);
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
高亮的代码是为了构建从数据库中得到流派和艺术家列表,将这些列表存在ViewBag中以便填充DropDownList辅助方法检索
SelectList类用于代表构建下拉表需要的数据,该构造函数第一个参数指定了要放在列表中的项,第二个参数是一个属性名,该属性包含当用户选择一个指定的项时使用的值,第三个参数是每一项要显示的文本(“PoP”)最后,第四个参数包含了最初选定的项的值
4.3.2响应编辑时的POST请求
该操作有一个Album模型对象参数,并将该参数对象保存到数据库中
1、编辑happy path
Happy path就是当模型处于有效的状态并且可以将对象保存到数据库时执行的代码路径,操作通过ModelState.IsValid属性来检查模型对象的有效性,如果模型处于有效状态,则:
db.Entry(album).state=EntityState.Modified;,该代码告知该信息已存在,这时执行对其进行更新即可。
2、编辑sad path
Sad path 当模型无效时操作采用的路径,在sad path中,控制器操作需要重新创建Edit视图,以使用户改正自身产生的错误。
4.4模型绑定
4.4.1DefaultModelBinder
当操作带有参数时,MVC运行环境会使用一个模型绑定器来构建这个参数,默认模型绑定器为DefaultModelBinder,例如在Album对象情形中,默认的绑定器检查Album类,并查找能用于绑定的所有Album属性。当模型绑定器看到Album有Title属性时,它就在请求中查找名为“Title”的参数,模型绑定器使用值提供器的组件请求在不同区域中查找参数,可以查看路由数据、查询字符串、表单集合等
4.4.3显示模型绑定
当操作中有参数时,模型绑定器会隐式的工作。可以使用控制器中的UpdateModel和TryUpdateModel方法显示的调用模型绑定,如果在模型绑定期间出现错误或者模型无效则UpdateModel则会抛出异常,但是使用TryUpdateModel方法则不会抛出异常,该方法会返回一个布尔值,true表示模型绑定成功,false表示失败。模型绑定会产生模型状态,模型绑定器移近模型中的每一个值在模型状态中都会有一条相应的记录,绑定后可以随时查看模型状态以检查模型绑定是否成功。