欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第二章 你的第一个MVC应用程序

程序员文章站 2024-03-02 14:58:34
...

第二章 你的第一个MVC应用程序

欣赏软件开发框架的最好方法是直接使用它. 在本章中, 你将使用ASP.NET Core MVC创建一个简单的数据实体应用. 我将一步一步来做, 这样你就可以看到MVC应用程序是如何构建的. 为了简单起见, 我暂时略过一些技术细节. 但是别担心. 如果你是MVC的新手, 你会发现很多东西让你感兴趣. 在我使用一些东西而不作解释的地方, 我提供了之后章节详情的位置, 你可以在其中找到所有的细节.

本书的更新说明

微软对.NET Core和ASP.NET Core MVC有一个积极的开发计划, 这意味着当你阅读本书时, 可能有一些新的发行版可用. 因为大多数变化相对较小, 让读者每隔几个月就购买一本新书似乎不太公平. 因此, 我将在本书的GitHub仓库中免费更新本书, 以防止小版本引起的更改.
这种更新是我(和Apress)的一个实验, 我还不知道这些更新的形式是什么,至少因为我不知道ASP.NET Core MVC将包含什么新特性, 但目的是提高这本书的作用时间.
我无法保证更新会是什么样子, 或者什么时候会发表一本新的修订版. 请保持开放的头脑, 并在新的ASP.NET Core MVC发布时检查这本书的仓库. 如果你有关于如何改进更新的想法, 请发送邮件到aaa@qq.com.com

安装Visual Studio

本书依赖提供ASP.NET Core MVC项目开发环境的Visual Studio 2017. 我使用免费的社区版, 可以从www.visualstudio.com下载. 当安装VS2017时, 必须选择.NET Core cross-platform development工作负载

注意: VS2017比ASP.NET Core MVC 2发布得更早, 如果你安装了之前版本的ASP.NET Core MVC, 需要更新到最新版本.

提示: Visual Studio仅支持Windows平台. 你可以在其他平台上使用VS Code来创建ASP.NET Core MVC应用. VS Code没有提供VS的所有功能, 但也是一个很棒的编辑器, 可以满足MVC应用开发的所有功能. 在第十三章中查看细节

安装.NET Core 2.0 SDK

VS安装包括了ASP.NET Core MVC开发所需的所有功能, 但不包括.NET Core 2.0, 需要手动安装.
前往https://www.microsoft.com/net/core下载.NET Core SDK安装程序.安装成功后, 在CMD或PowerShell中执行dotnet --version来查看已安装的.NET Core版本.

创建一个新的ASP.NET Core MVC项目

我将在VS中创建一个新的MVC项目. 选择New Project > Installed > Visual C# > Web > ASP.NET Core web Application

提示: ASP.NET Web Application(.NET Framework)是使用传统的ASP.NET的开发框架. 不要混淆

创建新项目名为PartyInvites, 在设置项目的初始内容时, 选择下拉菜单中的.NET CoreASP.NET Core 2.0, 选择Web Application(Model-View-Controller), 将会创建一个MVC应用模板来帮助开发.

提示: 这是我使用web application(MVC)项目模板的唯一章节. 我不喜欢使用预定义的项目模板, 因为它们鼓励开发者将一些重要特性(如认证)作为黑盒对待. 我这本书的目的是让你们了解并掌握MVC应用开发的每个方面, 所以在之后我将创建空项目. 因为这一章是为了快速开始, 所以使用模板很合适.

在身份验证窗口选择No Authentication, 这个项目不需要任何身份验证, 但我在第二十八到第三十章中解释了如何确保ASP.NET应用的安全.
不要选择Enable Docker Supprt, 然后创建项目.
一旦VS创建项目完成, 你会看见解决方案管理器中有很多文体, 这是使用模板创建的新的MVC项目的默认结构, 你将很快理解每个文件和文件夹的意义.

提示: 如果你看到的是Pages文件夹, 而不是Controllers, Models, Views文件夹, 那么你误选了Web Application模板. 我不知道微软为什么要把名字取得那么像, 但你需要重新创建项目了.

点击Start Debugging按钮运行. VS将会编译程序, 使用IIS Express来运行服务端, 并打开浏览器来请求页面. 第一次运行需要花费一些时间.
当VS使用web application (MVC)模板创建应用后, 会添加一些基本代码和内容, 这就是你运行应用时看到的. 在后面的章节中, 我将替换这些内容并创建一个简单的MVC应用程序.
完成后, 通过关闭浏览器窗口或在VS中点击Stop Debugging.
征信刚刚看到的那样, VS打开浏览器来展示项目. 你可以在调试的IIS Express下拉菜单中选择要使用的浏览器.
从这里开始,我将使用谷歌Chrome或谷歌Chrome Canary来制作这本书的所有截屏,但是你可以使用任何现代浏览器来显示书中的示例,包括Microsoft Edge.

添加控制器

