资源描述
传统的MVC概念
模型:组类,描述了要处理的数据以及修改和操作数据的业务规则
视图:定义应用程序用户界面的显示方式
控制器:一组类,用来处理来自用户,整个应用程序流以及特定应用程序逻辑的通信
MVC在web框架中的应用
模型:模型是描述程序设计人员感兴趣问题域的一些类,这些类通常封装存储在数据库中的数据,以及操作这些数据和执行特定域业务逻辑的代码。在ASP.NET MVC中,模型就像是一个使用了某个工具的数据访问层,包括实体框架。
视图:一个动态生成HTML页面的模板
控制器:一个协调视图和模型之间关系的特殊类。它响应用户输入,与模型进行对话,并决定呈现哪个视图。
约定优于配置
控制器
MVC中的控制器主要用来响应用户的输入,并且在响应时通常会修改模型.通过这种方式MVC关注的是应用程序流.输入数据的处理.以及对相关视图输出数据的提供.在MVC中,URL告诉路由机制去实例化哪个控制器,调用哪个操作方法,然后决定使用哪个视图.MVC提供的是方法调用的结果,而不是动态生成的页面.
整个项目的结构
HomeControler:负责网站根目录下的”home page”和”about page”
AccountControler:响应与账户相关的请求,比如登陆和账户注册
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "欢迎使用 ASP.NET MVC!";
return View();
}o
public ActionResult About()
{
return View();
}
}
这个类非常的简单,继承与Controler基类,HomeControler类的Index方法负责决定当浏览网站首页时触发的事件。
控制器一般以Controller结尾
public class StoreController : Controller
{
//
// GET: /Store/
public string Index()
{
return "Hello from Store.Index()";
}
public string Browse()
{
return "Hello from Store.Browse()";
}
public string Details(string name)
{
return "Hello from Store.Details():Name"+name;
}
}
Controller URL操作中的参数.
/Store/Browse?name=liuzhongdong
public string Details(string name)
{
return "Hello from Store.Details():Name"+name;
}
Name可以自动检测。
return HttpUtility.HtmlEncode("Hello from Store.Details():Name"+name);
利用实用方法HttpUtility.HtmlEncode来预处理用户输入。这能阻止用户向视图中用链接注入JavaScript代码或者html标记。
也可以使用/Srore/Deatails/5,URL路由可以自动检测到值。
控制器就好像是浏览器再直接调用控制器类中的方法。类方法和参数都被具体化为URL中特定路径片段或查询字符串,结果就是一个返回给浏览器的字符串。这就进行了极大的简化,而忽略了下面这些细节:
l 路由将URL映射到操作的方式
l 将试图作为模板生成向浏览器返回的字符串(通常是html格式)
l 操作很少返回原始的字符串,它通常返回合适的ActionResult来处理像HTTP状态码和调用视图模板系统这样的事项。
视图
视图的职责是向用户提供用户界面。向它提供对模型的引用后,它会将模型转换为准备提供给用户的格式。在ASP.NET MVC中,这个过程由两部分组成。第一个部分是检查由控制器提交的ViewDataDictionary(通过ViewData)属性访问。另外一部分是将其内容转换为HTML格式。视图并不一定只是渲染html,视图还可以渲染其他的内容。
从ASP.NET MVC3开始,视图数据可以通过ViewBag属性访问。ViewBag属性是动态的,它语法简单,可以通过访问ViewData属性访问的相同数据。它是一个高效地利用C#4中新的dynamic关键字的封装器,其中封装了ViewData。这样就可以使用类似属性访问的方法来检索字典中的数据。
所以ViewBag.Message就等同于ViewData[“Message”]
注意:如果在ViewData[“ this is a key”]中存放一个值,那么将不能使用ViewBag访问这个值。另外应该知道的一点是,这个动态的值不能作为一个参数传递给扩展方法。因为C#编译器为了选择正确的扩展方法,在编译时必须知道每个参数的真正类型。
@Html.TextBox("name", (string)ViewBag.Name)或者@Html.TextBox("name", ViewData["Name"]);因为动态类型只有在编译时才确定。
在强类型视图的情形下,ViewDataDictionary拥有一个视图渲染的强类型模型对象。这个模型可能代表了实际的域对象,或者它可能是一个视图专有的呈现模型对象。为了方便起见,这个模型对象可以通过视图的Model属性进行引用。
视图总是被一个控制器渲染,该控制器向它提供了要渲染的数据
public ActionResult Details()
{
ViewBag.Message = "Hello World.Welcome to ASP.NET MVC";
return View();
}
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Details</title>
</head>
<body>
<h1>@ViewBag.Message</h1>
<div>
<p>
This is a sample view.It is not much to look at,But is gets the job done.
</p>
</div>
</body>
</html>
指定视图
按照约定,每个控制器在Views目录下面都有一个对应的文件夹,其名称与控制器一样。只是没有Controller后缀名。在每一个控制器的View文件夹中,每一个操作方法都有一个名称相同的视图文件与之对应。这就提供了操作方法与视图关联的基础。
public ActionResult Details()
{
ViewBag.Message = "Hello World.Welcome to ASP.NET MVC";
return View();
}
默认就会去对应的文件夹中找到与操作名称相同的视图。但是这个约定是可以重写的。
return View("Index");
使用上述方式,也只会在控制器对应的View文件夹里查找,如果要在不同的视图目录中查找,可以使用
return View("~/Views/Store/Details.cshtml");
强类型视图
强类型视图指给视图指定一个实体对象或者对象集合,也就是所谓的数据。那么视图这个时候就可以展现数据。
实体类Person
Controller代码
public ActionResult Details()
{
List<Person> persons = new List<Person>();
persons.Add(new Person() { PersonId=1, FirstName="Liu", LastName="Zhongdong" });
persons.Add(new Person() { PersonId = 2, FirstName = "Ma", LastName = "Jun" });
persons.Add(new Person() { PersonId = 3, FirstName = "Yang", LastName = "Huan" });
persons.Add(new Person() { PersonId = 3, FirstName = "Zhou", LastName = "Yajie" });
ViewBag.Persons = persons;
return View();
}
View代码
<ul>
@foreach (Person p in ViewBag.Persons)
{
<li>PersonId:@p.PersonId FirstName:@p.FirstName LastName:@p.LastName</li>
</ul>
这里传递类型到视图的方式是使用ViewBag。然后在视图里面去迭代。
可以使用View的重载方法传递模型实例来指定模型
return View(persons);
在后台,传进View方法的值将赋给ViewData.Model属性。接下来是告诉视图那种类型的模型正在使用@model声明。注意,这里需要使用类型的完全限定名。
@using FirstMVCTest.Models;
@model IEnumerable<Person>
@foreach (Person p in Model)
{
<li>PersonId:@p.PersonId FirstName:@p.FirstName LastName:@p.LastName</li>
}
对于在视图中经常使用的名称空间,一个比较好的方式就值在Views目录下的web.config文件中声明。
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="FirstMVCTest.Models"/>
</namespaces>
</pages>
视图模型
视图通常需要显示各种没有映射到域模型的数据,例如,可能需要视图来显示单个商品的详细信息,但是同以试图也要显示商品附带的其他信息,显示视图与主模型无关的额外数据的一种简单的方法就是把这些数据存放在ViewBag属性中,这样是可以实现的,但是并非对每一个人都实用,如果想要严格控制流进视图的数据,就必须使所有的数据都要强类型的。以便视图编写人员可以使用智能感知功能。可能采用的一个方法就是编写自定义的视图模型类,这里的视图模型不是MVVM里面的VM。这里的视图模型之的是视图特定模型。相当于为视图提供数据模型的类,而不是特定于域的模型。
public class PersonSchoolViewModel
{
private School _mySchool;
public School MySchool
{
get { return _mySchool; }
set { _mySchool = value; }
}
private IEnumerable<Person> _persons;
public IEnumerable<Person> Persons
{
get { return _persons; }
set { _persons = value; }
}
}
View里面要显示的就是既包括了School又包括Person的数据
添加视图
1,在控制器的方法里面右键添加
也可以在解决方案管理器中直接添加
视图名称:指定视图的名称。
视图引擎:指定是使用aspx或者razor渲染view(生成HTML)。
创建强类型视图:是否在创建视图的时候选择一个模型类
支架模板:
l Empty:创建一个空视图,使用@model语法指定模型类型
l Create:创建一个视图,其中带有创建模型新实例的表单,并为模型类型的每一个属性生成一个标签和编辑器
l Delete:创建一个视图,其中带有删除现有模型实例的表单,并为模型的每一个属性显示一个标签以及当前该属性的值
l Details:创建一个视图,它显示了模型类型的每一个属性的标签及其相应值
l Edit:创建一个视图,其中带有编辑现有模型的表单,并为模型的每个属性生成一个标签和编辑器
l List:创建一个带有模型实例表的视图.为模型类型的每一个属性生成一列.确保操作方法向视图传递的是IEnumerable<YourModelType>类型.同时为了执行创建/编辑/删除操作,视图中还包含了指向操作的链接.
创建为分部视图:选择这个选项意味着要创建的视图不是一个完整的视图,因此,layout选项是不可用的.对于Razor视图引擎来说,生成的分部视图除了再其顶部没有<html>标签和<head>标签之外,很像一个常规视图.
使用布局或母版页:这个选项决定了要创建的视图是否引用布局(或母版页),或是成为一个完全独立的视图.对于Razor视图引擎来说,如果选择使用默认布局.就没有必要指定一个布局了,因为在_ViewStart.cshtml文件中已经指定了布局.这个选项是用来重写默认布局文件的
自定义T4视图模板
上面提到的视图模板是由C:\Program Files\Microsoft Visual Studio 11.0\Common7\IDE\ItemTemplates\CSharp\Web\MVC 3\CodeTemplates\AddView\CSHTML中的文件提供的.可以自定义这些模板,毕竟简单的一种方式是直接重写这些文件,但是会对所有的项目产生影响.所以我们可以把这些文件导入到我们自己的项目中.
但是这里会有一个编译错误
解决的方式是清空所有模板的自定义工具属性
Razor视图引擎
用Razor代码来渲染视图,有点是干净,轻量级,简单.减少了语法障碍.
Razor中的核心字符是@.这个单一字符用作标记-代码的转换字符,有时也反过来用作代码-标记的转换字符.这里一共有两种基本类型转换:代码表达式和代码块.求出表达式的值,然后将值写入到响应中.
<h1>Listing @stuff.Lenght items</h1>
表达式@stuff.Lenght是作为隐式代码表达式求解的,然后在输出中显示表达式的值。这里不需要指出代码表达式的结束位置,Razor十分智能,它能知道后面的空格符不是一个有效的标示符。
但是在有些时候,Razor也是存在二义性的。
@{
string rootNamespace = "MyApp";
}
<span>@rootNamespace.Model</span>
这个时候会提示string没有Model属性,其实我们想打印的是MyApp.Model,但是Razor不能理解我们的意图,而会认为@rootNamespace.Models是代码表达式。幸亏Razor还可以通过将表达式用圆括号括起来来支持显示代码表达式,这样就告知了Razor,.Model是字面量文本,而不是代码的一部分。
下面了解一下Razor在显示电子邮件时的情况
<span>Edrickliu@</span>
乍看之下会觉得有错误,因为@看起来像是一个企图打印出变量microsoft的com属性的有效代码表达式。但是Razor足够智能,可以辨别出电子邮箱的一般模式。这可以适应大多数的情况,在一些有效的电子邮件地址可能会显示不出来的时候,可以用两个@@来转义。但是这种情况也会存在问题,比如,我们要求下面的表达式
<li>Lenght@rootNamespace.Length</li>
这种情况下Razor会逐步打印,因为它会把它匹配成为一个电子邮件。但是我们希望的结果是
<li>Lenght3</li>
这里Razor再次有了二义性,那么我们就可以使用圆括号,任何时候Razor有了二义性,都可以使用圆括号指明想要的内容
到目前为止,我们还有一个二义性没有解决,比如微博的时候,我们要打印@Edrick,@Majun等等。Razor会尝试去解析这么隐式代码表达式,但是会以失败告终,这种情况,我们应该使用@@来转义。
HTML编码
因为在许多情况下都需要用视图显示用户输入。所以总是存在潜在的跨站脚本注入攻击。但是值得称赞的是Razor表示是是用HTML编码的
@{
string message = "<script>alert('haacked')</script>";
}
<span>@message</span>
这段代码不会弹出警告对话框。而会显示编码消息
<script>alert('haacked')</script>
然而在javascript中将用用户提供的值赋给变量时,要使用javascript字符串编码而不仅仅是HTML编码。要使用
@Ajax.JavaScriptStringEncode(message);
代码块
当我们需要在页面中输入多行代码的时候需要用到代码块
@{
string rootNamespace = "MyApp";
string message = "<script>alert('haacked')</script>";
}
Foreach为代码块。另外一个例子需要用代码块的是调用了没有返回值的方法
@{Html.RenderPartial("");}
Razor和纯文本
@if (isShowMessage)
{
<text>This is Span text</text>
@:This is Span text Too!
}
以上使用text标签或者@:都可以。
注释
@* *@
调用泛型方法
@(Html.SomeMothed<A Type>())
布局
Razor中的布局有助于使应用程序中的多个视图保持一致的外观.布局与ASP.NET中的母版页是作用是相同的.可以使用布局为网站定义公共模板(或只是其中的一部分).公共模板包含了一个或者多个占位符.应用程序中的其他视图为他们提供内容.从某些角度看,布局很想视图的抽象基类.
<div class="page">
<header>
<div id="title">
<h1>我的 MVC 应用程序</h1>
</div>
<div id="logindisplay">
@Html.Partial("_LogOnPartial")
</div>
<nav>
<ul id="menu">
<li>@Html.ActionLink("主页", "Index", "Home")</li>
<li>@Html.ActionLink("关于", "About", "Home")</li>
</ul>
</nav>
</header>
<section id="main">
@RenderBody()
</section>
这就是一个最简单的布局,看起来像一个标准的Razor视图,但是需要注意的是再视图中有一个@RenderBody调用.这是一个占位符,用来标记使用这个布局的视图将渲染他们的主要内容的位置.现在多个视图可以利用这一布局显示一致的外观
@{
Layout = "~/Views/Shared/_Layout.cshtml";
View.Title= " xxx";
}
<footer>
@RenderSection("Footer", false);
</footer>
@section Footer{
This is the <strong>Footer</strong>
}
完成布局中的其他节。
那么如果每一个视图都使用layout属性来指定它的布局。如果多个视图使用同一个布局,那么会产生冗余,很难维护。那么解决的方法是_ViewStart.cshtml可以消除这种冗余。这个文件优先于同目录下任何视图代码的执行。这个文件也可以递归地应用到子目录下的任何视图。
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
当然,在视图中可以重写这种设置。
模型
模型:应用程序关注的域,模型就是要保存,创建,更新,和删除的对象。
我们可以使用工具来为每个模型对象的标准索引,创建,编辑和删除功能构建控制器和视图。这个构建工作称为基架。我们可以在Model文件夹下面创建我们的模型。下面我们开始构建两个模型。Album和Artist
/// <summary>
/// 专辑模型类
/// </summary>
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; }
}
/// <summary>
/// 艺术家模型类
/// </summary>
public class Artist
{
public virtual int ArtistId { get; set; }
public virtual string Name { get; set; }
}
/// <summary>
/// 流派模型类
/// </summary>
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; }
}
为商店管理器构建基架
商店管理器其实是一个可以用来编辑专辑信息的控制器。我们来创建一个控制器
基架
MVC中的基架可以为应用程序的创建,读取,更新和删除功能生成所需的样板代码。基架模板检测模型类的定义,然后生成控制器以及该控制器的关联视图。基架知道如何命名控制器,视图以及每个组件需要执行什么代码。(如果不喜欢默认的基架,可以根据自己的需要自定义基架模板,也可以通过NuGet查找可替代的基架模板。)基架不是必须的,但是它最起码可以代劳在正确位置创建文件的操作。节省开发人员的时间
MVC有3个基架可以选择。
l 空控制器:会向Controller文件夹中添加一个具有指定名称且派生自Controller的类。这个控制器带有唯一的操作就是Index操作。这个模板不会生成任何视图
l 包含空的读写操作的控制器:这个模板会向项目中添加一个带有Index,Details,Create,Edit和Delete操作的控制器,虽然控制器内部的操作不是完全空白,但是,他们不会执行任何有实际意义的操作。也不创建视图
l 包含读写操作和视图的控制器(使用EntityFramerwork):它不仅生成带有整套Index,Details,Create,Edit和Delete操作的控制器及其需要的所有相关视图,而且还生成与数据库交互的代码。为了让模板产生合适的代码,需要指定一个模型类,为了生成数据访问代码。基架需要一个数据上下文对象。这里可以指定一个现有的,也可以根据需要创建一个新的。
EF4.1会随MVC3一起安装,EF4.1支持代码优先的开发风格,代码优先指的是可以在不创建数据库模式,也不打开VS设计器的情况下载SQL SERVER中存储或检索信息。我们在实体类中的属性都是虚拟的,虚拟属性不是必须的,但是他们会给EF提供一个纯指向C#类集的钩子,并为EF启动一些新特性,如高效的修改跟踪机制。实体框架需要知道模型属性的修改时刻,因为它要在这一刻生成并执行一个SQL UPDAGTE语句,使这些改变和数据库保持一致。
当使用EF的代码优先方法时,需要使用从EF的DBContext类派生出的一个类来访问数据库。该类具有一个或者多个DBSet<T>类型的字段。类型T表示一个要持久保存的对象。
Content代码
public class MusicStoreContext : DbContext
{
public MusicStoreContext() : base("name=MusicStoreContext")
{
}
public DbSet<Album> Albums { get; set; }
public DbSet<Genre> Genres { get; set; }
public DbSet<Artist> Artists { get; set; }
}
Controller部分代码
public class StoreManagerController : Controller
{
private MusicStoreContext db = new MusicStoreContext();
//
// GET: /StoreManager/
public ViewResult Index()
{
var album = db.Albums.Include(a => a.Genre).Include(a => a.Artist);
return View(album.ToList());
}
//
// GET: /StoreManager/Details/5
public ViewResult Details(int id)
{
Album album = db.Albums.Find(id);
return View(album);
}
…..
}
var album = db.Albums.Include(a => a.Genre).Include(a => a.Artist);
这句代码为预先加载,加载跟Album相关的Genre和Artist。还有一种方式为延迟加载。
一旦完成了基架,下面我们就可以来看看视图了。运用这个基架,MVC会帮我们生成对应的视图
执行基架的代码(生成数据库)
EF的代码优先的方法会尽可能地使用约定而非配置。如果不配置从模型到数据中表和列的具体映射。EF将使用约定创建一个数据库模式。如果在运行时不配置一个具体的数据库连接,那么EF将按照约定创建一个连接。
配置连接
显示地为代码优先于数据上下文配置连接很简单,即向web.config文件中添加一个连接字符串。该字符串的名称必须与数据上下文类的名称一样。
<add name="MusicStoreContext" connectionString="Data Source=(localdb)\v11.0; Initial Catalog=MusicStoreContext-20130613183211; Integrated Security=True; MultipleActiveResultSets=True; AttachDbFilename=|DataDirectory|MusicStoreContext-20130613183211.mdf"
providerName="System.Data.SqlClient" />
这里为MVC为我们生成的配置为localDB,那是因为在Application_Start里面设置了Database.DefaultConnectionFactory。如果我们要改变这一配置,只需要改变我们的配置文件
如果不配置具体的连接,那么EF将尝试使用默认的数据库模式进行连接。然后查找数据库,如果找不到数据库则创建新的数据库,如果找到数据库,则依据数据库初始化器设置来创建或者不创建数据库
数据库初始化器
保持数据库和模型变化同步的一个简单的方法时允许实体框架重新创建一个现有的数据库。可以告知EF是在应用程序每次启动时重新创建数据库或者仅当检测到模型变化时重新创建
Database.SetInitializer<FirstMVCTest.Models.MusicStoreContext>(new DropCreateDatabaseIfModelChanges<FirstMVCTest.Models.MusicStoreContext>());
或者
Database.SetInitializer<FirstMVCTest.Models.MusicStoreContext>(new DropCreateDatabaseAlways<FirstMVCTest.Models.MusicStoreContext>());
Database.SetInitializer为创建数据库的初始策略,而DropCreateDatabaseAlways,
DropCreateDatabaseIfModelChanges为具体的策略。一种是任何时候只要应用程序启动就创建,一种是当模型改变才创建
但是不管我们使用哪种形式创建数据库。我们的数据都会被清空,那么我们可以设置一些初始化数据。我们可以创建自己的策略类,这个类来继承与DropCreateDatabaseAlways或者DropCreateDatabaseIfModelChanges
public class MusicStoreDBInitializerAlways:DropCreateDatabaseAlways<MusicStoreContext>
{
protected override void Seed(MusicStoreContext 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.9m,
Title="Caravan" });
base.Seed(context);
}
}
public class MusicStoreDBInitializerAlwaysIfModelChange:DropCreateDatabaseIfModelChanges<MusicStoreContext>
{
protected override void Seed(MusicStoreContext 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.9m,
Title = "Caravan"
});
base.Seed(context);
}
}
调用
Database.SetInitializer<MusicStoreContext>(new MusicStoreDBInitializerAlwaysIfModelChange());
在编辑试图的时候,有些关联数据我们需要从数据库里面取到,然后显示给用户,比如专辑相关联的流派,艺术家等等,那么我们有两种办法解决
l ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);基架是可以理解模型之间的关联,然后从现有的数据中选择
l 使用ViewModel
{
public Album AlbumToEdit { get; set; }
public SelectList Genres { get; set; }
public SelectList Artists { get; set; }
}
然后选择合适的数据。
响应编辑时的Post请求
我们可以看到在生成的Controller中,有两个Edit方法。一个是用来获取信息,一个是用来提交更改,因为它具有[HttpPost]特性。
编辑Happy path和sad path
Hayyp path:就是当模型处于有效状态并可以将对象保存到数据库时执行的代码路径,操作通过ModelState.IsValid属性来检查模型对象的有效性。如果模型处于有效的状态,那么Edit操作将会执行下面的一行代码
db.Entry(album).State = EntityState.Modified;
完整代码如下
[HttpPost]
public ActionResult Edit(Album album)
{
if (ModelState.IsValid)
{
db.Entry(album).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(db.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(db.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
Sad path:当模型无效时操作采取的路径。一般来说就是错误验证和提示
模型绑定
由上面的代码我们可以看到,Edit方法的参数为Album类型的对象,而不是传统的从表单挖取值。当操作带有一个参数
展开阅读全文