ASP.NET MVC是一个适用于WEB应用程序的经典模型Model-View-Controller模式。相对于Web Forms一个单一的整体,ASP.NET MVC是由连接在一起的各种代码层所组成。

Global.asax文件

Global.asax文件概述

Global.asax这个文件包含全局应用程序事件的事件处理程序。它响应应用程序级别和会话级别事件的代码。

运行时, Global.asax 将被编译成一个动态生成的 .NET Framework 类,该类是从HttpApplication基类派生的。

因此在Global.asax中的代码可以访问HttpApplication类中所有的public或者protected的成员。

Global.asax不被用户直接请求,但Global.asax中的代码会被自动执行来响应特定的应用程序事件。

Global.asax是可选的,而且在一个web项目中是唯一的,它应该处于网站的根目录。

一个请求的完整处理过程

以下过程由Internet Information Service(inetinfo.exe)(IIS执行:

  • 客户端发出请求
  • 验证请求
  • 给请求授权
  • 确定请求的缓存
  • 获取缓存状态
  • 在请求的处理程序执行前
  • http处理程序执行请求 (asp.net页面由aspnet_wp.exe执行)
  • 在请求的处理程序执行后
  • 释放请求状态
  • 更新请求缓存
  • 请求结束

Global.asax中的事件

Global.asax中的所有事件可以分成两种,一种是满足特定事件时才会被触发,一种是每次请求都会被按照顺序执行的事件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace JohnSun.MVC5.Web
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //在Web应用程序的生命周期里就执行一次
            //在应用程序第一次启动和应用程序域创建事被调用
            //适合处理应用程序范围的初始化代码
            AreaRegistration.RegisterAllAreas();//注册 Area
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); //注册 Filter
            RouteConfig.RegisterRoutes(RouteTable.Routes);//注册 Route
            BundleConfig.RegisterBundles(BundleTable.Bundles);//注册 Bundle
        }

        void Application_End(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //在应用程序关闭时运行的代码,在最后一个HttpApplication销毁之后执行
            //比如IIS重启,文件更新,进程回收导致应用程序转换到另一个应用程序域
        }

        void Session_Start(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //会话开始时执行
        }

        void Session_End(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //会话结束或过期时执行
            //不管在代码中显式的清空Session或者Session超时自动过期,此方法都将被调用
        }

        void Application_Init(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //在每一个HttpApplication实例初始化的时候执行
        }

        void Application_Disposed(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //在应用程序被关闭一段时间之后,在.net垃圾回收器准备回收它占用的内存的时候被调用。
            //在每一个HttpApplication实例被销毁之前执行
        }

        void Application_Error(object sender, EventArgs e)
        {
            //不是每次请求都调用
            //所有没有处理的错误都会导致这个方法的执行
        }


        /*********************************************************************/
        //每次请求都会按照顺序执行以下事件
        /*********************************************************************/

        void Application_BeginRequest(object sender, EventArgs e)
        {
            //每次请求时第一个出发的事件,这个方法第一个执行
        }

        void Application_AuthenticateRequest(object sender, EventArgs e)
        {
            //在执行验证前发生,这是创建验证逻辑的起点
        }

        void Application_AuthorizeRequest(object sender, EventArgs e)
        {
            //当安全模块已经验证了当前用户的授权时执行
        }

        void Application_ResolveRequestCache(object sender, EventArgs e)
        {
            //当ASP.NET完成授权事件以使缓存模块从缓存中为请求提供服务时发生,从而跳过处理程序(页面或者是WebService)的执行。
            //这样做可以改善网站的性能,这个事件还可以用来判断正文是不是从Cache中得到的。
        }

        //------------------------------------------------------------------------
        //在这个时候,请求将被转交给合适程序。例如:web窗体将被编译并完成实例化
        //------------------------------------------------------------------------

        void Application_AcquireRequestState(object sender, EventArgs e)
        {
            //读取了Session所需的特定信息并且在把这些信息填充到Session之前执行
        }

        void Application_PreRequestHandlerExecute(object sender, EventArgs e)
        {
            //在合适的处理程序执行请求前调用
            //这个时候,Session就可以用了
        }

        //-------------------------------------------------
        //在这个时候,页面代码将会被执行,页面呈现为HTML
        //-------------------------------------------------

        void Application_PostRequestHandlerExecute(object sender, EventArgs e)
        {
            //当处理程序完成对请求的处理后被调用。
        }

        void Application_ReleaseRequestState(object sender, EventArgs e)
        {
            //释放请求状态
        }

        void Application_UpdateRequestCache(object sender, EventArgs e)
        {
            //为了后续的请求,更新响应缓存时被调用
        }

        void Application_EndRequest(object sender, EventArgs e)
        {
            //EndRequest是在响应Request时最后一个触发的事件
            //但在对象被释放或者从新建立以前,适合在这个时候清理代码
        }

        void Application_PreSendRequestHeaders(object sender, EventArgs e)
        {
            //向客户端发送Http标头之前被调用
        }

        void Application_PreSendRequestContent(object sender, EventArgs e)
        {
            //向客户端发送Http正文之前被调用
        }
    }
}