在MVC模式中, 使用控制器(controllers)处理到来的请求, 控制器其实就是C#类(通常继承于类Microsoft.AspNetCore.Mvc.Controller, 一个内建的MVC控制器基类)
控制器的公有方法被称为”行为方法”(action method), 意思是你可以通过URL从web来触发一个动作. MVC(这里的MVC都是指ASP.NET Core MVC框架, 下同)惯例是将控制器放置到VS创建项目时自动创建的Controllers文件夹中.

提示: 你不必遵循这些约定, 但推荐这样做, 至少可以帮助理解本书中的示例.

VS为本项目添加了一个默认的控制器类, 在解决方案资源管理器中可以看到Controllers/HomeController.cs. 控制器类的名字以Controller结尾以便于区分. 打开HomeController.cs你会看到如下代码:

// Controllers/HomeController.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

HomeConroller.cs中的默认代码替换为以下代码. 我只保留了一个方法, 修改了实现和返回值类型, 移除了不需要的using语句.

// Controllers/HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public string Index() {
            return "Hello World";
        }
    }
}

这些改变不会产生戏剧性的影响, 但可以很好地说明问题. 我修改了Index方法让它返回一个字符串”Hello World”. 再次运行项目.

提示: 如果之前的程序还在运行, 点击Restart, 或者先停止调试再开始调试.

浏览器会向服务器发送一个HTTP请求. 默认MVC配置表明请求会被Index方法(称为action method或action, “行为方法”或”行为”), 方法的返回值被 送回浏览器.

提示: VS将浏览器端口重定向了, 你可能看到URL中的端口号不同, 这是因为VS为项目随机分配了一个端口号. 如果你检查Windows的通知区域, 会发现IIS Express, 这是VS内置的阉割版本的IIS服务器, 用于ASP.NET Core开发. 我将在第十二章中解释如何将MVC应用部署到生产环境.

理解路由

除了模型, 视图和控制器, MVC应用还使用ASP.NET路由系统, 它决定了将哪些URL映射到哪个控制器和行为. 路由是决定请求如何处理的规则. 当VS创建MVC项目时, 添加了一些默认的路由. 你可以请求下面的URL, 它们都会被重定向到HomeController中的Index行为.

  • /
  • /Home
  • /Home/Index

所以, 浏览器请求http://yoursite/http://yoursite/Home时, 会获得Index方法的输出. 你可以自己尝试改变浏览器中的URL.
这是从ASP.NET Core MVC实现的约定中获益的一个很好的例子. 在此例中, 约定是我将有一个控制器HomeController, 它将是MVC应用程序的入口点. VS创建的新项目默认我应当遵循这个约定. 既然我确实遵循了此约定, 我就能够自动获得前面列出的URL的支持. 否则, 我需要修改配置文件, 指向我自己创建的控制器. 在这个简单的例子中, 我只需要默认的配置文件就够了.

渲染Web页面

上面例子的输出不是HTML, 只是一个字符串”Hello World”. 要为浏览器请求生成一个HTML响应, 需要一个视图, 它告诉MVC如何为请求生成响应.

创建并渲染视图

第一件要做的事是修改Index行为, 修改部分加粗标注, 本书后面的例子中也将遵守此约定(我这markdown就算了)

// Controllers/HomeController.cs

using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            return View("MyView");
        }
    }
}

当让行为返回一个ViewResult对象时, 就是命令MVC渲染一个视图. 我通过View()创建一个ViewResult对象, 指定想使用的名字MyView. 运行应用, MVC将试着找到这个视图, 然后显示错误信息

InvalidOperationException: The view 'MyView' was not found. The following locations were searched:
/Views/Home/MyView.cshtml
/Views/Shared/MyView.cshtml

这是很有帮助的错误信息. 它解释了MVC找不到我在行为中指定的视图, 并展示了它寻找的路径. 视图被存储在Views文件夹中, 通过子文件夹归类. 例如和Home控制器有关的视图存储在Views/Home中. 没有被指定到一个特定控制器的视图存储在Views/Shared中. VS在MVC模版创建时自动创建HomeShared文件夹, 并添加了一些默认视图.
要创建示例中需要的视图, 展开Views文件夹, 右击Home文件夹, Add > New Item > ASP.NET Core > Web > ASP.NET > MVC View Page (不要使用Razor页面模板, 这和MVC架构无关, (在最新版中已经只有”Razor视图”了))

提示: 很容易将视图文件创建到错误的文件夹中, 如果出错了, 删除并重新创建.

替换MyView.cshtml中的文件为如下内容

<!-- Views/Home/MyViews.cshtml -->

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
    <div>
        Hello World (from the view)
    </div>
</body>
</html>

视图文件的新内容大部分都是HTML, 除了

@{
    Layout = null;
}

