ASP.NET Core MVC相关技术

作者 Zhendong Ho 日期 2019-11-29
ASP.NET Core MVC相关技术

使用MVC相关技术

  • Controller
  • Tag Helper
  • Settings
  • View Component
  • Razor Page

建立Controller

添加实体类

项目右键,新建Models文件夹,在Models下添加Department类

public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public int EmployeeCount { get; set; }
}

在Models下添加Employee类

public class Employee
{
public int Id { get; set; }
public int DepartmentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Gender Gender { get; set; }
public bool Fired { get; set; }
}

public enum Gender
{
女 = 0,
男 = 1
}

在Models下添加CompanySummary类

public class CompanySummary
{
public int EmployeeCount { get; set; }
public int AverageDepartmentEmployeeCount { get; set; }
}

添加服务

在Serivces下,添加IDepartmentService接口

public interface IDepartmentService
{
Task<IEnumerable<Department>> GetAll();
Task<Department> GetById(int id);
Task<CompanySummary> GetCompanySummary();
Task Add(Department department);
}

在Services下,添加DepartmentService实现类

public class DepartmentService : IDepartmentService
{
private readonly List<Department> _departments = new List<Department>();

public DepartmentService()
{
_departments.Add(new Department
{
Id = 1,
Name = "HR",
EmployeeCount = 16,
Location = "Beijing"
});
_departments.Add(new Department
{
Id = 2,
Name = "G&D",
EmployeeCount = 52,
Location = "Shanghai"
});
_departments.Add(new Department
{
Id = 3,
Name = "Sales",
EmployeeCount = 200,
Location = "China"
});
}

public Task<IEnumerable<Department>> GetAll()
{
return Task.Run(function: () => _departments.AsEnumerable());
}

public Task<Department> GetById(int id)
{
return Task.Run(function: () => _departments.FirstOrDefault(x => x.Id == id));
}

public Task<CompanySummary> GetCompanySummary()
{
return Task.Run(function: () =>
{
return new CompanySummary
{
EmployeeCount = _departments.Sum(x => x.EmployeeCount),
AverageDepartmentEmployeeCount = (int)_departments.Average(x => x.EmployeeCount)
};
});
}

public Task Add(Department department)
{
department.Id = _departments.Max(x => x.Id) + 1;
_departments.Add(department);
return Task.CompletedTask;
}
}

在Services下,添加IEmployeeService接口

public interface IEmployeeService
{
Task Add(Employee employee);
Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId);
Task<Employee> Fire(int id);
}

在Services下,添加EmployeeService实现类

public class EmployeeService : IEmployeeService
{
private readonly List<Employee> _employees = new List<Employee>();

public EmployeeService()
{
_employees.Add(new Employee
{
Id = 1,
DepartmentId = 1,
FirstName = "Nick",
LastName = "Carter",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 2,
DepartmentId = 1,
FirstName = "Michael",
LastName = "Jackson",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 3,
DepartmentId = 1,
FirstName = "Mariah",
LastName = "Carey",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 4,
DepartmentId = 2,
FirstName = "Axl",
LastName = "Rose",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 5,
DepartmentId = 2,
FirstName = "Kate",
LastName = "Winslet",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 6,
DepartmentId = 3,
FirstName = "Rob",
LastName = "Thomas",
Gender = Gender.男
});
_employees.Add(new Employee
{
Id = 7,
DepartmentId = 3,
FirstName = "Avril",
LastName = "Lavigne",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 8,
DepartmentId = 3,
FirstName = "Katy",
LastName = "Perry",
Gender = Gender.女
});
_employees.Add(new Employee
{
Id = 9,
DepartmentId = 3,
FirstName = "Michelle",
LastName = "Monaghan",
Gender = Gender.女
});
}

public Task Add(Employee employee)
{
employee.Id = _employees.Max(x => x.Id) + 1;
_employees.Add(employee);
return Task.CompletedTask;
}

public Task<IEnumerable<Employee>> GetByDepartmentId(int departmentId)
{
return Task.Run(function: () => _employees.Where(x => x.DepartmentId == departmentId));
}

public Task<Employee> Fire(int id)
{
return Task.Run(function: () =>
{
var employee = _employees.FirstOrDefault(e => e.Id == id);
if (employee != null)
{
employee.Fired = true;
return employee;
}
return null;
});
}
}