路由 Routing

ASP.NET MVC不再是要依赖于物理页面,我们可以使用自己的语法自定义URL,通过这些语法来指定资源和操作。语法通过URL模式集合表达,也称为路由。

路由是代表URL绝对路径的模式匹配字符串。所以路由可以是一个常量字符串,也可能包含一些占位符。

新建一个ASP.NET MVC项目,在Global.asax文件我们可以看到路由在这里注册,让程序在启动的时候得到处理。

RouteConfig.RegisterRoutes(RouteTable.Routes);//注册 Route

转到定义可以看到注册的规则:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

注意:因为匹配路由是按照添加顺序去解析,所以基本的路由规则是从特殊到一般排列,否则可能导致解析异常或404 Not Found。

具体配置参考文章:史上最全的ASP.NET MVC路由配置Attribute Routing in ASP.NET MVC 5

控制器 Controller

mvc1

ASP.NET MVC会调用不同的控制器类(和其内部不同的操作方法)这取决于传入URL。所使用的ASP.NET MVC的默认URL路由逻辑使用这样的格式来判定哪些代码以便调用:/[Controller]/[ActionName]/[Parameters]

项目建立以后有一个默认的控制器\Controllers\HomeController.cs

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();
    }
}

Controllers文件夹右键 -> 添加 可以看到创建控制器的选项。

控制器每一个方法为一个Action,每个Action对应的是项目内\Views\[Controller]\[ActionName].cshml,在方法内右键,菜单有“添加视图”与“转到视图”选项。

ActionResult返回值

视图类型

返回视图:

public ActionResult Index()
{
    return View();
}

返回分部视图:

public ActionResult ViewTest()
{
    return PartialView();
}

注意: 以上均需要对Action添加视图文件,若无Action方法的视图文件也可以直接通过视图名称或视图路径指定视图文件,具体可以查看View()方法重载。

文本类型

返回JavaScript脚本:

public ActionResult ContentJS()
{
    return Content("alert('test js.');", "text/javascript");
}

返回CSS样式:

public ActionResult ContentCSS()
{
    HttpCookie cookie = Request.Cookies["theme"] ?? new HttpCookie("theme", "default");
    switch (cookie.Value)
    {
        case "Theme1": return Content("body{font-family: SimHei; font-size:1.2em}", "text/css");
        case "Theme2": return Content("body{font-family: KaiTi; font-size:1.2em}", "text/css");
        default: return Content("body{font-family: SimSong; font-size:1.2em}", "text/css");
    }
}

JSON类型

public ActionResult JsonTest()
{
    return Json(new { name = "Kangkang", country = "China", email = "kangkang@163.com" }, JsonRequestBehavior.AllowGet);
}

图片多媒体类型

