From e5bdfcb8785c2a929550498dd477a6af98d4a338 Mon Sep 17 00:00:00 2001 From: shiyangjiea <1317284428@qq.com> Date: Sun, 18 Jan 2026 23:23:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=AC=94=E8=AE=B0260118?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...13\347\232\204\344\275\234\347\224\250.md" | 59 ++ ...72\346\250\241\345\236\213\347\261\273.md" | 105 ++++ ...06\345\233\276\346\250\241\345\236\213.md" | 189 +++++++ ...41\345\236\213\347\273\221\345\256\232.md" | 516 ++++++++++++++++++ 4 files changed, 869 insertions(+) create mode 100644 "\347\216\213\351\221\253\346\272\220/20260112-\346\250\241\345\236\213\347\232\204\344\275\234\347\224\250.md" create mode 100644 "\347\216\213\351\221\253\346\272\220/20260114-\345\210\233\345\273\272\346\250\241\345\236\213\347\261\273.md" create mode 100644 "\347\216\213\351\221\253\346\272\220/20260115-\350\247\206\345\233\276\346\250\241\345\236\213.md" create mode 100644 "\347\216\213\351\221\253\346\272\220/20260116-\346\250\241\345\236\213\347\273\221\345\256\232.md" diff --git "a/\347\216\213\351\221\253\346\272\220/20260112-\346\250\241\345\236\213\347\232\204\344\275\234\347\224\250.md" "b/\347\216\213\351\221\253\346\272\220/20260112-\346\250\241\345\236\213\347\232\204\344\275\234\347\224\250.md" new file mode 100644 index 0000000..52a6b61 --- /dev/null +++ "b/\347\216\213\351\221\253\346\272\220/20260112-\346\250\241\345\236\213\347\232\204\344\275\234\347\224\250.md" @@ -0,0 +1,59 @@ +# **第四章:模型(Model)- 定义你的数据蓝图** + +## **章节目标** + +- 理解模型在MVC架构中的作用和重要性 +- 掌握如何创建和设计模型类 +- 学会使用数据注解(Data Annotations)为模型添加验证规则 +- 了解模型与数据库的映射关系 + +## **核心任务** + +创建完整的学生模型(Student),包括属性定义、数据验证规则,并理解模型如何与控制器和视图协同工作。 + +------ + +## **1. 任务引入:我们要做什么?** + +在前两章中,我们创建了控制器和视图,但学生数据是临时存储在控制器中的。现在,我们需要正式定义学生模型,为接下来的数据库操作做准备。模型是数据的蓝图,它定义了数据的结构和规则。 + +**任务分解**: + +1. 创建完整的Student模型类 +2. 为模型属性添加数据验证规则 +3. 创建用于视图的表单模型(ViewModel) +4. 理解模型绑定(Model Binding)的过程 + +------ + +## **2. 知识点学习** + +### **知识点1:模型(Model)的作用** + +#### **1.1 什么是模型?** + +模型是代表应用程序数据的类。它们包含: + +- 数据属性(字段) +- 验证规则 +- 业务逻辑方法 +- 数据访问逻辑(可选,通常分离) + +#### **1.2 模型在MVC中的位置** + +``` +用户请求 → 控制器 → 模型(获取/处理数据) → 视图(显示数据) +``` + +**模型的主要职责**: + +1. **数据表示**:定义数据的结构和类型 +2. **数据验证**:确保数据的有效性和完整性 +3. **业务逻辑**:封装与数据相关的操作 +4. **数据持久化**:与数据库交互(通常通过DbContext) + +#### **1.3 模型的类型** + +- **领域模型(Domain Model)**:代表业务实体,如Student、Course +- **视图模型(View Model)**:为特定视图定制的模型,包含显示和交互所需的数据 +- **输入模型(Input Model)**:用于接收用户输入,通常与视图模型类似 \ No newline at end of file diff --git "a/\347\216\213\351\221\253\346\272\220/20260114-\345\210\233\345\273\272\346\250\241\345\236\213\347\261\273.md" "b/\347\216\213\351\221\253\346\272\220/20260114-\345\210\233\345\273\272\346\250\241\345\236\213\347\261\273.md" new file mode 100644 index 0000000..1e72c5b --- /dev/null +++ "b/\347\216\213\351\221\253\346\272\220/20260114-\345\210\233\345\273\272\346\250\241\345\236\213\347\261\273.md" @@ -0,0 +1,105 @@ +### **知识点2:创建模型类** + +#### **2.1 基本模型类** + +模型类通常放在`Models`文件夹中,是普通的C#类。 + +**示例:Student.cs** + +``` +namespace StudentManagementSystem.Models +{ + public class Student + { + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public string Email { get; set; } + public DateTime EnrollmentDate { get; set; } + public string Major { get; set; } + } +} +``` + +#### **2.2 模型属性类型** + +- 简单类型:int、string、DateTime、decimal等 +- 复杂类型:自定义类、集合等 +- 可空类型:int?、string?(C# 8.0可空引用类型) + +#### **2.3 模型设计原则** + +1. **单一职责**:每个模型只代表一个实体 +2. **清晰命名**:属性名明确表示其含义 +3. **适当粒度**:包含必要属性,避免过度设计 +4. **考虑扩展**:预留未来可能需要的属性 + +### **知识点3:数据注解(Data Annotations)** + +#### **3.1 什么是数据注解?** + +数据注解是添加到模型属性上的特性(Attribute),用于: + +- 定义验证规则 +- 控制数据展示方式 +- 配置数据库映射 + +#### **3.2 常用数据注解** + +**验证注解**: + +``` +[Required] // 必填字段 +[StringLength(50)] // 字符串长度限制 +[Range(15, 40)] // 数值范围 +[EmailAddress] // 邮箱格式验证 +[RegularExpression(@"^1[3-9]\d{9}$")] // 正则表达式验证 +[Compare("ConfirmPassword")] // 比较两个属性值 +[Phone] // 电话格式验证 +[Url] // URL格式验证 +``` + +**显示注解**: + +``` +[Display(Name = "学生姓名")] // 显示名称 +[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}")] // 格式化显示 +[DataType(DataType.Date)] // 数据类型提示 +[ScaffoldColumn(false)] // 是否在脚手架中显示 +``` + +**数据库注解**: + +``` +[Key] // 主键 +[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 自增 +[Table("Students")] // 指定表名 +[Column("StudentName")] // 指定列名 +``` + +#### **3.3 自定义验证特性** + +可以创建自定义验证特性以满足特殊需求。 + +``` +public class ValidEnrollmentDateAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value is DateTime enrollmentDate) + { + if (enrollmentDate > DateTime.Now) + { + return new ValidationResult("入学时间不能晚于当前日期"); + } + + if (enrollmentDate.Year < 2000) + { + return new ValidationResult("入学时间不能早于2000年"); + } + } + + return ValidationResult.Success; + } +} +``` \ No newline at end of file diff --git "a/\347\216\213\351\221\253\346\272\220/20260115-\350\247\206\345\233\276\346\250\241\345\236\213.md" "b/\347\216\213\351\221\253\346\272\220/20260115-\350\247\206\345\233\276\346\250\241\345\236\213.md" new file mode 100644 index 0000000..c5d781a --- /dev/null +++ "b/\347\216\213\351\221\253\346\272\220/20260115-\350\247\206\345\233\276\346\250\241\345\236\213.md" @@ -0,0 +1,189 @@ +### **知识点4:视图模型(View Model)** + +#### **4.1 为什么需要视图模型?** + +- 领域模型可能不适合直接用于视图 +- 视图可能需要组合多个领域模型的数据 +- 避免过度暴露领域模型细节 +- 为视图定制专门的数据结构 + +#### **4.2 创建视图模型** + +``` +namespace StudentManagementSystem.ViewModels +{ + public class StudentCreateViewModel + { + [Required(ErrorMessage = "姓名不能为空")] + [Display(Name = "姓名")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在2-50个字符之间")] + public string Name { get; set; } + + [Required(ErrorMessage = "年龄不能为空")] + [Display(Name = "年龄")] + [Range(15, 40, ErrorMessage = "年龄必须在15-40岁之间")] + public int Age { get; set; } + + [Required(ErrorMessage = "邮箱不能为空")] + [Display(Name = "邮箱")] + [EmailAddress(ErrorMessage = "邮箱格式不正确")] + public string Email { get; set; } + + [Required(ErrorMessage = "请选择专业")] + [Display(Name = "专业")] + public string Major { get; set; } + + [Display(Name = "入学时间")] + [DataType(DataType.Date)] + [ValidEnrollmentDate] + public DateTime EnrollmentDate { get; set; } + + // 额外的视图特定属性 + [Display(Name = "确认邮箱")] + [Compare("Email", ErrorMessage = "邮箱与确认邮箱不一致")] + public string ConfirmEmail { get; set; } + + [Display(Name = "我已阅读并同意条款")] + [Range(typeof(bool), "true", "true", ErrorMessage = "必须同意条款")] + public bool AgreeToTerms { get; set; } + } +} +``` + +### **知识点5:模型绑定(Model Binding)** + +#### **5.1 什么是模型绑定?** + +模型绑定是ASP.NET Core将HTTP请求数据自动映射到控制器动作参数的过程。 + +**绑定来源**: + +- 路由数据(Route data) +- 查询字符串(Query string) +- 表单数据(Form data) +- JSON/XML请求体 + +#### **5.2 绑定特性** + +``` +// 指定绑定来源 +[FromQuery] // 从查询字符串绑定 +[FromRoute] // 从路由数据绑定 +[FromForm] // 从表单数据绑定 +[FromBody] // 从请求体绑定(JSON/XML) +[FromHeader] // 从请求头绑定 +[FromServices] // 从依赖注入容器绑定服务 +``` + +**示例**: + +``` +public IActionResult Edit([FromRoute] int id, [FromBody] Student student) +{ + // id来自路由,student来自请求体(JSON) +} +``` + +#### **5.3 绑定前缀** + +当参数名与模型属性名不匹配时使用: + +``` +public IActionResult Create([Bind(Prefix = "stu")] Student student) +{ + // 绑定名为stu.Name、stu.Age的表单字段 +} +``` + +#### **5.4 模型状态(ModelState)** + +模型绑定后,系统会验证模型并填充ModelState。 + +``` +[HttpPost] +public IActionResult Create(Student student) +{ + if (!ModelState.IsValid) + { + // 验证失败,重新显示表单 + return View(student); + } + + // 验证成功,处理数据 + return RedirectToAction("Index"); +} +``` + +------ + +## **3. 任务实施:完善学生模型** + +### **步骤1:创建完整的学生模型** + +**更新 `Models/Student.cs`**: + +``` +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace StudentManagementSystem.Models +{ + public class Student + { + public int Id { get; set; } + + [Required(ErrorMessage = "姓名不能为空")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在2-50个字符之间")] + [Display(Name = "姓名")] + public string? Name { get; set; } + + [Required(ErrorMessage = "年龄不能为空")] + [Range(15, 40, ErrorMessage = "年龄必须在15-40岁之间")] + [Display(Name = "年龄")] + public int Age { get; set; } + + [Required(ErrorMessage = "邮箱不能为空")] + [EmailAddress(ErrorMessage = "邮箱格式不正确")] + [Display(Name = "邮箱")] + public string? Email { get; set; } + + [Required(ErrorMessage = "请选择专业")] + [Display(Name = "专业")] + public string? Major { get; set; } + + [Required(ErrorMessage = "请选择入学时间")] + [DataType(DataType.Date)] + [Display(Name = "入学时间")] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } + + [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号码格式不正确")] + [Display(Name = "手机号码")] + public string? Phone { get; set; } + + [StringLength(200, ErrorMessage = "地址长度不能超过200个字符")] + [Display(Name = "地址")] + public string? Address { get; set; } + + [Display(Name = "是否在读")] + public bool IsActive { get; set; } = true; + + [Display(Name = "创建时间")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public DateTime CreatedAt { get; set; } = DateTime.Now; + + [Display(Name = "更新时间")] + [DatabaseGenerated(DatabaseGeneratedOption.Computed)] + public DateTime UpdatedAt { get; set; } = DateTime.Now; + + // 计算属性(不映射到数据库) + [NotMapped] + [Display(Name = "姓名和学号")] + public string? NameAndId => $"{Name} (ID: {Id})"; + + [NotMapped] + [Display(Name = "在校状态")] + public string? Status => IsActive ? "在读" : "已毕业"; + } +} +``` \ No newline at end of file diff --git "a/\347\216\213\351\221\253\346\272\220/20260116-\346\250\241\345\236\213\347\273\221\345\256\232.md" "b/\347\216\213\351\221\253\346\272\220/20260116-\346\250\241\345\236\213\347\273\221\345\256\232.md" new file mode 100644 index 0000000..b06b410 --- /dev/null +++ "b/\347\216\213\351\221\253\346\272\220/20260116-\346\250\241\345\236\213\347\273\221\345\256\232.md" @@ -0,0 +1,516 @@ +### **步骤2:创建自定义验证特性** + +**创建 `Validations` 文件夹和 `ValidEnrollmentDateAttribute.cs`**: + +``` +using System; +using System.ComponentModel.DataAnnotations; + +namespace StudentManagementSystem.Validations +{ + public class ValidEnrollmentDateAttribute : ValidationAttribute + { + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value is DateTime enrollmentDate) + { + if (enrollmentDate > DateTime.Now) + { + return new ValidationResult("入学时间不能晚于当前日期"); + } + + if (enrollmentDate.Year < 2000) + { + return new ValidationResult("入学时间不能早于2000年"); + } + + // 确保入学时间不是周末(示例) + if (enrollmentDate.DayOfWeek == DayOfWeek.Saturday || + enrollmentDate.DayOfWeek == DayOfWeek.Sunday) + { + return new ValidationResult("入学时间不能是周末"); + } + } + + return ValidationResult.Success; + } + } +} +``` + +**更新Student模型,使用自定义验证**: + +``` +using StudentManagementSystem.Validations; + +// 在EnrollmentDate属性上添加 +[ValidEnrollmentDate] +public DateTime EnrollmentDate { get; set; } +``` + +### **步骤3:创建视图模型** + +**创建 `ViewModels` 文件夹和 `StudentCreateViewModel.cs`**: + +``` +using System.ComponentModel.DataAnnotations; + +namespace StudentManagementSystem.ViewModels +{ + public class StudentCreateViewModel + { + [Required(ErrorMessage = "姓名不能为空")] + [Display(Name = "姓名")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在2-50个字符之间")] + public string? Name { get; set; } + + [Required(ErrorMessage = "年龄不能为空")] + [Display(Name = "年龄")] + [Range(15, 40, ErrorMessage = "年龄必须在15-40岁之间")] + public int Age { get; set; } + + [Required(ErrorMessage = "邮箱不能为空")] + [Display(Name = "邮箱")] + [EmailAddress(ErrorMessage = "邮箱格式不正确")] + public string? Email { get; set; } + + [Display(Name = "确认邮箱")] + [Compare("Email", ErrorMessage = "邮箱与确认邮箱不一致")] + public string? ConfirmEmail { get; set; } + + [Required(ErrorMessage = "请选择专业")] + [Display(Name = "专业")] + public string? Major { get; set; } + + [Required(ErrorMessage = "请选择入学时间")] + [Display(Name = "入学时间")] + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } = DateTime.Today; + + [Display(Name = "手机号码")] + [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号码格式不正确")] + public string? Phone { get; set; } + + [Display(Name = "地址")] + [StringLength(200, ErrorMessage = "地址长度不能超过200个字符")] + public string? Address { get; set; } + + [Display(Name = "我已阅读并同意条款")] + [Range(typeof(bool), "true", "true", ErrorMessage = "必须同意条款")] + public bool AgreeToTerms { get; set; } + + // 专业列表,用于下拉框 + public List? MajorOptions { get; set; } + } +} +``` + +**创建 `StudentEditViewModel.cs`**: + +``` +using System.ComponentModel.DataAnnotations; + +namespace StudentManagementSystem.ViewModels +{ + public class StudentEditViewModel + { + public int Id { get; set; } + + [Required(ErrorMessage = "姓名不能为空")] + [Display(Name = "姓名")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在2-50个字符之间")] + public string? Name { get; set; } + + [Required(ErrorMessage = "年龄不能为空")] + [Display(Name = "年龄")] + [Range(15, 40, ErrorMessage = "年龄必须在15-40岁之间")] + public int Age { get; set; } + + [Required(ErrorMessage = "邮箱不能为空")] + [Display(Name = "邮箱")] + [EmailAddress(ErrorMessage = "邮箱格式不正确")] + public string? Email { get; set; } + + [Required(ErrorMessage = "请选择专业")] + [Display(Name = "专业")] + public string? Major { get; set; } + + [Required(ErrorMessage = "请选择入学时间")] + [Display(Name = "入学时间")] + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } + + [Display(Name = "手机号码")] + [RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号码格式不正确")] + public string? Phone { get; set; } + + [Display(Name = "地址")] + [StringLength(200, ErrorMessage = "地址长度不能超过200个字符")] + public string? Address { get; set; } + + [Display(Name = "是否在读")] + public bool IsActive { get; set; } + + // 专业列表,用于下拉框 + public List? MajorOptions { get; set; } + } +} +``` + +### **步骤4:更新控制器使用视图模型** + +**更新 `StudentController.cs` 中的Create方法**: + +``` +using StudentManagementSystem.ViewModels; + +// GET: /Student/Create +public IActionResult Create() +{ + var viewModel = new StudentCreateViewModel + { + MajorOptions = new List + { + "计算机科学", + "软件工程", + "网络工程", + "数据科学", + "人工智能", + "信息安全", + "物联网工程", + "其他" + } + }; + + return View(viewModel); +} + +// POST: /Student/Create +[HttpPost] +[ValidateAntiForgeryToken] +public IActionResult Create(StudentCreateViewModel viewModel) +{ + if (!ModelState.IsValid) + { + // 重新加载专业列表 + viewModel.MajorOptions = GetMajorList(); + return View(viewModel); + } + + // 将视图模型转换为领域模型 + var student = new Student + { + Name = viewModel.Name, + Age = viewModel.Age, + Email = viewModel.Email, + Major = viewModel.Major, + EnrollmentDate = viewModel.EnrollmentDate, + Phone = viewModel.Phone, + Address = viewModel.Address + }; + + // 模拟保存(后续章节会保存到数据库) + student.Id = _students.Count > 0 ? _students.Max(s => s.Id) + 1 : 1; + _students.Add(student); + + return RedirectToAction(nameof(Index)); +} + +// 辅助方法:获取专业列表 +private List GetMajorList() +{ + return new List + { + "计算机科学", + "软件工程", + "网络工程", + "数据科学", + "人工智能", + "信息安全", + "物联网工程", + "其他" + }; +} +``` + +### **步骤5:更新Create视图使用视图模型** + +**更新 `Views/Student/Create.cshtml`**: + +``` +@model StudentManagementSystem.ViewModels.StudentCreateViewModel + +@{ + ViewData["Title"] = "添加新学生"; +} + +
+
+
+
+
+

