ASP.NET Core MVC 中的视图

作者:Steve SmithDave Brock

本文档介绍在 ASP.NET Core MVC 应用程序中使用的视图。 有关 Pages 的信息Razor,请参阅 ASP.NET Core 中的 Pages 简介Razor

在“模型-视图-控制器(MVC)”模式中,视图处理应用的数据表示和用户交互。 视图是带有嵌入 Razor 标记的 HTML 模板。 Razor 标记一个代码,用于与 HTML 标记交互以生成发送给客户端的网页。

在 ASP.NET Core MVC 中,视图是在 Razor 标记中使用 C# 编程语言.cshtml 文件。 通常,视图文件会分组到以每个应用的控制器命名的文件夹中。 文件夹存储在应用根目录的 Views 文件夹中:

Views folder in Solution Explorer of Visual Studio is open with the Home folder open to show About.cshtml, Contact.cshtml, and Index.cshtml files

Home控制器由 Views 文件夹内的 Home 文件夹表示。 Home 文件夹包含“About”、“Contact”和“Index”(主页)网页的视图。 用户请求这三个网页中的一个时,Home控制器中的控制器操作决定使用三个视图中的哪一个来生成网页并将其返回给用户。

使用布局提供一致的网页部分并减少代码重复。 布局通常包含页眉、导航和菜单元素以及页脚。 页眉和页脚通常包含许多元数据元素的样板标记以及脚本和样式资产的链接。 布局有助于在视图中避免这种样板标记。

分部视图通过管理视图的可重用部分来减少代码重复。 例如,分部视图可用于在多个视图中出现的博客网站上的作者简介。 作者简介是普通的视图内容,不需要执行代码就能生成网页的内容。 可以仅通过模型绑定查看作者简介内容,因此使用这种内容类型的分部视图是理想的选择。

视图组件与分部视图的相似之处在于它们可以减少重复性代码,但视图组件还适用于需要在服务器上运行代码才能呈现网页的视图内容。 呈现的内容需要数据库交互时(例如网站购物车),视图组件非常有用。 为了生成网页输出,视图组件不局限于模型绑定。

使用视图的好处

视图可帮助在 MVC 应用内建立关注点分离,方法是分隔用户界面标记与应用的其他部分。 采用 SoC 设计可使应用模块化,从而提供以下几个好处:

  • 应用组织地更好,因此更易于维护。 视图一般按应用功能进行分组。 这使得在处理功能时更容易找到相关的视图。
  • 应用的若干部分是松散耦合的。 可以生成和更新独立于业务逻辑和数据访问组件的应用视图。 可以修改应用的视图,而不必更新应用的其他部分。
  • 因为视图是独立的单元,所以更容易测试应用的用户界面部分。
  • 由于应用组织得更好,因此你不太可能会意外重复用户界面的各个部分。

创建视图

Views/[ControllerName] 文件夹中创建特定于控制器的视图。 控制器之间共享的视图都将置于 Views/Shared 文件夹。 要创建一个视图,请添加新文件,并将其命名为与 .cshtml 文件扩展名相关联的控制器操作的相同名称。 要创建与Home控制器中 About 操作相对应的视图,请在 Views/Home 文件夹中创建一个 About.cshtml 文件:

@{
    ViewData["Title"] = "About";
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>

Razor 标记@ 符号开头。 通过在大括号 () { ... } 设置的代码块中Razor放置 C# 代码来运行 C# 语句。 有关示例,请参阅上面显示的“About”到 ViewData["Title"] 的分配。 只需用 @ 符号来引用值,即可在 HTML 中显示这些值。 请参阅上面的 <h2><h3> 元素的内容。

以上所示的视图内容只是呈现给用户的整个网页中的一部分。 其他视图文件中指定了页面布局的其余部分和视图的其他常见方面。 要了解详细信息,请参阅布局主题

控制器如何指定视图

视图通常以 ViewResult 的形式从操作返回,这是一种 ActionResult 类型。 操作方法可以直接创建并返回 ViewResult,但通常不会这样做。 由于大多数控制器均继承自 Controller,因此只需使用 View 帮助程序方法即可返回 ViewResult

HomeController.cs:

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

    return View();
}

此操作返回时,最后一节显示的 About.cshtml 视图呈现为以下网页:

About page rendered in the Edge browser

View 帮助程序方法有多个重载。 可选择指定:

  • 要返回的显式视图:

    return View("Orders");
    
  • 要传递给视图的模型

    return View(Orders);
    
  • 视图和模型:

    return View("Orders", Orders);
    

视图发现