注册服务

在Startup类中的ConfigureServices方法中,注册服务。

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

//services.AddSingleton<IClock, ChinaClock>();
services.AddSingleton<IDepartmentService, DepartmentService>();
services.AddSingleton<IEmployeeService, EmployeeService>();
}

添加Controller

在Controllers下添加DepartmentController

public class DepartmentController : Controller
{
private readonly IDepartmentService _departmentService;

public DepartmentController(IDepartmentService departmentService)
{
_departmentService = departmentService;
}

public async Task<IActionResult> Index()
{
ViewBag.Title = "Department Index";
var departments = await _departmentService.GetAll();
return View(departments);
}

public IActionResult Add()
{
ViewBag.Title = "Add Department";
return View(new Department());
}

[HttpPost]
public async Task<IActionResult> Add(Department model)
{
if (ModelState.IsValid)
{
await _departmentService.Add(model);
}

return RedirectToAction(nameof(Index)); // 使用nameof的好处是,避免写固定的字符串
}
}

注意

  1. 对于POST请求,必须在方法前写HttpPost特性不写默认为HttpGet
  2. 使用nameof的优点是,当符号重命名时,所有引用之处都会随之重新命名

在Controllers下添加EmployeeController

public class EmployeeController : Controller
{
private readonly IDepartmentService _departmentService;
private readonly IEmployeeService _employeeService;

public EmployeeController(IDepartmentService departmentService, IEmployeeService employeeService)
{
_departmentService = departmentService;
_employeeService = employeeService;
}

public async Task<IActionResult> Index(int departmentId)
{
var department = await _departmentService.GetById(departmentId);

ViewBag.Title = $"Employees of {department.Name}";
ViewBag.DepartmentId = departmentId;

var employees = await _employeeService.GetByDepartmentId(departmentId);

return View(employees);
}

public IActionResult Add(int departmentId)
{
ViewBag.Title = "Add Employee";
return View(new Employee
{
DepartmentId = departmentId
});
}

[HttpPost]
public async Task<IActionResult> Add(Employee model)
{
if (ModelState.IsValid)
{
await _employeeService.Add(model);
}

return RedirectToAction(nameof(Index), routeValues: new { departmentId = model.DepartmentId });
}

public async Task<IActionResult> Fire(int employeeId)
{
var employee = await _employeeService.Fire(employeeId);

return RedirectToAction(nameof(Index), routeValues: new { departmentId = employee.DepartmentId });
}
}

注意routeValues的作用是,RedirectToAction方法重定向到Index需要一个参数,通过匿名类型传递参数。

建立视图

项目右键,新建文件夹Views,在Views下新建文件夹Shared,用于存放所有页面的模板

Shared右键,新建Razor 布局,命名为默认的_layout.cshtml

image-20191202225528287

_layout.cshtml的代码如下,其中asp-append-version属性就是使用了Tag Helper

<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-2">
<img asp-append-version="true" alt="Logo" src="~/images/ae86.jpg" style="height: 60px;" />
</div>
<div class="col-md-10">
<span class="h2">@ViewBag.Title</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
@RenderBody()
</div>
</div>
</div>
</body>
</html>

然后,设置_layout作为其他所有页面的模板。

Views右键,新建Razor 视图开始,命名为默认的_ViewStart.cshtml

image-20191202230431405

_ViewStart.cshtml中设置了,所有的页面的母页面是_Layout

@{
Layout = "_Layout";
}

使用Tag Helper

Views右键,新建Razor 视图导入,命名为_ViewImports.cshtml

image-20191202231927733

输入以下Razor代码,可以为全局的View添加Tag Helper自定义的Tag Helper

@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"	// Tag Helper所在程序集的名称

使用Tag Helper区分当前环境

可以使用environment标签可以区分当前环境是开发环境还是其他环境,从而加载不同的静态资源。

<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>

<environment include="Development">
<link rel="stylesheet" asp-href-include="css/*" asp-href-exclude="css/all.min.css" />
</environment>

<environment exclude="Development">
<link rel="stylesheet" asp-href-include="all.min.css" />
</environment>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-2">
<img asp-append-version="true" alt="Logo" src="~/images/ae86.jpg" style="height: 60px;" />
</div>
<div class="col-md-10">
<span class="h2">@ViewBag.Title</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
@RenderBody()
</div>
</div>
</div>
</body>
</html>