@ViewData["Title"]

+
+
+
+ @Html.AntiForgeryToken() + + +
+ +
+ +
+ + + +
+ + +
+ + + +
+
+ +
+ +
+ + + +
+ + +
+ + + +
+
+ +
+ +
+ + + +
+ + +
+ + + +
+
+ +
+ +
+ + + +
+ + +
+ + + +
+
+ + +
+
+ + + +
+
+ + +
+ + 返回列表 + + +
+
+
+
+ + +
+
+
填写说明
+
    +
  • 所有带 * 的字段为必填项
  • +
  • 姓名长度在2-50个字符之间,支持中文、英文
  • +
  • 年龄必须在15-40岁之间
  • +
  • 邮箱和确认邮箱必须一致
  • +
  • 入学时间不能是周末,且不能晚于当前日期
  • +
  • 手机号码必须是11位中国大陆手机号
  • +
+
+
+
+
+
+ + + + +@section Scripts { + +} +``` + +### **步骤6:添加模型相关扩展方法** + +**创建 `Extensions` 文件夹和 `ModelExtensions.cs`**: + +``` +using StudentManagementSystem.Models; +using StudentManagementSystem.ViewModels; + +namespace StudentManagementSystem.Extensions +{ + public static class ModelExtensions + { + // 将领域模型转换为编辑视图模型 + public static StudentEditViewModel ToEditViewModel(this Student student) + { + return new StudentEditViewModel + { + Id = student.Id, + Name = student.Name, + Age = student.Age, + Email = student.Email, + Major = student.Major, + EnrollmentDate = student.EnrollmentDate, + Phone = student.Phone, + Address = student.Address, + IsActive = student.IsActive + }; + } + + // 将编辑视图模型转换为领域模型 + public static Student ToDomainModel(this StudentEditViewModel viewModel, Student existingStudent = null) + { + var student = existingStudent ?? new Student(); + + student.Name = viewModel.Name; + student.Age = viewModel.Age; + student.Email = viewModel.Email; + student.Major = viewModel.Major; + student.EnrollmentDate = viewModel.EnrollmentDate; + student.Phone = viewModel.Phone; + student.Address = viewModel.Address; + student.IsActive = viewModel.IsActive; + student.UpdatedAt = DateTime.Now; + + return student; + } + + // 验证学生年龄是否合理 + public static bool IsValidAge(this Student student) + { + return student.Age >= 15 && student.Age <= 40; + } + + // 获取学生状态描述 + public static string GetStatusDescription(this Student student) + { + var years = (DateTime.Now - student.EnrollmentDate).TotalDays / 365; + + if (!student.IsActive) + { + return "已毕业"; + } + else if (years < 1) + { + return "大一新生"; + } + else if (years < 2) + { + return "大二学生"; + } + else if (years < 3) + { + return "大三学生"; + } + else if (years < 4) + { + return "大四学生"; + } + else + { + return "超期在读"; + } + } + } +``` \ No newline at end of file -- Gitee