这是将被Razor视图引擎解释的表达式, 该引擎处理视图中的内容并生成要发送到浏览器的HTML. 这是一个简单的Razor表达式, 它告诉Razor我不使用布局(布局就像一个被发送到HTML的模板, 我将在第五章描述). 现在我会先忽略Razor. 要查看创建的视图的效果, 开始调试程序.

我一开始修改Index行为时, 返回了一个字符串值. 这意味着MVC仅仅将字符串值发送给浏览器. 现在Index方法返回ViewResult, MVC渲染一个视图并将生成的HTML返回浏览器. 我告诉MVC我要使用的视图, 它将按照命名约定自动查找. 约定是视图具有方法的名字, 并被包含在以控制器命名的文件夹中: /Views/Home/MyView.cshtml.
除了字符串个ViewResult, 还可以返回其他结果. 例如重定向RedirectResult, 提示用户登录的HttpUnauthorizedResult. 这些对象统称行为结果. 行为结果系统允许你在行为中封装和重用通用响应. 我将在第十七章中具体描述.

添加动态输出

web应用平台的唯一目标就是构造和显示动态输出. 在MVC中, 将数据组织并传递给视图是控制器的工作, 视图负责将数据渲染为HTML.
一种将数据从控制器传递到视图的方法是使用ViewBag对象, 这是Controller基类的一个成员. ViewBag是一个动态对象, 你可以为它分配任意属性, 让这些值在随后渲染的任何视图中可用. 对HomeController.cs进行了一些修改

// Controllers/HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }
    }
}

当我为ViewBag.Greeting属性复制的时候, 就为视图提供了数据. 直到我赋值后, Greeting属性才存在, 这允许我以*流畅的方式传递数据, 而不需要预先定义类. 我在视图中再次引用ViewBag.Greeting属性来获取数据

<!-- Views/Home/MyViews.cshtml -->

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
    <div>
        @ViewBag.Greeting World (from the view)
    </div>
</body>
</html>

新增了一个在MVC将视图渲染生成HTML时计算的Razor表达式. 当我调用View()方法时, MVC定位MyView.cshtml视图文件, 然后让Razor引擎解析文件内容. Razor查找向我之前添加那样的表达式并处理它们. 此例中, 处理表达式意味着将在行为中为ViewBag.Greeting赋的值插入视图中.
属性名Greeting并没有什么特殊的, 替换为任意合法的名字都可以, 只要控制器和视图中的属性名匹配. 可以通过给不同属性赋值的方式来实现从控制器向视图传递多个值.

创建一个简单的数据实体应用程序

在本章的剩余部分中, 我将加快步伐, 通过创建一个简单的数据实体应用的方式, 来展示大多数的MVC基本特性. 我的目的是演示实际运行的MVC, 因此我将先不解释一些幕后工作. 但别担心, 我将在后面的章节中深入讨论这些主题.

设置场景

想象一下, 一个朋友决定举办一个新年晚会, 她让我创建一个web应用, 从而让她邀请的人可以通过电子方式回复. 她提出了一下四个功能:

  • 一个展示聚会信息的主页
  • 一个用于回复的表单
  • 回复表单的确认, 展示一个感谢页面
  • 一个展示谁参加了聚会的总结页面

在接下来的部分中, 我将使用之前创建的项目, 并添加这些功能. 我可以通过使用前面介绍的内容, 并在现有的视图中添加一些提供聚会详细信息的HTML来完成第一个功能.

<!-- Views/Home/MyViews.cshtml -->

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
    <div>
        @ViewBag.Greeting World (from the view)
        <p>
            We're going to have an exciting party.<br />
            (To do: sell it better. Add pictures or something.)
        </p>
    </div>
</body>
</html>

运行应用后将看到聚会详情.

设计一个数据模型

在MVC中, “M”代表模型, 应用程序中最重要的部分. 模型是现实世界对象\过程和定义应用程序主题的规则(即领域, domain)的表示. 模型(通常称为”领域模型”)包括组成整个应用程序的C#对象(领域对象)和操作他们的方法, 视图和控制器以一致的方式将领域暴露给客户端. 一个设计良好的MVC应用应该从一个设计良好的模型开始, 然后作为添加控制器和视图的聚焦点.
我不需要为PartyInvites创建很复杂的模型, 因为这是一个很简单的应用. 我只需要一个领域类GuestResponse. 这个对象将负责存储\验证并确认一个回复.
MVC约定, 组成模型的类被放置在Models文件夹中, VS的web application (MVC)模版会自动创建这个文件夹.
Models文件夹中右键创建新类GuestResponse

// Models\GuestResponse.cs

namespace PartyInvites.Models
{
    public class GuestResponse
    {
        public string Name { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
        public bool? WillAttend { get; set; }
    }
}

提示: WillAttendNullable<bool>类型, 即可为true, false, null

创建第二个行为以及一个强类型视图

应用程序的一个目标是包含回复表单, 这表示我需要有一个行为方法来接受这个表单的请求. 一个单独的控制器内可有多个行为, 惯例是将相关联的行为放在同一个控制器中.

// Controllers\HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }

        public ViewResult RsvpForm()
        {
            return View();
        }
    }
}

RsvpForm方法没有参数, 则MVC将调用默认的与方法同名的视图RsvpForm.cshtml. 新建视图并填入

<!-- Views/Home/RsvpForm.cshtml -->

@model PartyInvites.Models.GuestResponse

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=devide-width"/>
    <title>RsvpForm</title>
</head>
<body>
    <div>
        This is the RsvpForm.cshtml View
    </div>
</body>
</html>

大部分内容都是HTML, 但有一个额外的Razor表达式@model, 用于创建一个强类型视图. 强类型视图用于渲染特定的模型类型. 如果我指定了我想处理的类型(此例中是PartyInvites.Models.GuestResponse), MVC可以创建一些有用的快捷方式来简化它. 稍后我将利用强类型特性.
要测试新的行为和它的视图, 运行程序并导航到URL/Home/RsvpForm.
MVC使用前面提到的约定, 将请求指向HomeController.RsvpForm, 此行为告诉MVC渲染默认视图, MVC渲染Views/Home/RsvpForm.cshtml.

链接到行为方法

我将在MyView中创建一个到RsvpForm的链接, 这样访客要提交表单就不需要知道特定的URL.

<!-- Views/Home/MyView.cshtml -->

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width"/>
    <title>Index</title>
</head>
<body>
    <div>
        @ViewBag.Greeting World (from the view)
        <p>
            We're going to have an exciting party.<br />
            (To do: sell it better. Add pictures or something.)
        </p>
        <a asp-action="RsvpForm">RSVP Now</a>
    </div>
</body>
</html>

增加了一个具有asp-action属性的a标签. 这是tag helper属性的一个例子, 它将在Razor渲染视图时被执行. asp-action属性是一条指令, 向DOM元素中的href属性添加行为的URL. 我将在第二十四到二十六章介绍tag helper如何工作, 但这是a元素中tag helper发挥最简单作用的地方, 它告诉Razor要插入一个本视图所属的控制器下的行为的URL. 你可以运行并在浏览器中尝试.

运行应用并在浏览器中用鼠标放在链接上, 你会看到以下URL:
http://localhost:57628/Home/RsvpForm
这是一个重要的原则, 即使用MVC框架来生成URL, 而不是直接写在HTML元素中. 当tag helper为a元素生成href属性时, 首先检查应用配置以确定URL. 这允许将应用程序配置为支持不同的URL格式, 而不需要更新视图. 我在第十五章中解释了它如何工作.

构建表单

现在我已经创建了一个强类型视图, 并可以在Index视图中跳转到它了, 我将构建RsvpForm.cshtml的具体内容, 来创建一个可以编辑GuestResponse对象的HTML表单.

<!-- Views\Home\RsvpForm.cshtml -->

@model PartyInvites.Models.GuestResponse

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=devide-width" />
    <title>RsvpForm</title>
</head>
<body>
    <form asp-action="RsvpForm" method="post">
        <p>
            <label asp-for="Name">Your name:</label>
            <input asp-for="Name" />
        </p>
        <p>
            <label asp-for="Email">Your email:</label>
            <input asp-for="Email" />
        </p>
        <p>
            <label asp-for="Phone">Your phone:</label>
            <input asp-for="Phone" />
        </p>
        <p>
            <label asp-for="WillAttend">Will you attend?</label>
            <select asp-for="WillAttend">
                <option value="">Choose an option</option>
                <option value="true">Yes, I'll be there</option>
                <option value="false">No, I can't come</option>
            </select>
        </p>
        <button type="submit">Submit RSVP</button>
    </form>
</body>
</html>

我为模型类GuestResponse的每个属性都定义了一个label元素和一个input元素(除了给WillAttend属性定义了select元素). 这里使用了另一个tag helper属性asp-for, 每个元素都关联到了一个模型属性. 这个tag helper属性将元素绑定到模型对象. 这是tag helper制造并发送到浏览器的HTML的一个例子:

<p>
    <label for="Name>Your name:</label>
    <input type="text" id="Name" name="Name" value="">
</p>

asp-for属性为label元素设置了for属性, 为input元素设置了idname属性. 现在看起来没什么用, 但在定义程序的功能后, 你将看到将元素与模型属性关联的额外优势.
更直接的用法是form元素的asp-action属性, 它使用应用程序的路由配置将action属性指向指定的行为, 像这样

<form method="post" action="/Home/RsvpForm">

与在a元素中使用的一样, 使用asp-action的最大好处是修改了应用的URL, tag helper也会自动反映相应的改变.
运行程序就可以看到这个表单.

接收表单数据