操作返回一个视图时,会发生称为“视图发现”的过程。 此过程基于视图名称确定使用哪个视图文件。

View 方法 的默认行为 (return View();) 旨在返回与其从中调用的操作方法同名的视图。 例如,控制器的 AboutActionResult 方法名称用于搜索名为 About.cshtml 的视图文件。 运行时首先在 Views/[ControllerName] 文件夹中搜索该视图。 如果在此处找不到匹配的视图,则会在 Shared 文件夹中搜索该视图。

return View(); 隐式返回 ViewResult 还是用 return View("<ViewName>"); 将视图名称显式传递给 View 方法并不重要。 在这两种情况下,视图发现都会按以下顺序搜索匹配的视图文件:

  1. Views/\[ControllerName]/\[ViewName].cshtml
  2. Views/Shared/\[ViewName].cshtml

可以提供视图文件路径而不提供视图名称。 如果使用从应用根目录开始的绝对路径(可选择以“/”或“~/”开头),则须指定 .cshtml 扩展名:

return View("Views/Home/About.cshtml");

也可使用相对路径在不同目录中指定视图,而无需指定 .cshtml 扩展名。 在 HomeController 内,可以使用相对路径返回 Manage 视图的 Index 视图:

return View("../Manage/Index");

同样,可以用“./”前缀来指示当前的控制器特定目录:

return View("./About");

分部视图视图组件使用类似(但不完全相同)的发现机制。

可以使用自定义 IViewLocationExpander 自定义如何在应用中定位视图的默认约定。

视图发现依赖于按文件名称查找视图文件。 如果基础文件系统区分大小写,则视图名称也可能区分大小写。 为了各操作系统的兼容性,请在控制器与操作名称之间,关联视图文件夹与文件名称之间匹配大小写。 如果在处理区分大小写的文件系统时遇到无法找到视图文件的错误,请确认请求的视图文件与实际视图文件名称之间的大小写是否匹配。

按照组织视图文件结构的最佳做法,反映控制器、操作和视图之间的关系,实现可维护性和清晰度。

将数据传递给视图

使用多种方法将数据传递给视图:

  • 强类型数据:viewmodel
  • 弱类型数据
    • ViewData (ViewDataAttribute)
    • ViewBag

强类型数据 (viewmodel)

最可靠的方法是在视图中指定模型类型。 此模型通常称为 viewmodel。 将 viewmodel 类型的实例传递给此操作的视图。

使用 viewmodel 将数据传递给视图可让视图充分利用强类型检查。 强类型化(或强类型)意味着每个变量和常量都有明确定义的类型(例如 stringintDateTime。 在编译时检查视图中使用的类型是否有效。

Visual StudioVisual Studio Code 列出了使用 IntelliSense 功能的强类型类成员。 如果要查看 viewmodel 的属性,请键入 viewmodel 的变量名称,后跟句点 (.)。 这有助于提高编写代码的速度并降低错误率。

使用 @model 指令指定模型。 使用带有 @Model 的模型:

@model WebApplication1.ViewModels.Address

<h2>Contact</h2>
<address>
    @Model.Street<br>
    @Model.City, @Model.State @Model.PostalCode<br>
    <abbr title="Phone">P:</abbr> 425.555.0100
</address>

为了将模型提供给视图,控制器将其作为参数进行传递:

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

    var viewModel = new Address()
    {
        Name = "Microsoft",
        Street = "One Microsoft Way",
        City = "Redmond",
        State = "WA",
        PostalCode = "98052-6399"
    };

    return View(viewModel);
}

没有针对可以提供给视图的模型类型的限制。 建议使用普通旧 CLR 对象 (POCO) viewmodel,它几乎没有已定义的行为(方法)。 通常,viewmodel 类要么存储在 Models 文件夹中,要么存储在应用根目录处的单独 ViewModels 文件夹中。 上例中使用的 Address viewmodel 是存储在 Address.cs 文件中的 POCO viewmodel:

namespace WebApplication1.ViewModels
{
    public class Address
    {
        public string Name { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
    }
}

可随意对 viewmodel 类型和业务模型类型使用相同的类。 但是,使用单独的模型可使视图独立于应用的业务逻辑和数据访问部分。 模型为用户发送给应用的数据使用模型绑定验证时,模型和 viewmodel 的分离也会提供安全优势。

弱类型数据(ViewData[ViewData] 属性和 ViewBag

ViewBag默认情况下,不可用以用于 Razor PagesclassesPageModel

除了强类型视图,视图还可以访问弱类型(也称为松散类型)的数据集合。 与强类型不同,弱类型(或松散类型)意味着不显式声明要使用的数据类型。 可以使用弱类型数据集合将少量数据传入及传出控制器和视图。

传递数据于... 示例
控制器和视图 用数据填充下拉列表。
视图和布局视图 从视图文件设置布局视图中的 <title> 元素内容。
分部视图和视图 基于用户请求的网页显示数据的小组件。

可以通过控制器和视图上的 ViewDataViewBag 属性来引用此集合。 ViewData 属性是弱类型对象的字典。 ViewBag 属性是 ViewData 的包装器,为基础 ViewData 集合提供动态属性。 注意:对于 ViewDataViewBag,键查找都不区分大小写。

ViewDataViewBag 在运行时进行动态解析。 由于它们不提供编译时类型检查,因此使用这两者通常比使用 viewmodel 更容易出错。 出于上述原因,一些开发者希望尽量减少或根本不使用 ViewDataViewBag

ViewData

ViewData 是通过 string 键访问的 ViewDataDictionary 对象。 字符串数据可以直接存储和使用,而不需要强制转换,但是在提取其他 ViewData 对象值时必须将其强制转换为特定类型。 可用于 ViewData 将数据从控制器传递到视图和视图中,包括 部分视图布局

以下是在操作中使用 ViewData 设置问候语和地址值的示例:

public IActionResult SomeAction()
{
    ViewData["Greeting"] = "Hello";
    ViewData["Address"]  = new Address()
    {
        Name = "Steve",
        Street = "123 Main St",
        City = "Hudson",
        State = "OH",
        PostalCode = "44236"
    };

    return View();
}

在视图中处理数据:

@{
    // Since Address isn't a string, it requires a cast.
    var address = ViewData["Address"] as Address;
}

@ViewData["Greeting"] World!

<address>
    @address.Name<br>
    @address.Street<br>
    @address.City, @address.State @address.PostalCode
</address>

[ViewData] 特性

使用 ViewDataDictionary 的另一种方法是 ViewDataAttribute。 控制器或 Razor 页面模型上使用 [ViewData] 特定的属性将其值存储在字典中并从中进行加载。

在下面的示例中,“Home”控制器包含使用 [ViewData] 标记的 Title 属性。 About 方法设置“关于”视图的标题:

public class HomeController : Controller
{
    [ViewData]
    public string Title { get; set; }

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

        return View();
    }
}

在布局中,从 ViewData 字典读取标题:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"] - WebApplication</title>
    ...

ViewBag

ViewBag默认情况下,不可用以用于 Razor PagesclassesPageModel

ViewBagMicrosoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData 对象,可提供对存储在 ViewData 中对象的动态访问权限。 ViewBag 不需要强制转换,因此使用起来更加方便。 下例演示如何使用与上述 ViewData 有相同结果的 ViewBag

public IActionResult SomeAction()
{
    ViewBag.Greeting = "Hello";
    ViewBag.Address  = new Address()
    {
        Name = "Steve",
        Street = "123 Main St",
        City = "Hudson",
        State = "OH",
        PostalCode = "44236"
    };

    return View();
}
@ViewBag.Greeting World!

<address>
    @ViewBag.Address.Name<br>
    @ViewBag.Address.Street<br>
    @ViewBag.Address.City, @ViewBag.Address.State @ViewBag.Address.PostalCode
</address>

同时使用 ViewDataViewBag

ViewBag默认情况下,不可用以用于 Razor PagesclassesPageModel

由于 ViewDataViewBag 引用相同的基础 ViewData 集合,因此在读取和写入值时,可以同时使用 ViewDataViewBag,并在两者之间进行混合和匹配。

About.cshtml 视图顶部,使用 ViewBag 设置标题并使用 ViewData 设置说明:

@{
    Layout = "/Views/Shared/_Layout.cshtml";
    ViewBag.Title = "About Contoso";
    ViewData["Description"] = "Let us tell you about Contoso's philosophy and mission.";
}

读取属性,但反向使用 ViewDataViewBag。 在 _Layout.cshtml 文件中,使用 ViewData 获取标题并使用 ViewBag 获取说明:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>@ViewData["Title"]</title>
    <meta name="description" content="@ViewBag.Description">
    ...

请记住,字符串不需要为 ViewData 进行强制转换。 可以使用 @ViewData["Title"] 而不需要强制转换。

可同时使用 ViewDataViewBag也可混合和匹配读取及写入属性。 呈现以下标记:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>About Contoso</title>
    <meta name="description" content="Let us tell you about Contoso's philosophy and mission.">
    ...

ViewDataViewBag 之间差异的摘要

ViewBag默认情况下,不可用以用于 Razor PagesclassesPageModel