注意:img标签中的asp-append-version属性的作用是,在图片地址后添加Hash值,这样可以防止图片缓存

建立View

Views右键,新建Department文件夹,在Department下新建Razor 视图,命名为Index.cshtml

这时候启动项目Three,可以看到页面的样式正常。并且复制图片地址后,也能看到Hash值

image-20191203213231754

由于开发环境,在Tag Helper的environment标签作用下,all.min.css也没有加载出来

image-20191203212234313

建立列表

在Views文件夹下的Department下的Index.cshtml

由于DepartmentController中的Index方法,返回给View的参数是IEnumerable<Department>,因此使用@model IEnumerable<Department>

@using Three.Models;
@model IEnumerable<Department>

<div class="row">
<div class="col-md-10 offset-md-2">
<table class="table">
<tr>
<th>Name</th>
<th>Location</th>
<th>EmployeeCount</th>
<th>操作</th>
</tr>
@Html.DisplayForModel()
</table>
</div>
</div>
<div class="row">
<div class="col-md-4 offset-md-2">
<a asp-action="Add">Add</a>
</div>
</div>

注意

  1. @Html.DisplayForModel(),这里使用了Html Helper,作用是展示数据
  2. asp-action指定了Action为Add,但是它所在的a标签不需要指定Controller,因为默认就是在DepartmentController。

建立展示模板

Department模板

Department文件夹下,新建DisplayTemplates文件夹,在DisplayTemplates下新建Razor 视图,命名为Department.cshtml。编辑如下代码。

@model Three.Models.Department

<tr>
<td>@Model.Name</td>
<td>@Model.Location</td>
<td>@Model.EmployeeCount</td>

<td>
<a asp-controller="Employee" asp-action="Index" asp-route-departmentId="@Model.Id">
Employees
</a>
</td>
</tr>

注意:该展示模板是一行数据。asp-controller的指定了Controller,asp-action指定了Action,asp-route-departmentId的作用是给指定的Action传递参数departmentId和Action的参数名保持一致

运行项目,可以发现列表正常显示了。

image-20191204212151893

Employee模板

同理,又在Views文件夹下,新建Employee文件夹,然后新建Index.cshtml,在Employee文件夹下新建DisplayTemplate文件夹,再新建Employee.cshtml

image-20191204214858354

编辑Views/Employee/Index.cshtml代码如下。

@using Three.Models
@model IEnumerable<Employee>

<div class="row">
<div class="col-md-10 offset-md-2">
<table class="table">
<tr>
<th>FirstName</th>
<th>LastName</th>
<th>Gender</th>
<th>Is Fired</th>
<th>操作</th>
</tr>
@Html.DisplayForModel()
</table>
</div>
</div>
<div class="row">
<div class="col-md-4 offset-md-2">
<a asp-action="Add">Add</a>
</div>
</div>

编辑Views/Employee/DisplayTemplates/Employee.cshtml代码如下。

@model Three.Models.Employee

<tr>
<td>@Model.FirstName</td>
<td>@Model.LastName</td>
<td>@Model.Gender</td>
<td>@(Model.Fired ? "是" : "")</td>

<td>
@if (!Model.Fired)
{
<a asp-action="Fire" asp-route-employeeId="@Model.Id">
Fire
</a>
}
</td>
</tr>

运行项目,可以进入Employee页面,并解雇Employee了。

image-20191204215329126

新建添加页面

部门添加页面

Views/Department下新建Add.cshtml,代码如下。

@using Three.Models
@model Department

<form asp-action="Add">
<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="Name"></label>
</div>
<div class="col-md-6">
<input class="form-control" asp-for="Name" />
</div>
</div>
<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="Location"></label>
</div>
<div class="col-md-6">
<input class="form-control" asp-for="Location" />
</div>
</div>
<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="EmployeeCount"></label>
</div>
<div class="col-md-6">
<input class="form-control" asp-for="EmployeeCount" />
</div>
</div>
<div class="row">
<div class="col-md-2 offset-md-2">
<button type="submit" class="btn btn-primary">Add</button>
</div>
</div>
</form>

注意asp-for的作用是指定属性,也可以在Model类中使用Display特性,来改变Label显示的内容

[Display(Name = "部门名称")]
public string Name { get; set; }

完成部门添加页面。