我还没有告诉MVC当表单被post到服务器时我想做什么. 当前, 当表单被提交时, 表单的已输入值将被清空, 这是因为表单返回到HomeController.RsvpForm, 这仅仅通知MVC重新渲染一遍视图. 我将使用一个核心的控制器功能, 来接收并处理提交的表单数据. 我将创建另一个RsvpForm方法.

  • 一个方法接收HTTP GET请求: GET请求一般来自于浏览器链接, 这个版本的行为应该是渲染视图, 显示一个空表单.
  • 一个方法接收HTTP POST请求: 默认情况下, 浏览器将渲染为Html.BeginForm()的表单以POST方式提交. 这个版本的行为将负责接收并处理数据.

由于两种请求方式责任不同, 将其分割为两个C#方法会更使代码更简洁. 两个方法被相同的URL触发, 但MVC会确保调用合适的方法.

// Controllers/HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }

        [HttpGet]
        public ViewResult RsvpForm()
        {
            return View();
        }

        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse)
        {
            // TODO: store response from guest
            return View();
        }
    }
}

我为现有的RsvpForm添加了HttpGet属性, 这告诉MVC这个方法仅适用于处理GET请求. 然后我添加了一个重载版本的RsvpForm, 它接受GuestResponse对象, 并添加了HttpPost属性. 我也引入了PartyInvites.Models空间.

使用模型绑定

RsvpForm的第一个重载渲染了视图, 第二个重载由于参数更有趣, 但是考虑到这个行为将在响应POST请求时被调用, GuestResponse则是一个C#类, 他们是怎么连接的?
答案是模型绑定, 一种有用的MVC特性, 通过解析传入数据和HTTP请求中的键值对来填充领域模型类型的属性.
模型绑定是一种强大的可定制的特性, 它消除了直接处理HTTP请求的麻烦, 并允许直接处理C#对象, 而不是处理浏览器发送的单个数据值. 作为参数传递给行为的GuestResponse对象将自动使用来自表单字段的数据进行填充. 在第二十六章中将深入探究模型绑定的细节, 以及如何定制.
应用程序的一个目标是显示一个包含出席者详细信息的总结页面, 这意味着我需要跟踪收到的响应. 我将通过在内存中创建对象集合来实现这一点. 这在实际的应用程序中是没用的, 因为当应用停止或重启时这些数据将丢失, 但这种方式允许我将重点放在MVC上, 创建一个可以轻松重置为初始状态的应用程序.

提示: 我在第八章中演示了如何持久存储和访问数据, 那是更实际的一个示例应用程序SportsStore的一部分.

我添加了一个类PartyInvites.Models.Reposity:

// Models\Reposity.cs

using System.Collections.Generic;

namespace PartyInvites.Models
{
    public static class Repository
    {
        private static List<GuestResponse> responses = new List<GuestResponse>();

        public static IEnumerable<GuestResponse> Responses
        {
            get
            {
                return responses;
            }
        }

        public static void AddResponse(GuestResponse response)
        {
            responses.Add(response);
        }
    }
}

Repository类及其成员被设计为静态类, 更容易从应用的不同地方收集数据. MVC提供了一种更复杂的方法”依赖注入”, 我将在第十八章中描述, 但静态类很适合这样的一个简单应用.

存储请求

已经有地方来存储数据了, 现在要把POST请求的数据存进去.

// Controllers\HomeController.cs

using System;
using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }

        [HttpGet]
        public ViewResult RsvpForm()
        {
            return View();
        }

        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse)
        {
            Repository.AddResponse(guestResponse);
            return View("Thanks", guestResponse);
        }
    }
}

将POST请求的GuestResponse对象存储到Repository.

为什么模型绑定不像web forms

在第一章中, 我解释了传统ASP.NET Web Forms的一个缺点是对开发者隐藏了HTTP和HTML. 你可能会疑惑, 我在MVC中, 从HTTP POST请求中获取一个GuestResponse对象是不是做了同样的事.
并不是. 模型绑定让我从繁琐易错的检查HTTP请求和解析需要的数据值中解脱出来, 但(很重要我也可以手动处理一个请求, 因为MVC提供了访问所有请求数据的简便方式. 没有任何内容对开发者隐藏, 但很多有用的特性让HTTP和HTML工作更简便. 可以*选择是否使用这些特性.
这似乎是一种微妙的差异, 但是随着对MVC的深入了解, 你将看到开发体验与传统Web表单完全不同, 并且你始终知道应用程序接收的请求是如何处理的.

View方法的调用告诉MVC渲染一个视图Thanks, 并传递一个GuestResponse对象给这个视图. 创建视图Views/Home/Thanks.cshtml

<!-- Views/Home\Thanks.cshtml -->

@model PartyInvites.Models.GuestResponse

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Thanks</title>
</head>
<body>
    <p>
        <h1>Thank you, @Model.Name!</h1>
        @if (Model.WillAttend == true)
        {
            @:It's great that you're coming. The drinks are already in the fridge!
        }
        else
        {
            @:Sorry to hear that you can't make it, but thanks for letting us know.
        }
    </p>
    <p>
        Click <a asp-action="ListResponses">here</a> to see who is coming.
    </p>
</body>
</html>

视图Thanks.cshtml使用Razor来展示数据, 基于在RsvpForm中通过View传递的值. Razor表达式@model指定了领域模型类型和强类型视图.
要访问领域对象的值, 使用Model.PropertyName. 例如, 要得到Name属性的值, 就使用Model.Name. 不用担心不理解Razor语法, 我会在第五章中详细解释.
现在我已经创建了Thanks视图, 就有了一个使用MVC处理表单的基本工作示例, 你可以在浏览器中测试.

Thanks.cshtml视图的结尾, 我添加了一个连接到ListResponses行为. 把鼠标悬停在上面, 你会看到链接指向/Home/ListResponses, 这并没有对应HomeController的任何方法, 需要添加一个行为.

// Controller\HomeController.cs
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }

        [HttpGet]
        public ViewResult RsvpForm()
        {
            return View();
        }

        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse)
        {
            Repository.AddResponse(guestResponse);
            return View("Thanks", guestResponse);
        }

        public ViewResult ListResponses()
        {
            return View(Repository.Responses.Where(r => r.WillAttend == true));
        }
    }
}