public ActionResult ImageTest(string id)
{
    string path = Server.MapPath($"/images/{id}.gif");
    return File(path, "image/gif");
}

Javascript脚本类型

public ActionResult JavaScriptTest()
{
    return JavaScript("alert('test js.');");
}

文件类型(下载)

public ActionResult FileTest(string id)
{
    string fileName = "可爱.gif";//客户端保存的文件名
    string filePath = Server.MapPath($"/images/1.gif");//路径
    return File(new FileStream(filePath, FileMode.Open), "image/gif", fileName);
}

注意:

  • FileContentResult:是针对文件内容创建的FileResult,它只是调用当前HttpResponseOutputStream属性的Write方法直接将表示文件内容的字节数组写入响应输出流。
  • FilePathResult:是一个根据物理文件路径创建FileResult
  • FileStreamResult:允许我们通过一个用于读取文件内容的流来创建FileResult

可以参考: 了解ASP.NET MVC几种ActionResult的本质:FileResult

返回null或者void

public ActionResult Empty()
{
    return null;
}

返回未经授权浏览状态

public ActionResult HttpUnauthorizedResult()
{
    return new HttpUnauthorizedResult();
}

注意: 响应给客户端错误代码 401(未经授权浏览状态),如果程序启用了 Forms 验证,并且客户端没有任何身份票据,则会跳转到指定的登录页。

页面跳转

public ActionResult Redirect()
{
    // 直接返回指定的url地址  
    return Redirect("http://www.google.com");
}
  • RedirectToRouteResult:直接使用 Action Name 进行跳转,也可以加上ControllerName以及参数。
  • RedirectToActionResult:指定路由进行跳转。
public ActionResult RedirectActionResult()
{
    return RedirectToAction("Index", "Home", new { id = 1, name = "Kangkang" });
}

public ActionResult RedirectResult()
{
    return RedirectToRoute("Default", new { controller = "Home", action = "Index" });
}

总结: 这些返回类型的共同点,那便是对Action有一定的要求:

  • 必须是一个public方法
  • 必须是实例方法
  • 不能被重载
  • 必须返回ActionResult类型

视图 View

mvc2

Views文件夹下常用文件种类

文件类型扩展名概述
HTML.htmlhtm静态html文件;
在开发中通用性最高的页面;
属于ASP.NET MVC常用页面;
Razor文件.cshtml动态MVC Razor文件;
ASP.NET MVC中,属于常用文件;
采用Razor语法格式;
基本取代.aspx文件;
WebForm文件.aspx动态ASPX文件;
属于WebForm架构文件;
ASP.NET MVC中,也可以用该页面布局页面但不推荐;
ASP.NET MVC中基本被cshtml文件取代
ASP文件.asp传统ASP文件,目前已过时;
发展:asp=>aspx=>cshtml
对应架构:ASP=>ASP.NET WebForm=>ASP.NET MVC

分析:

  • ASP.NET MVC页面基本被放在Views文件夹下;
  • 利用ASP.NET MVC模板生成框架,Views文件夹下的默认页面为.cshtml页面;
  • ASP.NET MVC默认页面为Razor格式的页面,因此默认页面为.cshtml页面;
  • ASP.NET MVC中,支持WebForm页面,即.aspx页面;
  • ASP.NET MVC中,支持静态html页面;

默认Views文件夹包含内容

文件夹名称概述
Account文件夹包含用于注册并登录用户账户的页面
Home文件夹存储首页和About页面信息
Share文件夹存储控制器间分享的视图,如布局页面和模板页等
_ViewStart.cshtml程序最开始执行的页面

分析:

  • 这里没添加Account控制器;
  • 默认约定:在Controllers新增一个控制器,就会默认地在Views文件夹下新增一个视图文件夹,用来存放该控制器添加的视图,如上图中增加Home控制器,在Views下就自动新增加Home文件,用来存放是Home控制器视图;

视图种类

起始视图:_ViewStart.cshtml

@*@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}*@

