服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - ASP.NET教程 - 详解Asp.Net MVC——控制器与动作(Controller And Action)

详解Asp.Net MVC——控制器与动作(Controller And Action)

2020-04-24 14:25SeeYouBug ASP.NET教程

这篇文章主要介绍了详解Asp.Net MVC——控制器与动作(Controller And Action) ,具有一定的参考价值,感兴趣的小伙伴们可以参考一下。

一、理解控制器

 

1.1、什么是控制器

控制器是包含必要的处理请求的.NET类,控制器的角色封装了应用程序逻辑,控制器主要是负责处理请求,实行对模型的操作,选择视图呈现给用户。

简单理解:实现了IController接口,修饰符必须是public,不能是抽象的,不能是泛型的,类名必须以Controller结尾。

详解Asp.Net MVC——控制器与动作(Controller And Action)

在MVC框架中,控制器类必须实现System.Web.Mvc命名空间下的IController接口,如上图所示,这是一个非常简单的接口,该接口仅有一个Execute方法,当请求该控制器时Execute方法被调用。通过实现IController接口,你可以创建控制器类。

 

1.2、控制器的作用

a、每一个针对应用程序的请求,都是通过控制器自由地选择合适的方式来处理的,只要它不偏离到视图(View)和模型(Model)所负责的区域。

b、不要把业务或数据存储的逻辑放到控制器里面,也不要创建用户接口。

 

1.3、创建实现IController接口的控制器

示例: 创建一个实现Icontroller接口的类,读取路由数据,并生成数据写入响应。

在Controllers文件夹下创建一个名为MyFirstController的类,实现IController接口并添加如下代码

详解Asp.Net MVC——控制器与动作(Controller And Action)

运行该应用程序并在地址栏导航到/MyFirst,便可以看到此控制器产生的输出。

详解Asp.Net MVC——控制器与动作(Controller And Action)

创建一个类通过实现IController接口,MVC框架会将其视为一个控制器,并将请求发送给它,而且在如何处理和响应请求上没有任何限制,这是一个很好的示例,因为它向你展示了MVC框架的可扩展性,但用这种方式编写一个复杂的应用程序是非常困难的。

 

1.4、创建继承于Controller类的控制器

通过System.Web.Mvc.Controller类你可以派生你的控制器,System.Web.Mvc.Controller类是大多数Web开发人员需要熟悉的,用来对请求处理提供支持的一个类,Controller提供了以下三个关键特性。

(1)、动作方法(Action Method):一个控制器的行为被分解成多个方法(而并非只有唯一的Execute()方法)。每个动作方法被暴露给不同的URL,并通过从输入请求提取的参数进行调用。

(2)、动作结果(Action Result):你可以返回一个描述动作结果的对象(例如:渲染一个视图,或重定向到一个不同的URL或动作方法),然后通过该对象实现你的目的。这种指定结果和执行之间的分离简化了单元测试。

(3)、过滤器(Filter):你可以把可重用的行为封装成过滤器,然后通过在代码中添加特性的的方式,把这种行为标注到一个过多个控制器或动作方法上。

 除非在头脑中有一个非常明确的需求,否则创建控制器最好的办法就是通过Controller类进行派生,这也正是你在Visual Studio中添加一个控制器,Visual Studio为你所做的事情。

在Controllers文件夹下创建一个名为MySecondController的类,继承与Controller类,然后添加一个动作方法TestAction并编写如下代码返回一个动作结果,最后再该动作方法内右键添加对应的视图。

详解Asp.Net MVC——控制器与动作(Controller And Action)

运行应用程序并导航到/MySecond/TestAction浏览结果如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

作为Controller类的一个派生类,所要做的工作是实现动作方法、获取所需要的各种输入,以对请求进行处理,并生成一个适当的响应。后面的内容将介绍数据的接收与响应。

 

二、控制器对数据的接收

 

2.1、数据来源

a、查询字符串值 b、表单数据 c、路由数据

控制器需要经常访问来自输入请求的数据,如查询字符串表单数据、以及由路由系统根据输入的URL解析得到的参数的值。访问这些数据有三种主要方式。

(1)、从上下文对象提取。

(2)、作为参数被传递给动作方法(Action Method)而形成的数据。