新方法称为ListResponses, 调用View方法, 传递Repository.Responses, 这是行为方法向强类型视图传递数据的案例. LINQ语句过滤了数据, 仅留下WillAttend == trueGuestResponse集合.
ListResponses行为没有指定视图名称, 这意味着使用默认的命名约定, MVC将查找Views/Home/ListResponses.cshtmlViews/Shared/ListResponses.cshtml. 新建Views/Home/ListResponses.cshtml:

<!-- Views\Home\ListResponses.cshtml -->

@model IEnumerable<PartyInvites.Models.GuestResponse>
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Responses</title>
</head>
<body>
    <h2>Here is the list of people attending the party</h2>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Email</th>
                <th>Phone</th>
            </tr>
        </thead>
        <tbody>
            @foreach (PartyInvites.Models.GuestResponse r in Model)
            {
                <tr>
                    <td>@r.Name</td>
                    <td>@r.Email</td>
                    <td>@r.Phone</td>
                </tr>
            }
        </tbody>
    </table>
</body>
</html>

Razor视图文件的扩展名为cshtml, 因为它们是C#代码和HTML元素的混合. 你可以看到我使用了foreach循环来处理View传递过来的每一个GuestResponse. 不像一般的C#语句, Razor的foreach循环包括了HTML元素. 此例中, 每个GuestResponse对象创建一个tr元素.

添加验证

我现在应该向应用程序添加数据验证了. 如果没有数据验证, 用户可以输入无意义的数据, 甚至提交一个空表单. 在MVC应用程序中, 通常将验证应用到领域模型中, 而不是UI中. 这意味着你在一个地方定义了验证, 但会在使用模型类的任何地方起效. MVC支持System.ComponentModel.DataAnnotations中的属性的”声明式验证规则”, 这意味着验证约束是使用标准C#属性特性来表示的.
GuestResponse.cs中添加验证规则

// Models\GuestResponse.cs

using System.ComponentModel.DataAnnotations;

namespace PartyInvites.Models
{
    public class GuestResponse
    {
        [Required(ErrorMessage = "Please enter your name")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Please enter your email address")]
        [RegularExpression(".+\\@.+\\..+",
            ErrorMessage = "Please enter a valid email address")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Please enter your phone number")]
        public string Phone { get; set; }

        [Required(ErrorMessage = "Please specify whether you'll attend")]
        public bool? WillAttend { get; set; }
    }
}

MVC将自动检测属性, 并在模型绑定过程中进行验证.

提示: 之前提到, 我为WillAttend属性使用了bool?类型. 这样我就可以使用Required属性. 如果使用普通的bool类型, 那么WillAttend就只有true和false两个值, 不能判断用户将是否选了值. 使用bool?类型, 则浏览器可以发送null, 引发一个验证错误. 这是MVC将C#特性和HTML\HTTP优雅结合的一个例子.

在控制器中使用ModelState.IsValid属性来判断表单是否正确.

// Controllers\HomeController.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using PartyInvites.Models;

namespace PartyInvites.Controllers {

    public class HomeController : Controller {

        public ViewResult Index() {
            int hour = DateTime.Now.Hour;
            ViewBag.Greeting = hour < 12 ? "Good Moring" : "Good Afternoon";
            return View("MyView");
        }

        [HttpGet]
        public ViewResult RsvpForm()
        {
            return View();
        }

        [HttpPost]
        public ViewResult RsvpForm(GuestResponse guestResponse)
        {
            if (ModelState.IsValid)
            {
                Repository.AddResponse(guestResponse);
                return View("Thanks", guestResponse);
            }
            else
            {
                return View();
            }
        }

