文章目录
为什么需要在ASP.NET Core MVC中使用ViewModel
在某些情况下,Model对象可能无法包含View所需的所有数据。这个时候就需要使用ViewModel了,它会包含所需的所有数据,请参考以下Details()方法。注意,需要将学生的详细信息和PageTitle传递给View。
public class HomeController:Controller
{
private readonly IStudentRepository _studentRepository;
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public ViewResult Details()
{
Student model = _studentRepository.GetStudent(1);
ViewBag.PageTitle = "学生详情";
return View(model);
}
}
我们使用Student模型对象传递学生的详细信息,而是用ViewBag传递PageTitle
。使用ViewBag将数据从Controller传递到View会创建一个弱类型的视图,因此我们不会得到智能提示,以及编译时类型和错误检查。
在Student类中包含PageTitle属性没有任何意义,因为Student类应该只包含与学生相关的属性。此时我们就需要创建一个能包含View所需的数据类,该类称为ViewModel。
ViewModel示例
ViewModel类可以放在ASP.NET Core MVC项目的任何位置。但为了管理方便,我们通常将它们放在一个名为ViewModels的文件夹中。
我们将创建一个名为HomeDetailsViewModel的类。这个类的名称中有Home一词,这是因为Controller的名称是HomeController。因为操作方法的名称是Details(),所以包含单词Details()。
此ViewModel类包含View所需的所有数据。通常,我们使用ViewModels在View和Controller之间传递数据。因此,VIewModels也简称为数据传输对象或DTO(Data Transfer Object,数据传输对象)。
现在可以使用此ViewModel装载Student类的数据和PageTitle,然后从Controller传递数据到View,代码如下:
public class HomeDetailsViewModel
{
public Student Student { get; set; }
public string PageTitle { get; set; }
}
在Controller中使用ViewModel
在Controller中使用ViewModel的代码如下:
public ViewResult Details()
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = _studentRepository.GetStudent(1),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
在视图中使用ViewModel
- 该视图可以使用VIew()方法访问控制器中的ViewModel对象。
- 使用
@model
指令,将HomeDetailsViewModel设置为视图的Model。 - 然后就可以访问学生的详细信息和PageTitle的属性。
- 请注意,
@model
指令中的 ‘m’ 为小写字母,@Model
属性的 **“M”**为大写字母。
@model WebApplication12.ViewModels.HomeDetailsViewModel
<html>
<head>
<title>Details</title>
</head>
<body>
<h3>@Model.PageTitle</h3>
<div>
姓名:@Model.Student.Name
</div>
<div>
邮箱:@Model.Student.Email
</div>
<div>
主修科目:@Model.Student.Major
</div>
</body>
</html>
在ASP.NET Core MVC中实现List视图
让我们通过一个示例来理解这一点。我们想要查询所有学生信息并将其显示在网页上,如表所示:
ID | Name | Major | |
---|---|---|---|
1 | 张三 | zhangsan@qq.com | 计科 |
2 | 李四 | lisi@foxmail.com | 信安 |
修改IStuentRepository中的代码
修改IStudentRepository接口以包含GetAllStudents()方法,此方法返回所有学生的列表信息。
public interface IStudentRepository
{
Student GetStudent(int ID);
IEnumerable<Student> GetAllStudents();
}
修改StudentRepository中的代码
目前,在我们的应用程序中,只有一个实现IStudentRepository
接口的类(StudentRepository
)。因此修改StudentRepository类文件,实现GetAllStudents()方法。
注意,GetAllStudents()返回的是_studentList中的学生列表,而这些数据都是我们硬编码来的。
public class StudentRepository : IStudentRepository
{
private List<Student> _studentList;
public StudentRepository()
{
_studentList = new List<Student>() {
new Student(){ID=1,Name="张三",Major="计科",Email="zhangsan@qq.com"},
new Student(){ID=2,Name="李四",Major="信安",Email="lisi@outlook.com"},
new Student(){ID=3,Name="王五",Major="网工",Email="wangwu@foxmail.com"}
};
}
public IEnumerable<Student> GetAllStudents()
{
return _studentList;
}
public Student GetStudent(int ID)
{
return _studentList.FirstOrDefault(a => a.ID == ID);
}
}
修改HomeController中的代码
我们需要添加HomeController()中的Index()方法,如下所示:
public class HomeController:Controller
{
private readonly IStudentRepository _studentRepository;
public HomeController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
//返回学生的信息
public ViewResult Index()
{
//查询所有的学生信息
var model = _studentRepository.GetAllStudents();
//将学生列表传递到视图
return View(model);
}
public ViewResult Details()
{
//实例化HomeDetailsViewModel并存储Student详细信息和PageTitle
HomeDetailsViewModel homeDetailsViewModel = new HomeDetailsViewModel()
{
Student = _studentRepository.GetStudent(1),
PageTitle = "学生详情"
};
//将ViewModel对象传递给View()方法
return View(homeDetailsViewModel);
}
}
请注意,我们通过调用GetAllStudents()方法查询学生列表,并将该列表传递给View。
视图Index.cshtml中代码的变化
使用@model
指令来为View指定模型IEnumberable<WebApplication12.Models.Student>,同时在View指令中使用foreach循环遍历学生列表,并动态生成表单的行和单元格以显示学生信息,代码如下:
@model IEnumerable<WebApplication12.Models.Student>
<html>
<head>
<title>学生详细信息</title>
</head>
<body>
<table align="center" border="1" cellpadding="0" cellspacing="0" width="500" height="250">
<thead >
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Major</th>
</tr>
</thead>
<tbody>
@foreach (var student in Model)
{
<tr>
<td >@student.ID</td>
<td>@student.Name</td>
<td>@student.Email</td>
<td>@student.Major</td>
</tr>
}
</tbody>
</table>
</body>
</html>
布局视图
如果读者平时浏览网页,会发现大多Web应用程序网站由以下部分组成。
- Header——头部。
- Footer——页脚。
- Menu——导航菜单。
- View——具体内容的视图。
学过前端的同学,一定可以理解这一点。没学过的,可以看看淘宝、京东等网站,观察一下,基本上由上述四部分组成。
如果没有布局视图,我们将在Web程序中的每个视图中重复显示很多HTML代码,比如菜单栏、导航信息、关于我们和页脚等。在每个视图中都有这些重复的HTML,这样Web程序的维护是一场灾难。
比如,我们需要在导航菜单中添加或删除菜单项,或更改页眉或页脚。我们必须在每个视图中进行此修改,这显然单调乏味、耗时且容易出错,试想读者如果有400个视图文件需要维护会怎么样。那么我们可以在布局视图中定义它们,然后在所有视图中继承该布局视图文件,而不是在每个视图中都包含所有这些部分。
使用布局视图,让所有视图保持一致的外观变得更加容易,因为如果有任何更改,则只要一个要修改的布局视图文件,然后更改后将立即反映在整个应用程序的所有视图中,这样易于维护而且还提升效率。
ASP.NET Core MVC中的布局视图
就像常规视图一样,布局视图也具有扩展名为.cshtml的文件。读者如果使用过WebForm,则可以将布局视图视为ASP.NET Web Form中的母版页。
由于布局视图不特定于控制器,因此它通常放Views文件夹的子文件夹Shared中。默认情况下,在ASP.NET Core MVC中,布局视图文件名为**_Layout.cshtml**。
在ASP.NET Core MVC中有一些视图文件,如**_Layout.cshtml、_ViewStart.cshtml和_ViewImports.cshtml等,它们的文件名以下划线开头。这些文件名中的下划线**表示这些文件不是直接面向浏览器的,因此用户无法直接访问它们。它们是服务于应用程序的。一个应用程序中包含多个布局视图文件。比如一个布局试图文件服务于管理用户,另一个不同的布局视图文件服务于普通用户。
创建布局视图
创建布局视图的步骤如下:
- 右击Views文件夹并添加Shared文件夹。
- 右击Shared文件夹,然后选择添加➡新建项。
- 在添加新项窗口中选择Razor布局并单击添加按钮。
- 名为**_Layout.cshtml**的文件将添加到Shared文件夹。
- 打开**_Layout.cshtml**文件,其中已经自动生成HTML代码。
以下是**_Layout.cshtml**中默认生成的HTML代码。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
请注意,此布局视图文件已经包含了**<html>、<head>、<title>和<body>**元素。现在将它们放在布局试图文件中,因此我们不必在每个视图中重复所有这些HTML。
使用@ViewBag.Title
指令查看特定标题。比如,当使用此布局试图文件呈现Index.cshtml
视图时,Index.cshtml
将在ViewBag上设置Title属性。
使用指令@ViewBag.Title
通过布局视图检索它,并将其设置为<title>
元素的值。
ViewBag不提供智能提示和编译时错误检查。因此,使用它将大量数据从普通Razor视图传递到布局视图并不是很好,但是传递像PageTitle这样非常小的内容,就很合适了。
@RenderBody()
是注入视图特定内容的位置。比如,如果使用此布局视图呈现Index.cshtml视图,则会在我们调用@RenderBody()
方法的位置注入Index.cshtml视图内容。
使用布局视图
要使用布局视图(_Layout.cshtml)渲染视图,需要设置Layout属性。比如,要将布局视图与Details.cshtml一起使用,需要修改Details.cshtml中的代码以包含Layout属性,如下所示:
@model WebApplication12.ViewModels.HomeDetailsViewModel
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Student Details";
}
<h3>@Model.PageTitle</h3>
<div>
姓名:@Model.Student.Name
</div>
<div>
邮箱:@Model.Student.Email
</div>
<div>
主修科目:@Model.Student.Major
</div>
有一种更好的方法来设置Layout属性,而不是在每个视图中设置它,我们将在后面的内容中学习此方法。
总体来说,关于布局视图,我们需要了解以下内容。
- 布局视图是让Web应用程序中所有的视图保持外观一致。
- 布局视图看起来像ASP.NET WebForm中的母版页。
- 布局视图也具有.cshtml扩展名。
- 在ASP.NET Core MVC中,默认情况下布局文件名为_Layout.cshtml。
- 布局视图文件通常放在Views/Shared文件夹中。
- 在一个应用程序中可以包含多个布局视图文件。
布局页面的节点
ASP.NEt Core MVC中的布局页面还可以包含一些节点。节点可以是必需的,也可以是可选的,它提供了一种方法让某些页面元素有组织地放置在一起。
布局页面示例
假设读者有一个自定义Javascript文件,如果所有视图都需要这个自定义Javascript文件,那么我们可以将它放在布局页面中,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
<script src="/wwwroot/js/JavaScript.js"></script>
</body>
</html>
在这个例子中,不需要在每个视图中使用自定义Javascript文件。假设我们只在Details视图中需要它,而在其他视图中不需要它,就可以使用一个节点。
渲染节点
在布局页面中,在要渲染节点内容的位置调用RenderSection()
方法。在本例中,我们把@RenderSection()
放置在<body>结束元素之前。
RenderSection()方法有两个参数。第一个参数指定节点的名称,第二个参数指定该节点是必需的还是可选的。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
@RenderSection("Scripts", required: false)
</body>
</html>
如果将required
设置为true,而内容视图不包含该部分,则会出现以下错误。
使布局部分可选
有两个选项将布局部分标记为可选。
- 选项1:将
RenderSection()
方法的必需参数设置为false。
@RednerSection("Scripts",required:false);
- 选项2:使用
IsSectionDefined()
方法
@if (IsSectionDefined("Scripts"))
{
@RenderSection("Scripts",required:false);
}
节点的使用
要使用节点,那么每个视图都必须包含具有相同名称的部分。我们使用@section
指令包含该部分并提供如下所示的内容。
在我们的示例中希望Details视图在布局页面的Scripts节点中包含<script>元素。为此,我们在Details.cshtml中包含Scripts节点,如下所示:
@model WebApplication12.ViewModels.HomeDetailsViewModel
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Student Details";
}
<h3>@Model.PageTitle</h3>
<div>
姓名:@Model.Student.Name
</div>
<div>
邮箱:@Model.Student.Email
</div>
<div>
主修科目:@Model.Student.Major
</div>
@section Scripts{
<script src="/js/JavaScript.js"></script>
}
运行此项目测试节点是否生效。当导航到/Home/Details时,查看网页源代码,可以看到<script>元素位于</body>结束元素之前。
而我们导航到/Home/Index查看网页源码,看不到<script>元素,如图所示,表示节点已经正常运行。
什么是_ViewStart.cshtml文件
在当前Details.cshtml中,通过代码Layout="~/Views/Shared/_Layout.cshtml"
;的形式指定布局页。Layout属性会将视图与布局视图相关联。但是这样会引发一个问题,如果要使用其他布局文件,则需要更新每个视图。试想,如果项目有400个视图文件,而现在要修改其中200个文件,这项工作不仅繁琐而且耗时,而且还容易出错。MVC为我们提供了Razor试图开始文件,也就是**_ViewStart.cshtml**文件。
如果没有**_ViewStart.cshtml文件,那么我们需要在每个视图中设置Layout**属性,这违反了DRY(Don’ t Repeat Yourself)原则,并具有以下缺点:
- 冗余代码。
- 维护成本高。
ASP.NET Core MVC中的_ViewStart.cshtml文件
该文件是ASP.NET Core MVC中的一个特殊文件,该文件中的代码在调用单个视图中的代码之前执行。我们将公共代码放到**_ViewStart.cshtml**文件中,而不是在每个单独的视图中设置Layout属性。
- 右击Views文件夹,然后依次选择添加和新建项。
- 在添加新项窗口中选择Razor视图开始并单击添加按钮。
- 将名为**_ViewStart.cshtml的文件添加到Views**文件夹中。
- 打开**_ViewStart.cshtml**文件,其中已经自动生成的HTML代码如下图所示。
@{
Layout = "_Layout";
}
通过在_ViewStart.cshtml文件中设置Layout属性,维护应用程序变得更加容易。如果想要使用不同的布局文件,则只需要在**_ViewStart.cshtml中的一个位置更改代码。但是好像还是没有解决400个页面中要修改200个布局页面的问题,这就要使用_ViewStart.cshtml**的分层功能。
_ViewStart.cshtml文件支持分层
我们通常把**_ViewStart.cshtml文件放在Views文件夹中。由于此文件支持分层,因此也可以将它放在Views**文件中的任何子文件夹中,如图所示:
在上面的文件夹结构中,我们在Views文件夹中放置了一个_ViewStart.cshtml文件,在Home子文件夹中放置了另一个_ViewStart.cshtml文件。
Home子文件夹的_ViewStart.cshtml文件中指定的布局页面,将覆盖Views文件夹下_ViewStart.cshtml文件中指定的布局页面。
这意味着,Views文件夹的所有视图都使用Views中_ViewStart.cshtml文件指定的布局页面,但Home文件夹中的视图将使用Home文件夹中_ViewStart.cshtml文件指定的布局页面。
请注意,如果要使用_ViewStart.cshtml中指定的布局文件,可以通过在单个视图中设置Layout属性来实现。
当有特殊需求的时候,比如某视图希望在不使用布局视图的情况下渲染视图,那么我们只需要在该视图中将Layout属性设置为null,代码如下。
@{Layout = null; ViewBag.Title="学生详情";}
<h3>@Model.PageTitle</h3>
逻辑判断调用布局视图
在ASP.NET Core MVC应用程序中,我们可以有多个布局视图。比如,应用程序中有以下两个布局视图。
_AdminLayout.cshtml
_NotAdminLayout.cshtml
在_ViewStart.cshtml中可以通过判断登录用户角色是否为Admin来选择对应的布局视图。
@
{
if (User.IsInRole("Admin"))
{
Layout="_AdminLayout.cshtml";
}
else
{
Layout="_NotAdminLayout.cshtml";
}
}
修改视图
以下代码是修改后的布局页代码。
Index.cshtml的代码如下:
@model IEnumerable<WebApplication12.Models.Student>
@{ ViewBag.Title = "学生列表";}
<html>
<head>
<title>学生详细信息</title>
</head>
<body>
<table align="center" border="1" cellpadding="0" cellspacing="0" width="500" height="250">
<thead >
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Major</th>
</tr>
</thead>
<tbody>
@foreach (var student in Model)
{
<tr>
<td >@student.ID</td>
<td>@student.Name</td>
<td>@student.Email</td>
<td>@student.Major</td>
</tr>
}
</tbody>
</table>
</body>
</html>
Details.cshtml中的代码如下:
@model WebApplication12.ViewModels.HomeDetailsViewModel
@{
ViewBag.Title = "Student Details";
}
<h3>@Model.PageTitle</h3>
<div>
姓名:@Model.Student.Name
</div>
<div>
邮箱:@Model.Student.Email
</div>
<div>
主修科目:@Model.Student.Major
</div>
@section Scripts{
<script src="/js/JavaScript.js"></script>
}
_ViewStart.cshtml中的代码如下:
@{
Layout = "_Layout";
}
项目文件夹结构如图所示:
ASP.NET Core MVC中的_ViewImports.cshtml文件
创建_ViewImports.cshtml,步骤如下:
- 右击Views文件夹,然后选择添加➡新建项。
- 在添加新项窗口中选择Razor视图导入,并点击添加按钮。
- 名为_ViewImports.cshtml的文件将添加到Views文件夹中。
_ViewImports.cshtml文件通常放在Views文件夹中。它用于包含公共命名空间,因此我们不必在每个视图中引用这些命名空间。
如果我们在_ViewImports.cshtml文件中包含以下两个命名空间,则这两个命名空间中的所有类型都可用于Home文件夹的每个视图,而无需再次引入完整的命名空间,代码如下:
@using WebApplication12.Models
@using WebApplication12.ViewModels
注意,@using
指令用于包含公共命名空间。除@using
指令外,_ViewImports.cshtml文件还支持以下指令:
- @addTagHelper。
- @removeTagHelper。
- @tagHelperPrefix。
- @model。
- @inherits。
- @inject。
_ViewStart.cshtml和**_ViewImports.cshtml**文件均支持分层,除了把它们放在Views文件夹中,我们还可以在Views文件夹的Home子文件夹中放置另一个_ViewImports.cshtml。
请注意,如果在视图中指定了命名空间,则该设置将覆盖_ViewImports.cshtml文件中的匹配设置。
总结
现在,学习了布局页得构成,Razor视图和Razor视图导入,以及不同情况下对节点的使用。