(3)、明确调用框架的模型绑定(Model Binding)功能。

注意:参数名称是忽略大小写的,如Request["Test"]与Request["test"]结果是一样的。如下图:

View部分

详解Asp.Net MVC——控制器与动作(Controller And Action)

Controller部分

详解Asp.Net MVC——控制器与动作(Controller And Action)

 

2.2、通过上下文对象获取数据

当创建一个从Controller基类派生的控制器时,就能够访问一组非常便利的属性,这些属性包括:Request、Response、RouteData、HttpContext、Server等,每一个属性都包含了请求不同方面的的信息。在Action方法里是可以使用任何Context对象来访问这些属性。例如:

 
 1  public ActionResult Index()
 2         {
 3             string userName = User.Identity.Name;
 4             string serverName = Server.MachineName;
 5             string clientIP = Request.UserHostAddress;
 6             DateTime dateStamp = HttpContext.Timestamp;
 7 
 8             string oldProductName = Request.Form["OldName"];
 9             string newProductName = Request.Form["NewName"];
10 
11             ViewBag.Message = "本机的IP是:" + clientIP;
12             return View();
13         } 
 

可以利用VS智能感知,在动作方法中输入this.找到这些可用的上下文信息。

详解Asp.Net MVC——控制器与动作(Controller And Action)

 

2.3、使用动作(Action)方法参数

 

2.3.1、使用Action方法参数

 详解Asp.Net MVC——控制器与动作(Controller And Action)

下面的方式是不是比上面的方法更优雅易读呢?不过需要注意的是:Action方法里面是不允许有ref或out参数的,虽然编译不会报错但运行时会抛出一个异常。如下所示。

MySecond控制器代码如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

Index视图代码如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

运行结果如下:

 详解Asp.Net MVC——控制器与动作(Controller And Action)

 

2.3.2、理解参数对象实例化

Controller基类使用叫做“值提供器(Value Provider)”和“模型绑定器(Model Binder)”的MVC框架组件来获取动作方法的参数值。值提供器将可用的数据项集合呈现给控制器。有一组内建的值提供器从Request.For

m、Request.QueryString、Request.Files、RouteData.Values获取数据项,然后这些值会被传递给模型绑定器,模型绑定器会尝试将这些数据映射成动作方法参数的数据类型。默认的模型绑定能够创建和填充任何.NET类型的对象,包括自定义的类型和集合。

 

2.3.3、理解可选参数与必须的参数

如果MVC框架找不到引用类型参数(如:string或object)的值,动作方法仍然会被调用,但对该参数会使用一个null值,若找不到值类型参数(如:int或double)的值则会抛出一个异常,并且不会调用动作方法。

a、值类型参数是必须被赋值的。如果想让此参数和引用类型参数一样,可以定义成int?如:public ActionResult Index(int? num),当依然没有值时,不会发生异常,而是会传递null值。

b、引用类型的参数是可选的。为了使它成为必须的(保证一个非空的值被传递),在动作(Action)方法上添加一些代码拒绝null。例如,在该值等于null时,抛出一个ArgumentNullException异常。

 

2.3.4、指定默认参数值

如果希望处理不含动作方法参数的请求,但又不想在代码中检查null值或抛出异常,可以使用C#的可选参数特性来代替。如下所示:

 
1  public ActionResult List(string query = "all", int page = 1)
2         {
3             //此处省略代码N行...
4             return View();
5         }
 

在定义参数时,通过对参数赋值的办法,可以将参数标记为可选的,如上诉代码中,给query和page参数提供了默认值。MVC框架会试图通过请求为这些参数获取值,但如果没有值可用,那么将用所指定的默认值代替。

对于string类型参数query,注意string类型是引用类型,这意味着不需要检查null值。如果请求为指定查询字符串,那么该动作(Action)方法将以字符串“all”进行调用。对于int类型参数,注意int类型是值类型,在没有page值时,请求不会导致错误,该方法将以默认值“1”进行调用。

 

三、控制器对数据的响应

 

3.1、理解动作结果(Action Result)

原理:

a、MVC框架接收从Action方法返回的ActionResult对象,并调用定义在ActionResult类里面ExecuteResult方法,对ActionResult进行实现,为我们处理Response对象并生成相关的输出。