<div>
    <h2>这里是_ViewStart.cshtml视图</h2>
    <p>原则上程序运行时,这个视图首先被运行,其他视图在这个视图之后被运行。</p>
</div>

浏览http://127.0.0.1/Home/Index

mvc8

布局视图:_Layout.cshtml

首先将_ViewStart.cshtml文件还原修改其指向的_LayoutDemo.cshtml文件

@{
    Layout = "~/Views/Shared/_LayoutDemo.cshtml";
}

添加_LayoutDemo.cshtml文件

@{
    ViewBag.Title = "LayoutDemo";
}
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <h2>---如下内容来源为视图---</h2>
    <div>
        @RenderBody()
    </div>
    <h2>---如下内容为Footer区---</h2>
    <div>
        <footer>Copyright by John Sun.</footer>
    </div>
</body>
</html>

重新浏览http://127.0.0.1/Home/Index

mvc7

  • _Layout.cshtml基本结构就是HTML基本结构(其实.aspx.cshtml结构,均是html结构);
  • _LayoutDemo.cshtml文件一个后台代码:@RenderBody()@RenderBody()表示视图体,此外还有@RenderSection()表示部分视图和节点;

弱类型视图

Controller向View传递少量数据,一般情况,我们可以归为两大类别:弱类别传递(ViewBag,ViewData,TempData)和强类别传递(强类型视图)。然而,在实际操作中,当涉及大量数据时,弱类别就显得不是那么方便,此时,一般采用强类型视图。强类型视图一般由三部分构成,即控制器层,视图层和模型层,三者之间调用关系可表示为:

mvc3

ViewData和TempData

ViewData只在当前 Action 中有效,生命周期和 View 相同;

TempData的数据至多只能经过一次Controller传递,并且每个元素至多只能被访问一次,访问以后,自动被删除。

TempData一般用于临时的缓存内容或抛出错误页面时传递错误信息,可以将TempData在使用之前存储到相应的ViewData中以备循环使用。

TempData保存在Session中,Controller每次执行请求的时候,会从Session中先获取TempData,而后清除Session,获取完TempData数据,虽然保存在内部字典对象中,但是其集合中的每个条目访问一次后就从字典表中删除。具体代码层面,TempData获取过程是通过SessionStateTempDataProvider.LoadTempData方法从ControllerContext的Session中读取数据,而后清除Session,故TempData只能跨Controller传递一次。

HomeController.cs

public ActionResult Index()
{
    ViewBag.Message = "Welcome to ASP.NET MVC!";
    ViewData["myName"] = "我的名字";
    TempData["myAgeOne"] = "26岁";
    TempData["myAgeTwo"] = "27岁";
    return View();
}

Index.cshtml文件

姓名:@ViewData["myName"]
<br />
年龄1:@TempData["myAgeOne"]

About.cshtml文件

姓名:@ViewData["myName"]
<br />
年龄1:@TempData["myAgeOne"]
<br />
年龄2:@TempData["myAgeTwo"]
ViewBag和ViewData
ViewBag.Name = ViewData["Name"];
ViewDataViewBag
Key/Value集合dynamic类型对象
ASP.NET MVC 1 就有ASP.NET MVC 3 才有
基于ASP.NET 3.5基于ASP.NET 4.0 与 .NET Framework
ViewData较快ViewBag较慢
视图中查询数据需要转换合适的类型视图中查询数据不需要类型转换
有一些类型转换代码可读性好

强类型视图

增加实体类UserInfo.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace JohnSun.MVC5.Web.Models
{
    public class UserInfo
    {
        public int UserId { get; set; }
        public string UserName { get; set; }
        public string Country { get; set; }
        public string Hobby { get; set; }
        public string Email { get; set; }
    }
}

HomeController.cs增加Action方法