image-20191204222030900

员工添加页面

由于EmployeeController中的Add方法,需要一个参数departmentId。因此在Views/Employee/Index的Add按钮中,通过ViewBag添加参数。

image-20191204223208073

Views/Employee下新建Add.cshtml,代码如下。

@using Three.Models
@model Employee

<form asp-action="Add">
<input type="hidden" asp-for="DepartmentId" />

<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="FirstName"></label>
</div>
<div class="col-md-6">
<input class="form-control" asp-for="FirstName" />
</div>
</div>
<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="LastName"></label>
</div>
<div class="col-md-6">
<input class="form-control" asp-for="LastName" />
</div>
</div>
<div class="row form-group">
<div class="col-md-2 offset-md-2">
<label asp-for="Gender"></label>
</div>
<div class="col-md-6">
<select class="form-control"
asp-for="Gender"
asp-items="Html.GetEnumSelectList<Gender>()">
</select>
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Add</button>
</div>
</form>

注意:Gender下拉框使用asp-items,然后通过Html Helper获取枚举值显示在列表中。

完成员工添加页面。

image-20191204224913668

配置信息

ASP.NET Core的配置信息

  • Key-Value键值对。
  • 可以读取自,内存里、JSON、XML、INI等文件,或者系统环境变量。
  • 配置信息与配置系统是解耦的。
  • 可以依赖注入

ASP.NET Core的配置信息源

先后顺序从以下位置依次读取,如果存在重复的配置,则以最后读取的位置为准。

  1. appsettings.json
    • appsettings.{Environment}.json
  2. Secret Manager
  3. 环境变量
  4. 命令行参数

appsettings.json文件

创建ASP.NET Core项目之后,都会有一个appsettings.json文件。

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
  • Logging:日志的配置。
  • AllowedHosts:允许在哪个Host上运行应用。

注意:在开发环境中,appsettings.Development.json会覆盖appsettings.json中的配置。

使用配置

添加自定义配置

在appsettings.json中添加自定义配置BoldDepartmentEmployeeCountThreshold,可以使用对象与属性的形式。

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Three": {
"BoldDepartmentEmployeeCountThreshold": 30
},
"AllowedHosts": "*",
"me": 25
}

构造函数注入使用配置

Startup类中,使用构造函数注入,通过索引的方式读取配置

public class Startup
{
private readonly IConfiguration _configuration;

public Startup(IConfiguration configuration) // 构造函数注入
{
_configuration = configuration;
// 30,通过对象:属性读取
var three = _configuration["Three:BoldDepartmentEmployeeCountThreshold"];
var me = _configuration["me"]; // 25,直接读取
}
}

把配置映射到类中

由于C#是强类型,不推荐使用对象冒号属性的方式读取配置。因此把对象配置映射到类中

项目右键,新建ThreeOptions类,配置的属性和类的属性一一对应。

public class ThreeOptions
{
public int BoldDepartmentEmployeeCountThreshold { get; set; }
}

在Startup类中的ConfigureServices方法映射配置文件中Three的配置

public void ConfigureServices(IServiceCollection services)
{
// ...
services.Configure<ThreeOptions>(_configuration.GetSection(key: "Three"));
}

在Controller中使用配置

DepartmentController中,使用构造函数注入,然后就可以在整个Controller中使用配置。

private readonly IDepartmentService _departmentService;
private readonly IOptions<ThreeOptions> _threeOptions;

public DepartmentController(IDepartmentService departmentService, IOptions<ThreeOptions> threeOptions)
{
_departmentService = departmentService;
_threeOptions = threeOptions;
}

public async Task<IActionResult> Index()
{
int count = _threeOption.Value.BoldDepartmentEmployeeCountThreshold; // 30,读取配置
// ...
}

在View中使用配置

Department.cshtml中,修改如下代码。通过判断部门人数大于配置人数,则部门名称显示为粗体。

@using Microsoft.Extensions.Options
@using Three
@model Three.Models.Department
@inject IOptions<ThreeOptions> options

<tr>
@if (Model.EmployeeCount > options.Value.BoldDepartmentEmployeeCountThreshold)
{
<td><strong>@Model.Name</strong></td>
}
else
{
<td>@Model.Name</td>
}
<td>@Model.Location</td>
<td>@Model.EmployeeCount</td>