b、MVC框架内置了许多ActionResult类型,都是从ActionResult类派生的,能够方便我们在Action方法里面选择具体的返回类型,比如要呈现到View,可以选择ViewResult作为Action方法的返回值。

MVC框架含有许多内置的动作结果类型如下图所示,所有这些类型都是派生于ActionResult,其中在Controller类中有便利的辅助器方法。下面将解释如何使用这些结果类型。

MVC框架支持的输出类型有: 1.视图 2.文本数据 3.XML数据 4.JSON数据 5.文件或者二进制数据 6.返回错误和HTTP Codes 7.定制的ActionResult 8.重定向

详解Asp.Net MVC——控制器与动作(Controller And Action)

 

3.2、通过渲染视图(View)返回HTML

最常见的来自Action方法的响应就是生成HTML并发送给浏览器,为了使用动作结果(ActionResult)生成HTML,需要创建一个指定了要呈现的视图ViewResult类的实例。我们在Home控制器中编写如下代码.代码中指定了HomePage的视图。

详解Asp.Net MVC——控制器与动作(Controller And Action)

当MVC框架调用ViewResult对象的ExecuteResult时,就会开始对指定的View进行搜素。

使用了区域(Area),则搜索顺序如下:

1、/Area/<AreaName>/Views/<ControllerName>/<ViewName>.aspx

2、/Area/<AreaName>/Views/<ControllerName>/<ViewName>.ascx

3、/Area/<AreaName>/Views/Shared/<ControllerName>/<ViewName>.aspx

4、/Area/<AreaName>/Views/Shared/<ControllerName>/<ViewName>.ascx

5、/Area/<AreaName>/Views/<ControllerName>/<ViewName>.cshtml

6、/Area/<AreaName>/Views/<ControllerName>/<ViewName>.vbhtml

7、/Area/<AreaName>/Views/Shared/<ControllerName>/<ViewName>.chtml

8、/Area/<AreaName>/Views/Shared/<ControllerName>/<ViewName>.vbhtml

如果没有使用区域(Area)或者在前面找不到则会在下面搜索,搜索顺序如下:

1、/Views/<ControllerName>/<ViewName>.aspx

2、/Views/<ControllerName>/<ViewName>.ascx

3、/Views/Shared/<ControllerName>/<ViewName>.aspx

4、/Views/Shared/<ControllerName>/<ViewName>.ascx

5、/Views/<ControllerName>/<ViewName>.cshtml

6、/Views/<ControllerName>/<ViewName>.vbhtml

7、/Views/Shared/<ControllerName>/<ViewName>.chtml

8、/Views/Shared/<ControllerName>/<ViewName>.vbhtml

只要有一个视图找到,就停止搜索,并开始将找到的视图(View)呈现给客户端。

通过路径来指定呈现的视图(View)

这种命名约定的方法非常方便和简捷,但是它限制了我们所能呈现的一些视图。如果要呈现一个具体的视图,可以提供一个明确的路径,下面是一个例子。

详解Asp.Net MVC——控制器与动作(Controller And Action)

当我们这样指定一个视图时,指定的路径必须以“/”或者“~/”并且包含扩展名(如:.aspx)。当然不推荐这样来使用,因为这不利于程序的扩展和维护,这是一种绑定或耦合,有违MVC设计思想,可以有其他的方法来达到同样的效果例如:使用RedirectToAction()方法。

View辅助方法

View():返回到Action同名的视图。

View(“viewName”):返回到此控制器的ViewName视图。

View(“~/views/othercontroller/viewname.cshtml”):这种方式必须以~/或者/开头,但并不建议这么做,因为可以调用RedirectToAction这样的方法。

View(“viewname”,”layout”):呈现这个视图的时候,换一个母版页。

 

3.3、将数据从动作(Action)方法传递给视图(View)

传输数据的几种方式:1.视图模型  2.ViewData  3.ViewBag  4.TempData

 

3.3.1、提供视图模型对象

将一个对象作为View方法的参数传递给视图。例如:

Controller部分

 
1 public ViewResult Index2()
2 {
3    DateTime date = DateTime.Now;
4    return View(date);
5 }
 