  • ViewData
    • 派生自 ViewDataDictionary,因此它有可用的字典属性,如 ContainsKeyAddRemoveClear
    • 字典中的键是字符串,因此允许有空格。 示例: ViewData["Some Key With Whitespace"]
    • 任何非 string 类型均须在视图中进行强制转换才能使用 ViewData
  • ViewBag
    • 派生自 Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.DynamicViewData,因此它可使用点表示法 (@ViewBag.SomeKey = <value or object>) 创建动态属性,且无需强制转换。 ViewBag 的语法使添加到控制器和视图的速度更快。
    • 更易于检查 NULL 值。 示例: @ViewBag.Person?.Name

何时使用 ViewDataViewBag

ViewDataViewBag 都是在控制器和视图之间传递少量数据的有效方法。 根据偏好选择使用哪种方法。 可以混合和匹配 ViewDataViewBag 对象,但是,使用一致的方法可以更轻松地读取和维护代码。 这两种方法都是在运行时进行动态解析的,因此容易造成运行时错误。 因而,一些开发团队会避免使用它们。

动态视图

不使用 @model 声明模型类型,但有模型实例传递给它们的视图(如 return View(Address);)可动态引用实例的属性:

<address>
    @Model.Street<br>
    @Model.City, @Model.State @Model.PostalCode<br>
    <abbr title="Phone">P:</abbr> 425.555.0100
</address>

此功能提供了灵活性,但不提供编译保护或 IntelliSense。 如果属性不存在,则网页生成在运行时会失败。

更多视图功能

标记帮助程序可以轻松地将服务器端行为添加到现有的 HTML 标记。 使用标记帮助程序可避免在视图内编写自定义代码或帮助程序。 标记帮助程序作为属性应用于 HTML 元素,并被无法处理它们的编辑器忽略。 这可让你在各种工具中编辑和呈现视图标记。

通过许多内置 HTML 帮助程序可生成自定义 HTML 标记。 通过视图组件可以处理更复杂的用户界面逻辑。 视图组件提供的 SoC 与控制器和视图提供的相同。 它们无需使用处理数据(由常见用户界面元素使用)的操作和视图。

与 ASP.NET Core 的许多其他方面一样,视图支持依赖关系注入,可将服务注入视图

CSS 隔离

将 CSS 样式隔离到各个页面、视图和组件以减少或避免:

  • 依赖难以维护的全局样式。
  • 嵌套内容中的样式冲突。

若要为页面或视图添加限定范围的 CSS 文件,请将 CSS 样式置于与 .cshtml 文件的名称匹配的配套 .cshtml.css 文件中。 在下面的示例中,Index.cshtml.css 文件提供只应用于 Index.cshtml 页面或视图的 CSS 样式。

Pages/Index.cshtml.css (Razor Pages) 或 Views/Index.cshtml.css (MVC):

h1 {
    color: red;
}

CSS 隔离在生成时发生。 框架会重写 CSS 选择器,以匹配应用页面或视图所呈现的标记。 重写的 CSS 样式作为静态资产 {APP ASSEMBLY}.styles.css 进行捆绑和生成。 占位符 {APP ASSEMBLY} 是项目的程序集名称。 指向捆绑 CSS 样式的链接放置在应用的布局中。

在应用 Pages/Shared/_Layout.cshtml (Razor Pages) 或 Views/Shared/_Layout.cshtml (MVC) 的 <head> 内容中,添加或确认是否存在指向捆绑 CSS 样式的链接:

<link rel="stylesheet" href="{APP ASSEMBLY}.styles.css" />

在下面的示例中,应用的程序集名称为 WebApp

<link rel="stylesheet" href="WebApp.styles.css" />

在限定范围的 CSS 文件中定义的样式仅应用于匹配文件的呈现输出。 在上面的示例中,在应用的其他位置定义的任何 h1 CSS 声明都不会与 Index 标头样式冲突。 CSS 样式级联和继承规则对限定范围的 CSS 文件仍然有效。 例如,直接应用于 Index.cshtml 文件中的 <h1> 元素的样式会替代 Index.cshtml.css 中限定范围的 CSS 文件的样式。

注意

为了保证发生捆绑时的 CSS 样式隔离,不支持在 Razor 代码块中导入 CSS。

CSS 隔离仅适用于 HTML 元素。 标记帮助程序不支持 CSS 隔离。

在捆绑 CSS 文件中,每个页面、视图或 Razor 组件都与格式为 b-{STRING} 的范围标识符相关联,其中 {STRING} 占位符是框架生成的十个字符的字符串。 下面的示例提供了 Razor Pages 应用 Index 页面中前面 <h1> 元素的样式:

/* /Pages/Index.cshtml.rz.scp.css */
h1[b-3xxtam6d07] {
    color: red;
}

在从捆绑文件应用 CSS 样式的 Index 页面中,范围标识符追加为 HTML 属性:

<h1 b-3xxtam6d07>

标识符对应用是唯一的。 在生成时,会使用约定 {STATIC WEB ASSETS BASE PATH}/Project.lib.scp.css 创建项目捆绑包,其中占位符 {STATIC WEB ASSETS BASE PATH} 是静态 Web 资产的基路径。

如果利用了其他项目(如 NuGet 包或Razor 类库),则捆绑的文件将发生以下情况:

  • 使用 CSS 导入引用这些样式。
  • 不会发布为使用这些样式的应用的静态 Web 资产。

CSS 预处理器支持

利用 CSS 预处理器的变量、嵌套、模块、混合和继承等功能,可有效改进 CSS 开发。 虽然 CSS 隔离并不原生支持 CSS 预处理器(如 Sass 或 Less),但只要在生成过程中框架重写 CSS 选择器的步骤之前进行预处理器编译,就可以无缝集成 CSS 预处理器。 例如,使用 Visual Studio 将现有预处理器编译配置为 Visual Studio 任务运行程序资源管理器中的“生成前”任务。

许多第三方 NuGet 包(如 Delegate.SassBuilder)都可以在生成过程开始时编译 SASS/SCSS 文件,再进行 CSS 隔离,而无需其他配置。

CSS 隔离配置

CSS 隔离允许在某些高级场景(例如依赖于现有工具或工作流)下进行配置。

自定义范围标识符格式

在此部分中,{Pages|Views} 占位符为 Razor Pages 应用的 Pages 或 MVC 应用的 Views

默认情况下,范围标识符使用格式 b-{STRING},其中 {STRING} 占位符是框架生成的十个字符的字符串。 若要自定义范围标识符格式,请将项目文件更新为所需模式:

<ItemGroup>
  <None Update="{Pages|Views}/Index.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

在上面的示例中,为 Index.cshtml.css 生成的 CSS 将其范围标识符从 b-{STRING} 更改为了 custom-scope-identifier

使用范围标识符来实现与限定范围的 CSS 文件的继承。 在下面的项目文件示例中,BaseView.cshtml.css 文件包含跨视图的通用样式。 DerivedView.cshtml.css 文件继承了这些样式。

<ItemGroup>
  <None Update="{Pages|Views}/BaseView.cshtml.css" CssScope="custom-scope-identifier" />
  <None Update="{Pages|Views}/DerivedView.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

使用通配符 (*) 运算符跨多个文件共享范围标识符:

<ItemGroup>
  <None Update="{Pages|Views}/*.cshtml.css" CssScope="custom-scope-identifier" />
</ItemGroup>

更改静态 Web 资产的基路径

限定范围的 CSS 文件在应用的根目录生成。 在项目文件中,请使用 StaticWebAssetBasePath 属性来更改默认路径。 下面的示例将限定范围的 CSS 文件以及应用的其余资产放在 _content 路径:

<PropertyGroup>
  <StaticWebAssetBasePath>_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>

禁用自动捆绑

若要禁用框架在运行时发布和加载限定范围的文件,请使用 DisableScopedCssBundling 属性。 使用此属性时,由其他工具或进程从 obj 目录中捕获隔离的 CSS 文件,并在运行时发布和加载这些文件:

<PropertyGroup>
  <DisableScopedCssBundling>true</DisableScopedCssBundling>
</PropertyGroup>

Razor 类库 (RCL) 支持

Razor 类库 (RCL) 提供隔离的样式,<link> 标记的 href 属性指向 {STATIC WEB ASSET BASE PATH}/{PACKAGE ID}.bundle.scp.css,其中占位符为:

  • {STATIC WEB ASSET BASE PATH}:静态 Web 资产基路径。
  • {PACKAGE ID}:库的包标识符。 如果项目文件中未指定包标识符,则包标识符默认为项目的程序集名称。

如下示例中:

  • 静态 Web 资产基路径为 _content/ClassLib
  • 类库的程序集名称为 ClassLib

Pages/Shared/_Layout.cshtml (Razor Pages) 或 Views/Shared/_Layout.cshtml (MVC):

<link href="_content/ClassLib/ClassLib.bundle.scp.css" rel="stylesheet">

有关 RCL 的详细信息,请参阅以下文章:

有关 Blazor CSS 隔离的信息,请参阅 ASP.NET Core Blazor CSS 隔离