();
}
```
### 在模型中使用依赖注入
```c#
public class StudentController : Controller
{
private readonly IStudentRepository _studentRepository;
// 通过构造函数注入的方式注入 IStudentRepository
public StudentController(IStudentRepository studentRepository)
{
_studentRepository = studentRepository;
}
public JsonResult Index(int id)
{
return Json(_studentRepository.GetById(id));
}
}
```
## 控制器入门
### 内容格式协商
在控制器方法中使用 `ObjectResult` 返回类型,支持内容协商,根据请求头参数返回数据,
```c#
// 支持内容格式协商
public ObjectResult Details(int id)
{
return new ObjectResult(_studentRepository.GetById(id));
}
```
如:
```
Accept: application/xml
```
将返回xml格式。注:还要添加xml序列化器。
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
// 注册XML序列化器
.AddXmlSerializerFormatters();
}
```
## 视图入门
### 将数据从控制器传递到视图的方法
前两种都是弱类型的
- ViewData
- ViewBag
- 强类型视图
### ViewData
- 弱类型字典对象
- 使用string类型的键值,存储和chaxun
- 运行时动态解析
- 没有智能感知,编译时也没有类型检查
使用方法:
```c#
ViewData["Title"] = "学生视图";
ViewData["Model"] = model;
```
cshtml代码:
```scss
@ViewData["Title"]
@{
var student = ViewData["model"] as StudyManagement.Models.Student;
}
姓名:@student.Name
班级:@student.ClassName
```
### ViewBag
```c#
// 直接给动态属性赋值
ViewBag.PageTitle = "ViewBag标题";
ViewBag.Student = model;
```
cshtml使用:
```html
@ViewBag.PageTitle
姓名:@ViewBag.Student.Name
班级:@ViewBag.Student.ClassName
```
### 强类型视图
在控制器中传给View()模型
```c#
public IActionResult GetView()
{
var model = _studentRepository.GetById(1);
return View(model);
}
```
在cshtml中指定模型类型
```scss
@model StudyManagement.Models.Student
强类型模型
- @Model.Id
- @Model.Name
- @Model.ClassName
- @Model.Email
```
## ViewModel 视图模型
类似于DTO(数据传输对象)
### 定义ViewModel
```c#
public class StudentDetailsViewModel
{
public Student Student { get; set; }
public string PageTitle { get; set; }
}
```
### 修改控制器
```c#
public IActionResult Details()
{
var model = _studentRepository.GetById(1);
var viewModel = new StudentDetailsViewModel
{
Student = model,
PageTitle = "viewmodel里的页面标题"
};
return View(viewModel);
}
```
### 在View中使用
```scss
@model StudyManagement.ViewModels.StudentDetailsViewModel
强类型模型
@Model.PageTitle
- @Model.Student.Id
- @Model.Student.Name
- @Model.Student.ClassName
- @Model.Student.Email
```
### View中使用循环
```scss
@model IEnumerable
| Id |
姓名 |
班级 |
邮箱 |
@foreach (var student in Model)
{
| @student.Id |
@student.Name |
@student.ClassName |
@student.Email |
}
```
## 布局视图 LayoutView
### 创建布局视图
```scss
@ViewBag.Title
@RenderBody()
@RenderSection("Scripts", required: false)
```
### 渲染视图
```scss
@model IEnumerable
@{
Layout = "~/Views/Shared/_Layout.cshtml";
ViewBag.Title = "首页 学生列表";
}
```
### 视图节点 Section
在布局视图里渲染节点
```scss
@RenderSection("Scripts", required: false)
```
在普通视图里定义节点
```scss
@section Scripts{
}
```
### 视图开始 ViewStart
我的理解就是`_ViewStart.cshtml`文件所在目录下的每个视图文件开始渲染先执行这个文件的内容。一般直接放在`Views`目录下,全局生效,可以放在各个子文件夹下,这样可以覆盖全局的`_ViewStart.cshtml`。
```scss
@{
Layout = "_Layout";
}
```
### 视图导入 ViewImports
用来导入命名空间、注册模型等等n多种操作。
生效机制和ViewStart差不多。
## 路由
- 常规路由(传统路由)
- 属性路由
### 常规路由
在`MapRoute`方法中传入就好了。
```c#
// 自定义路由
app.UseMvc(route =>route.MapRoute("default",
"{controller=Home}/{action=Index}/{id?}"));
```
### 属性路由
比传统路由更加灵活,可以搭配传统路由使用。
即在控制器方法上添加路由注解,一个方法可以同时映射多个路由。
```c#
[Route("Home/Index")]
public IActionResult Index()
{
return View(_studentRepository.GetAll());
}
```
路由中也可以指定参数
```c#
[Route("test/{id?}")]
public IActionResult Details(int id = 1)
{
var model = _studentRepository.GetById(id);
var viewModel = new StudentDetailsViewModel
{
Student = model,
PageTitle = "viewmodel里的页面标题"
};
return View(viewModel);
}
```
可以直接在控制器类上加注解,`[controller]/[action]`。
## TagHelper
### 入门
优点:根据参数自动生成,不需要手写超链接,类似Django模板里面的url命令。
在ViewImport中添加TagHelper
```scss
@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers
```
比如,链接TagHelper使用
```html
查看
```
缓存破坏的TagHelper
```html
```
### 环境 TagHelper
在开发环境中使用本地css文件,在非开发环境下使用的是CDN的css文件。
注:`integrity`是用来做完整性检查的,保证CDN提供文件的完整和安全。
```html
```
为了防止CDN加载失败页面无法显示,可以加上fallback相关属性,第一个是失败时加载的文件,第二个是不检查这个文件的完整性
```scss
asp-fallback-href="~/lib/twitter-bootstrap/css/bootstrap.css"
asp-suppress-fallback-integrity="true"
```
### 表单 Tag Helper
直接贴上一个布局的代码,把class样式都去掉了,保留最基本代码。
确实是很方便的,和Django、jinja2之类的模板比完全不输。
```scss
@model Student
```
## 模型绑定
将Http请求中的数据绑定到控制器方法上对应参数的顺序:
- Form Values (Post表单数据)
- Route Values (路由中的值)
- Query String (Get的查询字符串)
### 模型验证
#### 1.设置模型
首先在Model中加入验证属性,如:
```c#
public class Student
{
public int Id { get; set; }
[Display(Name = "姓名"), MaxLength(4, ErrorMessage = "名字长度不能超过四个字")]
[Required(ErrorMessage = "请输入名字!")]
public string Name { get; set; }
[Display(Name = "班级")]
public ClassNameEnum ClassName { get; set; }
[Required(ErrorMessage = "请输入邮箱!")]
[Display(Name = "邮箱")]
[RegularExpression(@"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; }
}
```
##### 常用的模型验证方法
- Required
- Range:指定允许的最小值和最大值
- MinLength
- MaxLength
- Compare:比较两个属性,比如密码和确认密码
- RegularExpression:正则匹配
#### 2.在控制器中加入验证代码
使用`ModelState.IsValid`来验证模型属性是否正确
```c#
[HttpPost]
public IActionResult Create(Student student)
{
if (ModelState.IsValid)
{
var newStudent = _studentRepository.Add(student);
return RedirectToAction("details", new { id = newStudent.Id });
}
return View();
}
```
#### 3.使用TagHelper在网页上显示错误信息
例子如下:
```scss
```
## EF Core入门
### 首先实现DbContext
```c#
public class AppDbContext:DbContext
{
// 将应用程序的配置传递给DbContext
public AppDbContext(DbContextOptions options) : base(options) { }
// 对要使用到的每个实体都添加 DbSet 属性
// 通过DbSet属性来进行增删改查操作
// 对DbSet采用Linq查询的时候,EFCore自动将其转换为SQL语句
public DbSet Students { get; set; }
}
```
### 注册DbContext连接池
```c#
services.AddDbContextPool(options => options.UseSqlServer(_configuration.GetConnectionString("StudentDBConnection")));
```
其中,本地SqlServer数据库的配置,在`appserttings.json`中:
```json
"ConnectionStrings": {
"StudentDBConnection": "server=(localdb)\\MSSQLLocalDB;database=StudentDB;Trusted_Connection=true"
}
```
### 实现仓储
```c#
public class SqlStudentRepository : IStudentRepository
{
private readonly AppDbContext _context;
public SqlStudentRepository(AppDbContext context)
{
_context = context;
}
public Student Add(Student student)
{
_context.Students.Add(student);
_context.SaveChanges();
return student;
}
public Student Delete(int id)
{
var student = _context.Students.Find(id);
if (student != null)
{
_context.Students.Remove(student);
_context.SaveChanges();
}
return student;
}
public IEnumerable GetAll() => _context.Students;
public Student GetById(int id) => _context.Students.Find(id);
public Student Update(Student updatedStudent)
{
var student = _context.Students.Attach(updatedStudent);
student.State = EntityState.Modified;
_context.SaveChanges();
return updatedStudent;
}
}
```
### EF Core 常用命令
在nuget控制台中,不区分大小写
- Get-Help about_enti:显示帮助,`about_enti`全名很长可以只写前面的
- Add-Migration:添加迁移记录
- Update-Database:更新数据库
使用dotnet cli的话请先安装:dotnet tool install --global dotnet-ef
- dotnet ef migrations add initial: 添加迁移记录
- dotnet ef database update: 更新数据库
### 添加种子数据
重写`DbContext`的`OnModelCreating`方法
```c#
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity().HasData(
new Student { Id = 1, Name = "小米" },
);
}
```
为了避免`DbContext`代码太乱,也可以使用扩展方法的方式:
```c#
public static class ModelBuilderExtensions
{
public static void InsertSeedData(this ModelBuilder mBuilder)
{
mBuilder.Entity().HasData(
new Student { Id = 1, Name = "小米" },
);
}
}
```
### 领域模型与数据库架构
- 使用迁移功能同步领域模型和数据库架构
- 使用 `add-migration` 添加迁移记录
- 使用 `remove-migration` 删除最近一条记录
- 使用 `update-database 迁移记录名称` 可以回滚至任意一次迁移
## 文件上传
### 定义ViewModel
要上传的字段采用 `IFormFile` 类型
```c#
public class StudentCreateViewModel
{
public int Id { get; set; }
// 省略无关代码...
[Display(Name = "图片")]
public IFormFile Photo { get; set; }
}
```
### 编写视图
修改`cshtml`视图文件,修改模型绑定:
```scss
@model StudentCreateViewModel
```
加入上传文件的表单项
```scss
```
为了选择文件后能显示出文件名还要编写js:
```js
$(document).ready(function () {
$('.custom-file-input').on('change', function () {
var fileName = $(this).val().split('\\').pop();
$(this).next('.custom-file-label').html(fileName);
});
});
```
### 编写控制器
通过构造函数注入 `HostingEnvironment`
```csharp
public StudentController(IStudentRepository studentRepository, HostingEnvironment hostingEnvironment)
{
_studentRepository = studentRepository;
_hostingEnvironment = hostingEnvironment;
}
```
处理文件上传和保存的逻辑
```c#
[HttpPost]
public IActionResult Create(StudentCreateViewModel model)
{
if (ModelState.IsValid)
{
var uniqueFileName = "";
if (model.Photo != null)
{
var uploadDir = Path.Combine(
_hostingEnvironment.WebRootPath,
"uploads", "images");
uniqueFileName = Guid.NewGuid().ToString() +
"_" + model.Photo.FileName;
model.Photo.CopyTo(new FileStream(
Path.Combine(uploadDir, uniqueFileName),
FileMode.Create));
}
var student = new Student
{
Name = model.Name,
Email = model.Email,
ClassName = model.ClassName,
PhotoPath = uniqueFileName,
};
var newStudent = _studentRepository.Add(student);
return RedirectToAction("details",
new { id = newStudent.Id });
}
return View();
}
```
## 多文件上传
和单文件差不多
### ViewModel
增加 `List` 类型字段
```c#
[Display(Name = "图库")]
public List Gallery { get; set; }
```
### 修改视图
```scss
```
js代码:
```js
$('#gallery-input').on('change', function () {
var label = $(this).next('#gallery-label');
var files = $(this)[0].files;
if (files.length > 1) {
label.html(`你已经选择了${files.length}个文件`);
} else if (files.length == 1) {
label.html(files[0].name);
}
});
```
### 修改控制器代码
```c#
// 处理多文件上传
if (model.Gallery != null && model.Gallery.Count > 0)
{
foreach(var photo in model.Gallery)
{
uniqueFileName = Guid.NewGuid().ToString() + "_"
+ photo.FileName;
photo.CopyTo(new FileStream(Path.Combine(uploadDir,
uniqueFileName),FileMode.Create));
}
}
```
## 错误处理
### 添加错误处理页面
在 `Startup.cs` 中设置中间件:
```c#
app.UseStatusCodePagesWithReExecute("/error/{0}");
```
推荐用 `UseStatusCodePagesWithReExecute` 而不是 `UseStatusCodePagesWithRedirects`,前者在管道内执行执行错误跳转url,后者会重定向到该url,导致http错误状态码变成新页面的正常执行的200码了。
接着编写错误控制器:
```c#
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult Index(int statusCode)
{
var statusCodeResult = HttpContext.Features.
Get();
var viewModel = new ErrorViewModel
{
Path = statusCodeResult.OriginalPath,
QueryString = statusCodeResult.
OriginalQueryString,
};
switch (statusCode)
{
case 404:
viewModel.Message = "页面未找到";
break;
}
return View("Error", viewModel);
}
}
```
对了,我还定义了ViewModel:
```c#
public class ErrorViewModel
{
public int Code { get; set; }
public string Message { get; set; }
public string Path { get; set; }
public string QueryString { get; set; }
}
```
视图代码就不贴了,无非就是显示ViewModel里的这些错误信息~
### 设置全局异常跳转
添加中间件
```c#
app.UseExceptionHandler("/exception");
```
编写处理用的控制器,这里需要添加`AllowAnonymous`注解,允许用户在未登录的时候访问到这个异常页面,保证无论如何可以显示出异常页面。
```c#
[AllowAnonymous]
[Route("exception")]
public IActionResult ExceptionHandler()
{
var exception = HttpContext.Features.
Get();
var viewModel = new ExceptionViewModel
{
Path = exception.Path,
Message = exception.Error.Message,
StackTrace = exception.Error.StackTrace,
};
return View("Exception", viewModel);
}
```
另外,ViewModel定义如下:
```c#
public class ExceptionViewModel
{
public string Path { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
}
```
## 日志记录
AspNetCore里面自带了一套日志系统,默认已经注册到了服务容器里了,只要在控制器的构造函数里注入就可以使用了,比如:
```c#
public class ErrorController : Controller
{
private ILogger _logger;
public ErrorController(ILogger logger)
{
this._logger = logger;
}
}
```
默认的日志只会记录到控制台或者调试输出,不过我们为了实现更多功能,比如记录到文件或者推送到日志服务器,我们需要使用第三方的日志组件。这里我用的是NLog。
首先要安装`NLog.Web.AspNetCore`这个包。
之后在`Program.cs`里引入nlog服务:
```c#
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging((hostingContext, logging) =>
{
// 保留官方的代码中的默认日志程序
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
// 引入 nlog
logging.AddNLog();
}).UseStartup();
```
保留官方默认日志程序那里,要看AspNetCore的源代码,本文用的是2.2版本,在github看,地址如下:
https://github.com/dotnet/aspnetcore/blob/v2.2.8/src/DefaultBuilder/src/WebHost.cs
然后,为了使用nlog,需要创建一个配置文件,在项目根目录创建 `NLog.config`:
关于配置文件的说明可以参考:https://github.com/NLog/NLog/wiki
```xml
```
之后在程序中就可以正常使用日志功能了。比如:
```c#
var viewModel = new StatusCodeViewModel
{
Code = statusCode,
Path = statusCodeResult.OriginalPath,
QueryString = statusCodeResult.OriginalQueryString,
};
_logger.LogWarning(viewModel.ToString());
```
还有可以在`appsettings.json`里面配置日志等级和命名空间过滤,跟在`NLog.conf`里面配置效果是一样的。例如:
```json
"Logging": {
"LogLevel": {
"Default": "Warning",
"StudyManagement.Controllers.ErrorController":
"Warning"
}
},
```