View部分,添加视图时,选择Razor视图引擎。

 
1 @{ 
2     ViewBag.Title = "Index2"; 
3 } 
4 <h2>Index</h2> 
5 The day is: @(((DateTime)Model).DayOfWeek)  
 

上面的视图是一个没有类型或者说是弱类型的视图,它不知道关于视图模型对象的任何信息,并且将它作为object对象的实例进行处理。为了得到DayOfWeek属性的值,需要将object对象的实例强转为DateTime,这样做能够实现效果,但却让视图变得杂乱。

我们可以通过创建强类型的View来改进,即在View里指定视图模型对象的类型,只需要添加代码:@model DateTime,如下所示:

 
1 @model DateTime
2 @{ 
3     ViewBag.Title = "Index2"; 
4 } 
5 <h2>Index</h2> 
6 The day is: @Model.DayOfWeek 
 

运行结果如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

可以发现,使用强类型的视图不仅让视图变得整洁,而且方便我们编码,因为对属性有智能感知。如下图所示:

 详解Asp.Net MVC——控制器与动作(Controller And Action)

 

3.3.2、使用ViewBag传递数据

在前面我们已经使用过了ViewBag视图包这个特性,该特性允许你在一个动态对象上定义任意属性,并能够在视图里面访问。如下所示:

Controller部分

 
1  public ViewResult Index2()
2         {
3             ViewBag.Message = "Hello ViewBag!";
4             ViewBag.Date=DateTime.Now;
5             return View();
6         }
 

View 部分

 
 1 @{
 2     Layout = null;
 3 }
 4 
 5 <!DOCTYPE html>
 6 
 7 <html>
 8 <head>
 9     <meta name="viewport" content="width=device-width" />
10     <title>Index2</title>
11 </head>
12 <body>
13     <div>
14         the message is:@ViewBag.message<br/>
15          the day is:@ViewBag.Date.DayOfWeek
16     </div>
17 </body>
18 </html>
 

运行效果如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

相对于视图模型对象方面,ViewBag有一个优点,即它能够很容易地发送多个对象到视图。假如我们被限制只能使用视图模型,那么为了实现相同的效果需要创建一个新的类型具有stringDateTime两个类型的的成员。使用动态对象可以在视图中输入属性和方法调用的任意序列。

 

3.3.3、使用ViewData传递数据

ViewData是在MVC3之前的版本中出现的,主要的功能类似于ViewBag,但ViewData是使用ViewDataDictionary类实现的而不是一个动态的类型,ViewDataDictionary类是一个常规的键/值对的集合,并通过Controller类的ViewData属性访问。如下示例:

Controller部分

 
1  public ViewResult Index2()
2         {
3             ViewData["message"] = "Hello ViewBag!";
4             ViewData["Date"]=DateTime.Now;
5             return View();
6         }
 

View部分

 
 1 @{
 2     Layout = null;
 3 }
 4 
 5 <!DOCTYPE html>
 6 
 7 <html>
 8 <head>
 9     <meta name="viewport" content="width=device-width" />
10     <title>Index2</title>
11 </head>
12 <body>
13     <div>
14         the message is:@ViewData["message"]<br/>
15         the day is:@(((DateTime)ViewData["Date"]).DayOfWeek)
16     </div>
17 </body>
18 </html>
 

运行结果与VIewBag运行结果一致。

上面的代码中,我们看到ViewData需要对object对象进行类型转换,现在有了ViewBag以后,推荐使用ViewBag,并且尽量使用强类型视图和视图模型。

 

3.3.4、使用TempData传递数据

在【3.4.4、使用TempData保留重定向数据】节中会介绍使用TempData传递数据,这里不作介绍。

 

3.4、执行重定向

有一种动作(Action)方法的通常结果并不是直接产生输出,而是把用户的浏览器重定向导另一个URL。大多数情况下,这个URL是应用程序的另一个动作(Action)方法,用来生成你希望用户看到的输出。

重定向的Action方法不产生任何的输出,只是让浏览器重新请求一个其它的URL。在MVC程序中,一般会定向到其它的Action方法来产生输出。

在执行重定向时,发送了两个HTTP代码中的一个到浏览器。

(1)、发送HTTP 302状态编码,代表暂时重定向。(常用类型)

