前言
本篇博文主要介绍ASP.NET MVC中的三个核心元素:控制器、视图与模型,以下思维导图描述了本文的主要内容。
控制器
控制器简介
在介绍控制器之前,简单的介绍一下MVC工作原理:URL告知路由机制该使用哪个控制器(Controller),调用该控制器中的哪个方法(Action),并为该方法提供需要的参数。控制器响应用户的输入,在响应时修改模型(Model),并决定使用哪个视图(View),并对该视图进行渲染。
注意:MVC模式提供的是方法调用结果,而不是动态生成的页面。
以上内容对于初学者来说可能不太理解,不过没关系,经过后面的学习,待我们对MVC的整体架构有了一定的认识,再返回头来看这部分内容,便很好理解了。
控制器是MVC模式中的三个核心元素之一,主要负责相应用户的输入、对输入数据的处理以及对相关视图输出数据的提供。
控制器基础
首先,我们看一下上一篇《ASP.NET MVC5(一):ASP.NET MVC概览》创建的新项目MyFirstMvcProject中默认包含的几个控制器类:
- HomeController:负责网站根目录下的Home Page、About Page和Contact Page。
- AccountController:响应与账户相关的请求,如登录和注册。
- ManageController:响应启用外部服务认证的相关页面。
展开MyFirstMvcProject项目的Controller目录,打开HomeController.cs文件,代码如下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MyFirstMvcProject.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
这是一个非常简单的控制器类,它继承自Controller基类。Index方法负责决定当浏览网站首页时触发的事件。
控制器的创建
创建一个新的控制器,步骤如下:
- 右击Solution Eplorer下的Controllers文件夹,选择Add|Controller选项。
- 选择MVC 5 Controller - Empty模板,点击Add按钮。
- 将控制器命名为FirstController,点击Add按钮。
此时,已经创建了一个名为FirstController的控制器,并且控制器类中已经包含了一个Index方法,我们将这个方法稍作修改(将返回类型修改为String类型,然后将返回值修改为This is my First controller !),代码如下:
public string Index()
{
return "This is my first controller !";
}
启动项目,修改Url浏览到/First/Index,然后返回响应的字符串,如下图。
前面的例子返回的是字符串常量,下一步,则是通过响应Url传进来的参数动态的执行操作。修改Index方法如下:
public string Index(string message)
{
return "This is my first controller ! The message is '" + message + "'";
}
启动项目,修改Url浏览到/First/Index?message=I'm message。
由上述演示可以得出以下结论:
- 判断一个类是否为控制器的方式,就是看这个类是否继承自System.Web.Mvc.Controller。
- 不需要做额外的设置,直接修改Url浏览到First/Index就可以执行First类中的Index方法,这就是操作中的路由。
视图
视图简介
视图的作用是向用户提供可视的用户界面。当控制器针对被请求的Url执行完合适的逻辑后,就将要显示的信息(模型)委托给视图。这一过程分两部操作,检查模型对象以及将内容转换为HTML格式。
视图约定
视图约定:在与控制器同名的View文件夹下,每一个操作方法都有一个同名的视图文件与其对应,这就提供了视图与操作方法关联的基础。
注意:以上约定可以重写,如果希望操作方法渲染不同的视图,只需向其提供一个不同的视图名称,在使用这种语法时,必须提供视图文件的扩展名。我们在FirstController中添加Contact方法,使其渲染Home下的Contact页面。
public ActionResult Contact()
{
return View("~/Views/Home/Contact.cshtml");
}
启动示例,将Url定位到/First/Contact,可以看出渲染的页面是Home下的Contact视图。
视图创建
创建视图最简单的方法就是找到Views文件夹下与Action对应的位置,点击右键Add|View。不过Visual Studio中提供了一种更为方便的创建视图的方法,只需在Controller对应的方法上右击,选择AddView项,即可创建视图。
强类型视图
在MVC中,控制器与视图之间的数据传递可以使用ViewBag、ViewData,示例如下:
假设现在需要编写一个视图,显示所有的用户信息,首先,在Models文件夹下创建一个UserInfo类。
namespace MyFirstMvcProject.Models
{
public class UserInfo
{
public int UserId { get; set; }
public string UserName { get; set; }
public string RegisterDate { get; set; }
}
}
在FirstController类下添加List方法:
public ActionResult List()
{
var userInfo = new List<UserInfo>()
{
new UserInfo {UserId = 1,UserName = "Aaron",RegisterDate = "2015-08-08" },
new UserInfo {UserId = 2,UserName = "Bob",RegisterDate = "2010-03-23" },
new UserInfo {UserId = 3,UserName = "Hunter",RegisterDate = "2014-09-12" }
};
ViewBag.UserInfo = userInfo;
return View();
}
创建视图,在视图中迭代并显示用户信息:
<table class="table">
@foreach (var item in ViewBag.UserInfo) {
<tr>
<td>
@item.UserId
</td>
<td>
@item.UserName
</td>
<td>
@item.RegisterDate
</td>
</tr>
}
</table>
运行程序,效果图如下:
ViewBag的不足:向视图传递少量数据时,使用ViewBag很容易完成,当需要传递实际的对象时,就变的不太方便,主要体现在无法使用智能感知、获得强类型和编译时检查。此时,就可以使用强类型视图。
修改FirstController下的List方法:
public ActionResult List()
{
var userInfo = new List<UserInfo>()
{
new UserInfo {UserId = 1,UserName = "Aaron",RegisterDate = "2015-08-08" },
new UserInfo {UserId = 2,UserName = "Bob",RegisterDate = "2010-03-23" },
new UserInfo {UserId = 3,UserName = "Hunter",RegisterDate = "2014-09-12" }
};
return View(userInfo);
}
重新编译该项目后,修改视图时发现已经可以获得智能感知。
运行程序,效果图如下:
补充:ViewBag和ViewData的区别
- ViewBag是Dynamic类型的对象,ViewData是字典类型的对象。
- 只有当关键字是一个有效的C#标识符时,才可以使用ViewBag,例如:ViewData["User Name"],关键字中包含空格,此时若使用ViewBag无法通过编译。
- ViewBag动态值不能通过参数传递给扩展方法。例如: @Html.TextBox("name",ViewBag.UserName)无法通过编译,此时可以使用ViewData["UserName"],或 @Html.TextBox("name",(String)ViewBag.UserName)。
Razor视图引擎
Razor的概念:Razor是一个干净的、轻量级的、简单的视图引擎,不包含原有的Web Forms视图引擎的语法累赘,最大限度地减少语法和额外的字符。Razor通过理解标记的结构来实现代码和标记之间尽可能顺畅地转换。
Razor中的核心转换字符是 @符号,使用这个字符用来做标记-代码的转换字符。如上一节中的示例,Razor视图引擎可以根据 @符号自动识别<td>标记以及item.UserId代码。
<td>
@item.UserId
</td>
Razor视图引擎除了支持代码表达式外,还支持代码块。
@foreach (var item in Model) {
<tr>
<td>
@item.UserId
</td>
<td>
@item.UserName
</td>
<td>
@item.RegisterDate
</td>
</tr>
}
布局
布局的使用有助于使应用程序中的多个视图保持一致的外观,作用与Web Forms中的模板页相同。下面是ASP.NET MVC5新建项目时默认布局的部分代码(位于Views/Shared/_Layout.cshtml)。
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
@Html.Partial("_LoginPartial")
</div>
</div>
</div>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
视图中 @RenderBody相当于一个占位符,用来标记使用这个布局的视图将渲染它们的位置。
当创建一个默认的ASP.NET MVC项目时,Views目录下会自动添加一个_ViewStart.cshtml文件,这个文件会先于任何视图运行,如果一组视图拥有共同的设置,我们就可以在_ViewStart.cshtml中进行统一设置。
模型
本节,我们将通过一个简单的示例来讲述如何使用EntityFramework来操作模型。
为产品Product建模
在Model文件夹中添加一个新的Product类,右击Models文件夹,选择Add|Class,将新类命名为Product,并添加响应的属性:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace MyFirstMvcProject.Models
{
public class Product
{
//产品编号
public int Id { get; set; }
//产品名称
public string ProductName { get; set; }
//产品描述
public string Description { get; set; }
//产品价格
public decimal Price { get; set; }
}
}
为产品管理器构造基架
ASP.NET MVC5包含各种基架模板,不同的模板的代码生成量不同。首先,介绍一下ASP.NET MVC5中的一些常用的基架模板:
- MVC 5 Controller - Empty:
向Controller文件夹添加一个仅带有Index操作的Controller类,且在其内部出了返回一个默认的ViewResult实例的代码之外,没有其他任何代码。 - MVC 5 Controller with read/write Actions:
向项目中添加一个带有Index、Details、Create、Edit和Delete操作的控制器。 - MVC 5 Controller with Views,Using Entity Framework:
不仅生成带有整套Index、Details、Create、Edit和Delete操作的控制器,而且还生成了与数据库交互的代码。
使用基架为产品Product创建控制器,步骤如下:
- 右键Controller文件夹,选择Add|Controller。
- 在弹出的Add Scaffold选择MVC 5 Controller with Views,Using Entity Framework,点击Add按钮。
- 在Add Controller对话框中,修改控制器名称为ProductsController。
- 单击Data Context class右侧的+按钮,将数据库上下文名称修改为ProductStoreDB,单击Add按钮。
- 确认完成后回到Add Controller对话框,点击Add为Product类添加ProductsController及相关视图。
使用基架为模型创建控制器时,注意添加模型类后要重新编译该项目,否则在创建时Visual Studio可能会抛出异常
待创建完成后,修改Web.Config文件中的数据库连接字符串,使其连接到希望连接的数据库(本例中连接到本机的SQL Server数据库)
运行程序,将Url浏览到/Products/Index,因为此时还没有新增过产品,所以列表中没有数据。
点击Create New链接,在添加页面依次输入ProductName、Description、Price,点击Create,返回/Products/Index页面,数据添加成功。
数据库初始化与播种
保证数据库与模型变化同步最简单的办法就是允许实体框架重新创建一个现有的数据库。可以修改Global.asax文件,调用EF的Database类(位于命名空间System.Data.Entity)中的静态方法SetInitializer,告知EF在应用程序启动时重新创建数据库或仅当检测到模型变化时重建数据库(DropCreateDatabaseAlways和DropCreateDatabaseIfModelChanges)。
protected void Application_Start()
{
//启动应用程序时重新创建数据库
Database.SetInitializer(new DropCreateDatabaseAlways<ProductStoreDB>());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
在实际开发项目时,我们总希望数据库中能带有一些数据,以便开发时用来测试。下面介绍播种数据库(seeding the database)来实现这种情况,在Models文件夹下创建一个DropCreateDatabaseAlways类的派生类,重写其中的Seed方法,为应用程序创建一些初始的数据。
public class ProductStoreDbInitializer : DropCreateDatabaseAlways<ProductStoreDB>
{
protected override void Seed(ProductStoreDB context)
{
context.Products.Add(new Product
{
ProductName = "Apple Pencil",
Description = "iPad Pro - Apple Pencil - Apple",
Price = 99m
});
base.Seed(context);
}
}
在应用程序启动代码部分注册这个初始化器,如下所示:
protected void Application_Start()
{
//启动应用程序时重新创建数据库,并播种数据库
Database.SetInitializer(new ProductStoreDbInitializer());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
重启并运行应用程序,在浏览器中导航到/Products/Index,此时不必输入就有数据可用。