<td>
<a asp-controller="Employee" asp-action="Index" asp-route-departmentId="@Model.Id">
Employees
</a>
</td>
</tr>

使用其他配置文件

在Program类的CreateHostBuilder方法中,修改代码,增加ConfigureAppConfiguration。不使用原有的appsettings.json,使用自己添加的json文件nick.json

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, configBuilder) =>
{
configBuilder.Sources.Clear(); // 清除所有的源配置
configBuilder.AddJsonFile("nick.json"); // 添加自定义json配置
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

View Component

需求:需要做一个控件,在每个页面显示两个字段,公司的总人数和部门平均人数。可以使用View Component实现。

  • 为什么Partial View不行?因为没法添加业务逻辑
  • 如果在Controller里面写呢?那就无法到处复用
  • Child Action呢?开销太大,它也需要走完整个Controller的生命周期

View Component:就像Partial View,但是带有一个迷你的Controller,可以写业务逻辑。可以使用Razor语法来渲染VIew Conponent。

新建ViewComponent

ASP.NET Core MVC默认在项目根目录下的ViewComponents文件夹寻找Component。

项目右键,新建ViewConponents文件夹,在里面新建CompanySummaryViewConponent类

public class CompanySummaryViewComponent : ViewComponent
{
private readonly IDepartmentService _departmentService;

public CompanySummaryViewComponent(IDepartmentService departmentService)
{
_departmentService = departmentService;
}
public async Task<IViewComponentResult> InvokeAsync() // 方法名必须写InvokeAsync
{
var summary = await _departmentService.GetCompanySummary();
return View(summary);
}
}

新建View

Views/Shared下新建Components文件夹,在里面新建CompanySummary文件夹,在里面新建Default.cshtml

由于在CompanySummaryViewComponent中没有指定返回的view的名称,所以默认会找到Views/Shared/Components/CompanySummary下的Default.cshtml

@model Three.Models.CompanySummary

<div class="small">
<div class="row h5">Company Summary</div>
<div class="row">
<div class="col-md-8 h6">员工总数:</div>
<div class="col-md-4 h6">@Model.EmployeeCount</div>
</div>
<div class="row">
<div class="col-md-8 h6">部门平均:</div>
<div class="col-md-4 h6">@Model.AverageDepartmentEmployeeCount</div>
</div>
</div>

使用ViewComponent

编辑Views/Department/Index.cshtml,在Add按钮上面添加代码,通过await Component.InvokeAsync调用,参数name为对应ViewComponent的名称

@using Three.Models;
@model IEnumerable<Department>

<div class="row">
<div class="col-md-10 offset-md-2">
<table class="table">
<tr>
<th>Name</th>
<th>Location</th>
<th>EmployeeCount</th>
<th>操作</th>
</tr>
@Html.DisplayForModel()
</table>
</div>
</div>
<div class="row">
<div class="col-md-2">
@await Component.InvokeAsync(name: "CompanySummary")
</div>

<div class="col-md-4">
<a asp-action="Add">Add</a>
</div>
</div>

添加参数

添加动态标题,编辑CompanySummaryViewComponent.cs,修改InvokeAsync方法。

public async Task<IViewComponentResult> InvokeAsync(string title)
{
ViewBag.Title = title;
var summary = await _departmentService.GetCompanySummary();
return View(summary);
}

编辑Default.cshtml,替换标题。

@model Three.Models.CompanySummary

<div class="small">
<div class="row h5">@ViewBag.Title</div>
<div class="row">
<div class="col-md-8 h6">员工总数:</div>
<div class="col-md-4 h6">@Model.EmployeeCount</div>
</div>
<div class="row">
<div class="col-md-8 h6">部门平均:</div>
<div class="col-md-4 h6">@Model.AverageDepartmentEmployeeCount</div>
</div>
</div>

编辑Views/Department/Index.cshtml,在调用的地方修改代码。

@await Component.InvokeAsync(name: "CompanySummary", arguments: new { title = "部门列表页的汇总" })

使用Tag Helper方式调用

使用标签vc:ViewComponent的名称(用-分隔),参数通过属性传递。

<vc:company-summary title="部门列表页的汇总2"></vc:company-summary>

修改_ViewImports.cshtml,把当前程序集Three注册到Tag Helper中。

@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Three"

运行项目,效果如下。

image-20191211213340719