(2)、发送HTTP 301状态编码,表示永久重定向。(使用需谨慎)

重定向的类型有:1.重定向到文本URL   2.重定向到路由系统的URL  3.重定向到动作(Action)方法

 

3.4.1、重定向到文本URL

对浏览器重定向最基本的方式是调用Redirect方法,它返回RedirectResult类的一个实例。如下示例:

详解Asp.Net MVC——控制器与动作(Controller And Action)

如果希望重定的URL被表示成一个字符串,并作为参数传递给Redirect方法。Redirect方法发送的是一个临时重定向。可以用RedirectPermanent方法发送一个永久重定向。例如:

 详解Asp.Net MVC——控制器与动作(Controller And Action)

 

3.4.2、重定向到路由系统的URL

使用重定向到文本URL的缺点是:限定了URL,当路由发生改变后,必须更新URL。幸好我们可以使用路由系统,用RedirectToRoute方法来生成有效的URL该方法会创建 RedirectToRouteResult的一个实例。如下所示:

详解Asp.Net MVC——控制器与动作(Controller And Action)

 

3.4.3、重定向到动作(Action)方法

使用RedirectToAction方法能够很优雅的重定向到一个Action方法,这个方法仅仅是对RedirectToRoute方法的封装,让你指定Action方法和控制器的值,而不需要创建一个匿名类型。如下所示:

详解Asp.Net MVC——控制器与动作(Controller And Action)

如果想重定向到其他的控制器,需要提供一个控制器的名称如:

详解Asp.Net MVC——控制器与动作(Controller And Action)

注意:传入的Action参数或控制器参数在它们被传递给路由系统之前是不会被验证的,所以要确保目标控制器和Action方法是存在的。

 

3.4.4、使用TempData保留重定向数据

 重定向会引起浏览器提交整个新的HTTP请求,这意味着无法访问原始的请求的细节。如果想将数据从一个请求传递到下一个请求,可以使用TempData()方法。

TempData类似于Session,不同的是TempData在被读取以后会被标记删除,并且当请求被处理的时候被移除。这是一个针对想在整个重定向过程中保持短期数据的非常完美的安排。如下示例:

首先添加一个MySecond控制器,并添加对应的视图,视图引擎为Razor并编写如下代码:

 MySecond控制器代码

 
 1  public class MySecondController : Controller
 2     {
 3         public ActionResult Index()
 4         {
 5             TempData["Message"] = "Hello TempData";
 6             TempData["Data"] = "TempData的值只能读取一次";
 7             return View();
 8         }
 9         public ActionResult TempDataTest()
10         {
11             return View();
12         }
13     }
14     
 

Index视图代码

 
 1 @{
 2     Layout = null;
 3 }
 4 
 5 <!DOCTYPE html>
 6 
 7 <html>
 8 <head>
 9     <meta name="viewport" content="width=device-width" />
10     <title>Index</title>
11 </head>
12 <body>
13     <div>
14         the Data is:@TempData["Data"]
15     </div>
16 </body>
17 </html>
 

TempDataTest视图代码

 
 1 @{
 2     Layout = null;
 3 }
 4 
 5 <!DOCTYPE html>
 6 
 7 <html>
 8 <head>
 9     <meta name="viewport" content="width=device-width" />
10     <title>TempDataTest</title>
11 </head>
12 <body>
13     <div>
14         the Data is:@TempData["Data"]<br/>
15         the Message is:@TempData["Message"]
16     </div>
17 </body>
18 </html>
 

运行结果如下:

详解Asp.Net MVC——控制器与动作(Controller And Action)

详解Asp.Net MVC——控制器与动作(Controller And Action)

注意:在Index里面没有读取TempData["Message"]的值,但是Index和TempDataTest都读取了TempData["Data"]的值。这样做是为了验证TempData里面的值在不同的视图只能读取一次。

TempData里面的值在一次请求里面只能读取一次。 如果我们想读取TempData的值但是又不让它被删除,可以使用TempData.Peek(“Data”)方法。 如果想在保持一次TempData里面的值,可以使用TempData.Keep("Data")。

 

3.5、返回文本数据

 详解Asp.Net MVC——控制器与动作(Controller And Action)