        public ViewResult ListResponses()
        {
            return View(Repository.Responses.Where(r => r.WillAttend == true));
        }
    }
}

Controller基类提供了一个属性ModelState, 这个属性提供将HTTP请求转换为C#对象的相关信息. 如果ModelState.IsValid == true, 就说明MVC已经满足我在GuestResponse上设置的验证约束. 如果满足的话, 就渲染Thanks视图. 否则即说明有验证错误, ModelState对象包含了问题的每个细节, 但不需要深入控制这些细节, 因为我们可以依赖一个有用的特性, 它通过调用无参数的View方法, 能够自动地让用户解决这些问题.
当MVC渲染视图时, Razor可以访问与请求相关的任何验证错误的细节, tag helper可以访问这些细节并向用户显示验证错误.

<!-- Views\Home\RsvpForm.cshtml -->

@model PartyInvites.Models.GuestResponse

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=devide-width" />
    <title>RsvpForm</title>
</head>
<body>
    <form asp-action="RsvpForm" method="post">
        <div asp-validation-summary="All"></div>
        <p>
            <label asp-for="Name">Your name:</label>
            <input asp-for="Name" />
        </p>
        <p>
            <label asp-for="Email">Your email:</label>
            <input asp-for="Email" />
        </p>
        <p>
            <label asp-for="Phone">Your phone:</label>
            <input asp-for="Phone" />
        </p>
        <p>
            <label asp-for="WillAttend">Will you attend?</label>
            <select asp-for="WillAttend">
                <option value="">Choose an option</option>
                <option value="true">Yes, I'll be there</option>
                <option value="false">No, I can't come</option>
            </select>
        </p>
        <button type="submit">Submit RSVP</button>
    </form>
</body>
</html>

属性asp-validation-summary应用于div元素, 会在渲染视图时显示错误列表(如果写成<div />的形式则不起作用, 会直接被Razor忽略). asp-validation-summary属性的值是一个名为ValidationSummary的枚举值, 我指定了All, 这是大多数应用程序的良好起点. 我将在第二十七章详细描述.
要查看validation summary如何运行, 请运行程序.
RsvpForm行为直到GuestResponse对象满足所有验证约束时, 才会渲染Thanks视图. 在Razor使用validation summary渲染视图时, 之前输入的内容会被保存. 这是使用模型绑定的另一个好处, 简化了表单数据的操作.

注意: 如果你之前编写过Web Forms, 你可能会了解一个概念”服务器控制”, 将状态值序列化并存储到一个隐藏的表单字段_VIEWSTATE中. MVC模型绑定和Web Forms的服务器控制\回传\视图状态无关. MVC并没有向HTML页面中注入一个_VIEWSTATE, 而是设置了input元素的value属性.

高亮非法字段

Tag helper属性将模型属性和DOM元素关联, 还有一个方便的特性, 即和模型绑定一起使用. 当模型类属性验证失败时, helper属性会生成稍微不同的HTML. 以下是没有验证错误时Phone字段生成的input元素.

<input type="text"
    data-val="true"
    data-val-required="Please enter your phone number"
    id="Phone"
    name="Phone"
value="">

作为比较, 下面是在输入空的Phone字段并提交后新页面的HTML:

<input type="text"
    class="input-validation-error"
    data-val="true"
    data-val-required="Please enter your phone number" id="Phone"  name="Phone"
value="">

验证错误的例子在渲染时为元素增加了一个类input-validation-error, 可以应用不同的CSS样式. MVC中的约定是将静态文件放置在wwwroot文件夹中, CSS文件放置在wwwroot/css文件夹中, JS文件放置在wwwroot/js文件夹中. 在wwwroot/css中新建一个CSS文件styles.css, 并填充内容如下:

/* wwwroot\css\styles.css */

.field-validation-error {
    color: #f00;
}

.field-validation-valid {
    display: none;
}

.input-validation-error {
    border: 1px solid #f00;
    background-color: #fee;
}

.validation-summary-errors {
    font-weight: bold;
    color: #f00;
}

.validation-summary-valid {
    display: none;
}

提示: VS在创建模版项目时自动创建了wwwroot/css/site.css文件, 你可以忽略此文件, 在本章也不会用到.

要让样式表起作用, 还需要在RsvpForm视图的head部分添加一个link元素:

<!-- Views\Home\RsvpForm.cshtml -->
<!-- ... -->
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
    <link rel="stylesheet" href="/css/styles.css" />
</head>
<!-- ... -->

link元素使用href来指定样式表的位置. 注意wwwroot文件夹在URL中被省略了. ASP.NET的默认配置包括静态文件服务, 例如图片, CSS文件和JS文件. 它会自动将请求映射到wwwroot文件夹. 我在第十四章中描述了ASP.NET和MVC的配置过程.

提示: 如果你需要管理很多文件的时候, 有一个特殊的tag helper可以用来处理样式表, 到第二十五章查看细节.

应用样式表后, 验证错误看起来更明显了.

第二章 你的第一个MVC应用程序

改变内容的显示风格

现在应用程序的所有功能都完备了, 但整体外观很差. 在使用模板创建项目时, VS将安装一些常见的前端库, 如Bootstrap, 这是一个很好的框架CSS框架, 最初由Twitter开发, 并成为了web应用程序开发的支柱之一.

注意: 在我写这篇文章时的版本是Bootstrap 3, 但Bootstrap 4已经在开发中. 微软可能会更新模版自动引入的版本, 但不影响我们现在的操作.

改变欢迎视图的显示风格

Bootstrap基本运行方式是将DOM元素的class属性关联到wwwroot/lib/bootstrap中的CSS选择器上. 你可以在http://getbootstrap.com了解Bootstrap的详情, 也可以看看我在MyView.cshtml视图中的基本运用.

<!-- Views\Home\MyView.cshtml -->

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <div class="text-center">
        <h3>We're going to have an exciting party!</h3>
        <h4>And you are invited</h4>
        <a class="btn btn-primary" asp-action="RsvpForm">RSVP Now</a>
    </div>
</body>
</html>

通过在head部分中添加了一个link元素引用了bootstrap库. 约定是第三方CSS和JS库被安装到wwwroot\lib中, 我在第六章描述了包管理工具.
引入bootstrap后, 只需要将一些元素的class属性与bootstrap中的类关联.

第二章 你的第一个MVC应用程序

改变回复表单的显示风格

Bootstrap定义了用于表单的类, 示例如下

<!-- Views\Home\RsvpForm.cshtml -->

@model PartyInvites.Models.GuestResponse

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>RsvpForm</title>
    <link rel="stylesheet" href="/css/styles.css" />
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <div class="panel panel-success">
        <div class="panel-heading text-center">
            <h4>RSVP</h4>
        </div>
        <div class="panel-body">
            <form class="p-a-1" asp-action="RsvpForm" method="post">
                <div asp-validation-summary="All"></div>
                <div class="form-group">
                    <label asp-for="Name">Your name:</label>
                    <input class="form-control" asp-for="Name" />
                </div>
                <div class="form-group">
                    <label asp-for="Email">Your email:</label>
                    <input class="form-control" asp-for="Email" />
                </div>
                <div class="form-group">
                    <label asp-for="Phone">Your phone:</label>
                    <input class="form-control" asp-for="Phone" />
                </div>
                <div class="form-group">
                    <label>Will you attend?</label>
                    <select class="form-control" asp-for="WillAttend">
                        <option value="">Choose an option</option>
                        <option value="true">Yes, I'll be there</option>
                        <option value="false">No, I can't come</option>
                    </select>
                </div>
                <div class="text-center">
                    <button class="btn btn-primary" type="submit">Submit RSVP</button>
                </div>
            </form>
        </div>
    </div>
</body>
</html>

第二章 你的第一个MVC应用程序

改变Thanks视图的显示风格

为了让应用更易于管理, 尽可能避免重复的代码和标记. MVC提供了一些有助于减少重复的特性, 我将在后面的章节中进行描述, 包括Razor布局(第五章), 局部视图(第二十一章)和视图组件(第二十二章)

<!-- Views\Home\Thanks.cshtml -->

@model PartyInvites.Models.GuestResponse

@{ 
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Thanks</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body class="text-center">
    <p>
        <h1>Thank you, @Model.Name!</h1>
        @if (Model.WillAttend == true)
        {
            @:It's great that you're coming. The drinks are already in the fridge!
        }
        else
        {
            @:Sorry to hear that you can't make it, but thanks for letting us know.
        }
    </p>
    <p>
        Click <a class="nav-link" asp-action="ListResponses">here</a> to see who is coming.
    </p>
</body>
</html>

第二章 你的第一个MVC应用程序

改变List视图的显示风格

<!-- Views\Home\ListResponses.cshtml -->

@model IEnumerable<PartyInvites.Models.GuestResponse>
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Responses</title>
    <link rel="stylesheet" href="/lib/bootstrap/dist/css/bootstrap.css" />
</head>
<body>
    <div class="panel-body">
        <h2>Here is the list of people attending the party</h2>
        <table class="table table-sm table-striped table-bordered">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                    <th>Phone</th>
                </tr>
            </thead>
            <tbody>
                @foreach (PartyInvites.Models.GuestResponse r in Model)
                {
                    <tr>
                        <td>@r.Name</td>
                        <td>@r.Email</td>
                        <td>@r.Phone</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</body>
</html>

第二章 你的第一个MVC应用程序

总结

在本章中, 我创建了一个新的MVC项目, 并构造了一个简单的输入数据的应用程序, 让你对ASP.NET Core MVC架构和方法有了初步认知. 我跳过了一些关键特性(Razor语法, 路由和测试), 但会在后面的章节中深入讨论.
下一章, 我将介绍MVC设计模式, 这是使用ASP.NET Core MVC进行有效开发的基础.