public ActionResult ModelTest()
{
    return View(new List<UserInfo>
    {
        new UserInfo(){ UserId = 1, UserName = "Kangkang", Country = "China", Hobby = "Basketball", Email = "kangkang@163.com" },
        new UserInfo(){ UserId = 2, UserName = "Mary", Country = "America", Hobby = "Football", Email = "mary@gmail.com" },
        new UserInfo(){ UserId = 3, UserName = "Jane", Country = "Canada", Hobby = "Compute game", Email = "jane@yahoo.com" },
    });
}

增加视图ModelTest.cshtml

@model IEnumerable<JohnSun.MVC5.Web.Models.UserInfo>
@{
    ViewBag.Title = "ModelTest";
}

<h2>ModelTest</h2>
@foreach (var info in Model)
{
    <p>@($"{info.UserId}  {info.UserName}  {info.Country}")</p>
}

运行效果:

mvc4

分布页

我们在/Views/Shared文件夹下创建一个分布页_PartialPageDemo.cshtml,并向该页面中添加一段代码:

<h1 style="color:red;">我是分布页</h1>

在HomeController.cs文件中增加返回视图的方法

public ActionResult TestPartialPage()
{
    return PartialView();
}

在/Views/Home增加对应视图文件TestPartialPage.cshtml

<h1 style="color:red;">我是供控制器调用的分布页</h1>

在Index.cshtml中调用

@{
    ViewBag.Title = "Home Page";
}

@Html.Partial("~/Views/Shared/_PartialPageDemo.cshtml")
@Html.Partial("~/Views/Home/TestPartialPage.cshtml")
@Html.Action("TestPartialPage")

显示效果如下:

mvc5

调用分布页的几种方式:

  • @Html.Partial() 提供分布页路径
  • @Html.Action() 提供控制器返回视图的方法
  • 通过Ajax方式调用

区域 Area

ASP.NET MVC有预定义的目录规则,框架根据这些目录规则去加载各种类。

在MVC单项目中,随着业务越来越复杂多样,我们会希望按照功能对代码按文件夹分门别类。

如果在默认的目录结构下业务混合,这样不方便管理和维护;如果另开新项目,又比较散乱。那么MVC有没有这样一种机制来相对独立这些模块呢?答案是肯定的,这就是MVC的Area区域技术,用来实现在一个MVC项目中组织和维护多个相对独立的模块。

在VS中右键单击项目,在弹出的菜单中选择“添加(A)”->“Area...”,在弹出的对话框中输入区域名称(遵守C#标示符命名规则)即可(比如输入System),VS将自动在根目录创建Areas文件夹,此文件夹下每个独立的Area一个文件夹,System文件夹内也是一样的Models、Controllers、Views结构。

mvc6

唯一不同的是多了一个SystemAreaRegistration.cs(区域注册类),用于向MVC框架注册路由等信息,Global.asax.cs中会自动调用该类的RegisterArea方法。新建Area后VS自动创建相关目录结构,按需修改SystemAreaRegistration路由即可。

using System.Web.Mvc;

namespace JohnSun.MVC5.Web.Areas.System
{
    public class SystemAreaRegistration : AreaRegistration 
    {
        public override string AreaName 
        {
            get 
            {
                return "System";
            }
        }

        public override void RegisterArea(AreaRegistrationContext context) 
        {
            context.MapRoute(
                "System_default",
                "System/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

注意: 当Area中控制器与MVC中控制器同名,比如HomeController,此时访问首页会异常提示:找到多个与名为“Home”的控制器匹配的类型。如果为此请求({controller}/{action}/{id})提供服务的路由没有指定命名空间以搜索与此请求相匹配的控制器,则会发生这种情况。如果是这样,请通过调用带有namespaces参数的MapRoute方法的重载来注册此路由。

此时修改RouteConfig.cs文件中注册路由的方法,添加HomeController的命名空间指向即可。

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    namespaces: new string[] { "JohnSun.MVC5.Web.Controllers" }
);

快速上手入门视频:微软的船新框架ASP.NET Core - 10分钟讲完MVC基础 by Anduin


盈月舞清风,华灯自摇曳。