Content方法参数说明: 发送的文本数据、响应的HTTP content-type header、编码格式。 可以忽略最后两个参数,在MVC框架假定数据是HTML(content type为text/html)情况下,它会查询一种浏览器声明支持的编码格式。

 

3.6、返回XML数据

 

 1 using LinqService;
 2 using System.Xml.Linq;
 3 public ContentResult GetXMLData()
 4 {
 5     List<Books> books = bll.GetListBooks();
 6     XElement data = new XElement("BooksList", books.Select(e =>
 7     {
 8        return new XElement("Books",
 9          new XAttribute("title", e.Title),
10          new XAttribute("author", e.Author),
11          new XAttribute("Price", e.Price.ToString()));
12     }));
13 }
 
 

3.7、返回JSON数据

 
 1   public ActionResult JSON()
 2         {
 3             JsonResult json = new JsonResult();
 4 
 5             UserInfo userinfo=new UserInfo{ ID=1, Name="张三", Age=11 };
 6 
 7             List<UserInfo> list=new List<UserInfo>{
 8                 new UserInfo{ ID=1, Name="张三", Age=11 },
 9                 new UserInfo{ ID=1, Name="张三", Age=11 },
10                 new UserInfo{ ID=1, Name="张三", Age=11 },
11                 new UserInfo{ ID=1, Name="张三", Age=11 }
12             };
13             //json.Data = list;
14             //json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
15             //return json;
16             //return new JsonResult() { Data = list, JsonRequestBehavior=JsonRequestBehavior.AllowGet };
17 
18             return  Json(userinfo,JsonRequestBehavior.AllowGet);
19         }
 

JSON是一个轻量级的基于文本格式,用来描述分层数据结构的, JSON是有效的javascript代码,这意味着被所有的主流浏览器支持

在MVC框架里面内置了JsonResult类,让我们能够序列化.NET对象为json格式。通过Json方法可以创建返回JsonResult结果类型

 

3.8、返回文件和二进制数据

 
1 public FileResult AnnualReport()
2 {
3    string filename = @"D:C#高级编程.doc";
4    string contentType = "application/doc";
5    string downloadName = "C#高级编程2013.doc";
6    return File(filename, contentType, downloadName);
7 }
 

如果不知道具体的MIME类型,可以指定contentType 为application/octet-stream

lFileResult是一个抽象基类,其三个实现的子类 :

1.FilePathResult :直接把服务器的某个路径下的文件发给浏览器

2.FileContentResult :发送一个内存中的二进制数据给浏览器

3.FileStreamResult:发送已经打开的文件流内容给浏览器。

 

3.9、返回错误和HTTP Codes 

发送特定的HTTP结果码

可以使用HttpStatusResult类将一个特定的HTTP状态码发送给浏览器。这个类没有对应的控制器辅助器方法,因此必须直接对这个类进行实例化,如下所示:

1 public HttpStatusCodeResult StatusCode()
2 {
3      return new HttpStatusCodeResult(404,"file not found");
4 }

发送404结果

可以使用便利的HttpNotFoundResult类来获取上面相同的效果,这个类派生于HttpStatusResult,而且可以用控制器的便利方法HttpNotFound来创建。如下所示:

1  public HttpStatusCodeResult StatusCode()
2 {
3    return HttpNotFound();
4 }

发送401结果

另一个特定的HTTP状态码的封装程序类是HttpUnauthorizeResult,它返回401代码,用来指示一个未授权请求。如下所示:

1  public HttpStatusCodeResult StatusCode()
2  {
3      return new HttpUnauthorizedResult();
4  }

总结:在本文章中,主要是对MVC设计模式中的控制器和动作方法做了详细介绍,主要从实现自定义的控制器类两种方式着手,然后介绍了控制器对数据的接收与响应系列内容。对数据接收主要来源查询字符串值、表单数据、路由数据做了分别介绍,对数据的响应包括视图 、文本数据、XML数据、JSON数据、文件或者二进制数据、返回错误和HTTP Codes、定制的ActionResult、重定向做了分别介绍。最后说明一下本文章的部分理论观点引用自《Pro ASP.Net MVC 5.pdf》这本书。

延伸 · 阅读

精彩推荐