一、项目概述
1. 项目定位
本项目是一个基于 ASP.NET Core MVC 8 框架开发的产品管理系统,采用“模型-视图-控制器(MVC)”架构,实现了产品数据的完整生命周期管理(增删改查+筛选)。
2. 核心功能
● 产品列表展示:显示所有产品的基本信息(名称、类别、价格、状态等)
● 筛选功能:支持按类别、价格范围、在售状态多条件组合筛选
● 产品操作:添加新产品、编辑已有产品、删除产品、查看产品详情
● 数据持久化:使用 SQLite 数据库存储数据,程序重启后数据不丢失
3. 技术栈
● 框架:ASP.NET Core MVC 8
● 数据库:SQLite(文件型数据库,无需额外安装)
● ORM:Entity Framework Core(EF Core,数据库操作框架)
● 前端:Razor 视图引擎 + Bootstrap(样式框架)
● 工具:EF Core 命令行工具(数据库迁移)
4. 适用场景
● 编程新手学习 MVC 架构、前后端数据交互、数据库操作
● 小型项目的产品管理模块快速搭建
● 教学演示:讲解 HTTP 协议、MVC 分层、数据持久化等核心概念
5. 项目视图



二、项目结构详解
1. 整体目录结构
├─ Models/ # 模型层:定义数据结构和规则
│ └─ Product.cs # 产品实体类
├─ Data/ # 数据访问层:数据库交互核心
│ └─ AppDbContext.cs # 数据库上下文(EF Core 核心)
├─ Controllers/ # 控制器层:处理业务逻辑和请求
│ └─ ProductsController.cs # 产品管理控制器
├─ Views/ # 视图层:页面展示模板
│ └─ Products/ # 产品相关视图(与控制器同名)
│ ├─ Index.cshtml # 产品列表+筛选页面
│ ├─ Details.cshtml # 产品详情页面
│ ├─ Create.cshtml # 添加产品表单页面
│ ├─ Edit.cshtml # 编辑产品表单页面
│ └─ Delete.cshtml # 删除产品确认页面
├─ appsettings.json # 项目配置文件(数据库连接等)
├─ Program.cs # 程序入口(服务注册、中间件配置)
└─ ProductDb.db # SQLite 数据库文件(自动生成)
2. 各模块详细说明
1. 模型层(Models):Product.cs
作用:定义产品的数据结构、验证规则,是“数据的身份证”。
using System.ComponentModel.DataAnnotations; // 引入数据验证特性
namespace ProductMvcDemo.Models;
public class Product
{
/// <summary>
/// 产品ID(主键,唯一标识)
/// </summary>
public int Id { get; set; }
/// <summary>
/// 产品名称(必填,最多50个字符)
/// </summary>
[Required(ErrorMessage = "产品名称不能为空")] // 非空验证
[Display(Name = "产品名称")] // 页面显示的标签文本
[StringLength(50, ErrorMessage = "名称不能超过50个字符")] // 长度限制
public string Name { get; set; } = string.Empty;
/// <summary>
/// 产品类别(最多30个字符)
/// </summary>
[Display(Name = "产品类别")]
[StringLength(30)]
public string Category { get; set; } = string.Empty;
/// <summary>
/// 产品价格(必填,必须大于0)
/// </summary>
[Required(ErrorMessage = "价格不能为空")]
[Display(Name = "产品价格")]
[Range(0.01, double.MaxValue, ErrorMessage = "价格必须大于0")] // 数值范围验证
public decimal Price { get; set; }
/// <summary>
/// 是否在售(默认在售)
/// </summary>
[Display(Name = "是否在售")]
public bool IsActive { get; set; } = true;
/// <summary>
/// 创建时间(自动生成,无需用户输入)
/// </summary>
[Display(Name = "创建时间")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
}
关键说明:
● 数据验证特性([Required]、[Range] 等):由 MVC 框架自动验证,不符合规则时页面会显示错误提示
● 主键 Id:EF Core 会自动识别名为 Id 的字段作为数据库表的主键
● 默认值:IsActive = true、CreatedAt = DateTime.Now 避免空值问题
2. 数据访问层(Data):AppDbContext.cs
作用:程序与数据库的“桥梁”,负责将 C# 模型映射到数据库表,执行数据操作。
using Microsoft.EntityFrameworkCore;
using ProductMvcDemo.Models;
namespace ProductMvcDemo.Data;
public class AppDbContext : DbContext
{
/// <summary>
/// 构造函数:接收数据库配置(从 Program.cs 注入)
/// </summary>
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
/// <summary>
/// 产品数据表:操作数据库的“入口”,对应 SQLite 中的 Products 表
/// </summary>
public DbSet<Product> Products => Set<Product>();
/// <summary>
/// 初始化测试数据(程序首次运行时执行)
/// </summary>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 向 Products 表插入5条测试数据
modelBuilder.Entity<Product>().HasData(
new Product { Id = 1, Name = "智能手机", Category = "电子产品", Price = 2999.99m },
new Product { Id = 2, Name = "笔记本电脑", Category = "电子产品", Price = 5999.99m },
new Product { Id = 3, Name = "T恤衫", Category = "服装", Price = 99.99m },
new Product { Id = 4, Name = "运动鞋", Category = "服装", Price = 399.99m, IsActive = false },
new Product { Id = 5, Name = "咖啡机", Category = "家用电器", Price = 899.99m }
);
}
}
关键说明:
● 继承 DbContext:EF Core 的核心基类,提供数据库连接、数据操作的基础功能
● DbSet Products:映射数据库中的 Products 表,后续通过 _context.Products 操作数据(增删改查)
● OnModelCreating:数据库首次创建时执行,用于初始化测试数据,方便直接测试功能
3. 程序入口(Program.cs)
作用:项目的“启动配置中心”,注册服务(数据库、MVC 等)、配置中间件(路由、静态文件等)。
using Microsoft.EntityFrameworkCore;
using ProductMvcDemo.Data;
namespace ProductMvcDemo
{
public class Program
{
public static void Main(string[] args)
{
// 1. 创建 Web 应用构建器(负责注册服务)
var builder = WebApplication.CreateBuilder(args);
// 2. 注册 MVC 服务(支持控制器和视图)
builder.Services.AddControllersWithViews();
// 3. 注册数据库上下文(配置 SQLite 数据库)
//框架在此配置数据库上下文后,我们就可以在控制器中使用AppDbContext了
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
// 4. 构建应用实例
var app = builder.Build();
// 5. 开发环境配置(错误页、HTTPS 重定向等)
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection(); // HTTP 自动跳转到 HTTPS
app.UseStaticFiles(); // 支持静态文件(CSS/JS/图片)
app.UseRouting(); // 路由支持
app.UseAuthorization();// 授权(暂时用不到)
// 6. 默认路由:控制器/动作/参数 配置默认路由规则(核心!URL 解析规则)
app.MapControllerRoute(
name: "default",
// 路由模板:{控制器}/{动作}/{可选ID}
pattern: "{controller=Products}/{action=Index}/{id?}"); // 把默认控制器改为Products
// 7. 启动应用
app.Run();
}
}
}
关键说明:
● 服务注册:AddControllersWithViews() 启用 MVC 功能,AddDbContext 注册数据库上下文(依赖注入)
● 路由规则:{controller=Products}/{action=Index}/{id?} 定义 URL 格式:
○ controller=Products:默认控制器为 ProductsController
○ action=Index:默认方法为 Index(产品列表)
○ id?:可选参数(用于详情、编辑、删除时传递产品ID)
● 中间件顺序:必须按“静态文件→路由→授权→控制器映射”的顺序配置,否则功能失效
4. 配置文件(appsettings.json)
作用:存储项目配置(日志、数据库连接等),无需硬编码修改配置。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*", // 允许所有主机访问
"ConnectionStrings": {
// SQLite 数据库连接字符串:数据库文件为 ProductDb.db(项目根目录)
"DefaultConnection": "Data Source=ProductDb.db"
}
}
关键说明:
● ConnectionStrings:数据库连接配置,本项目使用 SQLite 时,Data Source=文件名 指定数据库文件路径
● 配置热更新:修改后无需重启程序(开发环境),程序会自动读取最新配置
5. 控制器层(Controllers):ProductsController.cs
作用:处理浏览器请求、执行业务逻辑、操作数据库、返回响应(页面或数据),是“业务逻辑核心”。
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProductMvcDemo.Data;
using ProductMvcDemo.Models;
namespace ProductMvcDemo.Controllers;
// 控制器类名必须以 Controller 结尾(框架自动识别)
public class ProductsController : Controller
{
// 注入数据库上下文(依赖注入:无需手动创建对象)
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
#region 1. 产品列表 + 筛选(GET 请求)
/// <summary>
/// 产品列表页面,支持多条件筛选
/// </summary>
/// <param name="category">类别筛选(可选)</param>
/// <param name="minPrice">最低价格(可选)</param>
/// <param name="maxPrice">最高价格(可选)</param>
/// <param name="isActive">是否在售(可选)</param>
/// <returns>产品列表视图</returns>
[HttpGet] // 明确标记:处理 GET 请求(可省略,默认是 GET)
public async Task<IActionResult> Index(string? category, decimal? minPrice, decimal? maxPrice, bool? isActive)
{
// 1. 从数据库查询所有产品(IQueryable 支持延迟加载,未执行数据库查询)
IQueryable<Product> products = _context.Products.OrderBy(p => p.Id);
// 2. 条件筛选:按类别(模糊匹配)
if (!string.IsNullOrEmpty(category))
{
products = products.Where(p => p.Category.Contains(category));
ViewData["CurrentCategory"] = category; // 保留筛选条件到页面
}
// 3. 条件筛选:按最低价格
if (minPrice.HasValue)
{
products = products.Where(p => p.Price >= minPrice.Value);
ViewData["CurrentMinPrice"] = minPrice;
}
// 4. 条件筛选:按最高价格
if (maxPrice.HasValue)
{
products = products.Where(p => p.Price <= maxPrice.Value);
ViewData["CurrentMaxPrice"] = maxPrice;
}
// 5. 条件筛选:按是否在售
if (isActive.HasValue)
{
products = products.Where(p => p.IsActive == isActive.Value);
ViewData["CurrentIsActive"] = isActive;
}
// 6. 执行数据库查询,将数据传递到视图(View)
return View(await products.ToListAsync());
}
#endregion
#region 2. 产品详情(GET 请求)
/// <summary>
/// 查看单个产品的详细信息
/// </summary>
/// <param name="id">产品ID(必填)</param>
/// <returns>产品详情视图</returns>
[HttpGet]
public async Task<IActionResult> Details(int? id)
{
// 1. 验证 ID 是否为空
if (id == null)
{
return NotFound(); // 返回 404 错误(页面不存在)
}
// 2. 查询指定 ID 的产品(FirstOrDefaultAsync:返回第一个匹配项或 null)
var product = await _context.Products.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound(); // 产品不存在,返回 404
}
// 3. 将产品数据传递到详情视图
return View(product);
}
#endregion
#region 3. 添加产品(GET:显示表单;POST:提交数据)
/// <summary>
/// 显示添加产品的表单页面(仅展示,不操作数据)
/// </summary>
[HttpGet]
public IActionResult Create()
{
return View(); // 返回添加产品表单
}
/// <summary>
/// 提交添加产品的表单数据(操作数据库)
/// </summary>
/// <param name="product">表单提交的产品数据(模型绑定自动组装)</param>
[HttpPost] // 明确标记:处理 POST 请求
[ValidateAntiForgeryToken] // 防止跨站请求伪造(CSRF)攻击,必须添加
public async Task<IActionResult> Create([Bind("Id,Name,Category,Price,IsActive")] Product product)
{
// 1. 验证表单数据是否符合模型规则(比如名称非空、价格>0)
if (ModelState.IsValid)
{
product.CreatedAt = DateTime.Now; // 手动设置创建时间
_context.Products.Add(product); // 向数据库添加产品
await _context.SaveChangesAsync(); // 保存更改(执行 SQL 插入语句)
return RedirectToAction(nameof(Index)); // 跳回产品列表页面
}
// 2. 验证失败:返回表单页面,保留用户输入的错误数据
return View(product);
}
#endregion
#region 4. 编辑产品(GET:显示表单;POST:提交数据)
/// <summary>
/// 显示编辑产品的表单页面(加载现有产品数据)
/// </summary>
[HttpGet]
public async Task<IActionResult> Edit(int? id)
{
if (id == null) return NotFound();
var product = await _context.Products.FindAsync(id); // 根据 ID 查询产品
if (product == null) return NotFound();
return View(product); // 将产品数据传递到编辑表单
}
/// <summary>
/// 提交编辑产品的表单数据(更新数据库)
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Category,Price,IsActive,CreatedAt")] Product product)
{
// 1. 验证 URL 中的 ID 与表单中的 ID 是否一致
if (id != product.Id)
{
return NotFound();
}
// 2. 验证表单数据
if (ModelState.IsValid)
{
try
{
_context.Products.Update(product); // 更新产品数据
await _context.SaveChangesAsync(); // 保存更改(执行 SQL 更新语句)
}
catch (DbUpdateConcurrencyException)
{
// 处理并发冲突(比如多人同时编辑同一产品)
if (!ProductExists(product.Id))
{
return NotFound();
}
else
{
throw; // 其他异常直接抛出
}
}
return RedirectToAction(nameof(Index)); // 跳回列表页
}
return View(product); // 验证失败,返回编辑表单
}
#endregion
#region 5. 删除产品(GET:确认页面;POST:执行删除)
/// <summary>
/// 显示删除产品的确认页面
/// </summary>
[HttpGet]
public async Task<IActionResult> Delete(int? id)
{
if (id == null) return NotFound();
var product = await _context.Products.FirstOrDefaultAsync(p => p.Id == id);
if (product == null) return NotFound();
return View(product); // 显示确认删除页面
}
/// <summary>
/// 执行删除产品操作(POST 请求,名称为 DeleteConfirmed 避免与 GET 方法冲突)
/// </summary>
[HttpPost, ActionName("Delete")] // ActionName:URL 中仍用 Delete,实际执行 DeleteConfirmed
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product); // 从数据库删除产品
}
await _context.SaveChangesAsync(); // 保存更改(执行 SQL 删除语句)
return RedirectToAction(nameof(Index)); // 跳回列表页
}
#endregion
/// <summary>
/// 辅助方法:检查产品是否存在(避免重复代码)
/// </summary>
private bool ProductExists(int id)
{
return _context.Products.Any(p => p.Id == id);
}
}
关键说明:
● 依赖注入:AppDbContext 通过构造函数注入,无需手动 new 对象,框架自动管理生命周期
● 模型绑定:表单数据或 URL 参数会自动组装成 Product 对象(比如 POST 提交时,表单字段名与 Product 属性名一致即可)
● [ValidateAntiForgeryToken]:防止恶意网站伪造请求提交数据,所有 POST 表单必须添加
● 响应类型:
○ return View(数据):返回指定视图,并传递数据
○ return RedirectToAction(方法名):重定向到其他方法(比如添加成功后跳回列表)
○ return NotFound():返回 404 错误页面
6. 视图层(Views):产品相关页面
作用:页面展示模板,接收控制器传递的数据,生成用户可见的 HTML 页面。
(1)产品列表页:Index.cshtml
@* 声明视图接收的数据类型:产品列表(IEnumerable<Product>)*@
@model IEnumerable<ProductMvcDemo.Models.Product>
@{
ViewData["Title"] = "产品列表"; // 页面标题
}
<h1>产品管理</h1>
<!-- 添加产品按钮(链接到 Create 方法的 GET 请求) -->
<p>
<a asp-action="Create" class="btn btn-primary">添加新产品</a>
</p>
<!-- 筛选表单(GET 请求:数据附在 URL 上) -->
<form asp-action="Index" method="get" class="row mb-4">
<!-- 类别筛选 -->
<div class="col-md-3">
<input type="text" name="category" class="form-control"
placeholder="输入类别筛选" value="@ViewData["CurrentCategory"]" />
</div>
<!-- 最低价格筛选 -->
<div class="col-md-2">
<input type="number" step="0.01" name="minPrice" class="form-control"
placeholder="最低价格" value="@ViewData["CurrentMinPrice"]" />
</div>
<!-- 最高价格筛选 -->
<div class="col-md-2">
<input type="number" step="0.01" name="maxPrice" class="form-control"
placeholder="最高价格" value="@ViewData["CurrentMaxPrice"]" />
</div>
<!-- 状态筛选(下拉框) -->
<div class="col-md-2">
<select name="isActive" class="form-control" onchange="this.form.submit()">
<option value="">全部状态</option>
<option value="true" @((ViewData["CurrentIsActive"]?.ToString() == "True") ? "selected" : "")>在售</option>
<option value="false" @((ViewData["CurrentIsActive"]?.ToString() == "False") ? "selected" : "")>下架</option>
</select>
</div>
<!-- 筛选/重置按钮 -->
<div class="col-md-2">
<button type="submit" class="btn btn-secondary">筛选</button>
<a asp-action="Index" class="btn btn-outline-secondary">重置</a>
</div>
</form>
<!-- 产品表格 -->
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>@Html.DisplayNameFor(model => model.Name)</th> <!-- 显示模型属性的 Display 名称 -->
<th>@Html.DisplayNameFor(model => model.Category)</th>
<th>@Html.DisplayNameFor(model => model.Price)</th>
<th>@Html.DisplayNameFor(model => model.IsActive)</th>
<th>@Html.DisplayNameFor(model => model.CreatedAt)</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@* 循环遍历控制器传递的产品列表(Model 即 IEnumerable<Product>)*@
@foreach (var item in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td> <!-- 显示产品名称 -->
<td>@Html.DisplayFor(modelItem => item.Category)</td> <!-- 显示类别 -->
<td>¥@item.Price.ToString("0.00")</td> <!-- 价格格式化(保留两位小数) -->
<td>
@* 状态显示(在售=绿色徽章,下架=灰色徽章)*@
@if (item.IsActive)
{
<span class="badge bg-success">在售</span>
}
else
{
<span class="badge bg-secondary">下架</span>
}
</td>
<td>@item.CreatedAt.ToString("yyyy-MM-dd")</td> <!-- 日期格式化 -->
<td>
@* 详情按钮:链接到 Details 方法,传递 id 参数 *@
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-info">详情</a>
@* 编辑按钮:链接到 Edit 方法 *@
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-warning">编辑</a>
@* 删除按钮:链接到 Delete 方法 *@
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-danger">删除</a>
</td>
</tr>
}
</tbody>
</table>

关键说明:
● @model IEnumerable:声明视图接收的数据类型,Model 变量即控制器传递的产品列表
● 标签助手(Tag Helper):
○ asp-action=“Create”:自动生成 URL /Products/Create(关联当前控制器 Products)
○ asp-route-id=“@item.Id”:传递路径参数(比如 Details 方法需要的 id)
● ViewData:控制器传递的临时数据(比如筛选条件),用于页面回显
● @Html.DisplayFor:显示模型属性值,自动应用 [Display] 特性的配置
(2)添加产品表单:Create.cshtml
@model ProductMvcDemo.Models.Product
@{
ViewData["Title"] = "添加产品";
}
<h1>添加新产品</h1>
<hr />
<div class="row">
<div class="col-md-4">
@* 表单:提交到 Create 方法(POST 请求)*@
<form asp-action="Create">
@* 显示模型验证错误(比如名称为空、价格<=0)*@
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<!-- 产品名称输入框 -->
<div class="form-group">
<label asp-for="Name" class="control-label"></label> <!-- 显示 "产品名称" -->
<input asp-for="Name" class="form-control" /> <!-- 绑定 Name 属性 -->
<span asp-validation-for="Name" class="text-danger"></span> <!-- 显示该字段的错误提示 -->
</div>
<!-- 产品类别输入框 -->
<div class="form-group">
<label asp-for="Category" class="control-label"></label>
<input asp-for="Category" class="form-control" />
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<!-- 产品价格输入框 -->
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" step="0.01" /> <!-- step=0.01 支持小数 -->
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<!-- 是否在售复选框 -->
<div class="form-group form-check">
<label asp-for="IsActive" class="form-check-label"></label>
<input asp-for="IsActive" class="form-check-input" />
<span asp-validation-for="IsActive" class="text-danger"></span>
</div>
<!-- 提交/取消按钮 -->
@* 这个的创建是一个httppost方法,因为这个页面是从httpget的create打开的,所以这里submit也是给httppost
这里的取消,走到了controller的Index *@
<div class="form-group mt-3">
<input type="submit" value="创建" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
</div>
@* 加载验证脚本(用于客户端实时验证,比如输入为空时立即提示)*@
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

关键说明:
● asp-for 标签助手:绑定模型属性,自动生成 name、id 属性(比如 asp-for=“Name” 生成 name=“Name”)
● 验证提示:asp-validation-summary 显示全局错误,asp-validation-for 显示单个字段错误
● 客户端验证:_ValidationScriptsPartial 提供实时验证(无需提交表单就知道数据是否合法)
其他视图(Details.cshtml、Edit.cshtml、Delete.cshtml)逻辑类似,核心都是“接收控制器数据→展示/提交数据”。
三、核心原理:前后端数据交互详解
1. 交互核心模型:HTTP 请求-响应模型
浏览器与服务器的交互遵循“请求-响应”模式,类比“写信通信”:
● 浏览器(客户端):写“信”(HTTP 请求),说明“要做什么”(查产品、提交表单)和“附带数据”(筛选条件、表单内容)
● 服务器(Controller):读“信”(解析请求)→ 做事(操作数据库)→ 写“回信”(HTTP 响应),返回“结果”(页面、数据)
● 浏览器:读“回信”→ 展示给用户(渲染 HTML 页面)
2. HTTP 核心方法:GET 与 POST 区别(结合项目)

项目中 GET 与 POST 的交互流程示例
示例1:查看产品列表(GET 请求)
- 浏览器行为:用户输入 URL https://localhost:5001(默认路由→Products/Index)
- 请求内容:
○ 方法:GET
○ URL:https://localhost:5001/Products/Index
○ 数据:无(或筛选条件,比如 ?category=电子产品) - 服务器处理(ProductsController.Index 方法):
○ 解析请求:是 GET 请求→匹配 [HttpGet] 标记的 Index 方法
○ 模型绑定:筛选条件(category=电子产品)自动映射到方法参数 string category
○ 业务逻辑:查询数据库→筛选产品→传递数据到 Index 视图 - 响应内容:返回 Index.cshtml 渲染的 HTML 页面
- 浏览器行为:渲染页面→用户看到产品列表
示例2:添加产品(POST 请求)
- 浏览器行为:用户填完 Create 表单→点“创建”按钮
- 请求内容:
○ 方法:POST
○ URL:https://localhost:5001/Products/Create
○ 数据:表单内容(Name=无线耳机&Category=电子产品&Price=599.99),放在请求体中 - 服务器处理(ProductsController.Create(Product product) 方法):
○ 解析请求:是 POST 请求→匹配 [HttpPost] 标记的 Create 方法
○ 模型绑定:请求体数据自动组装成 Product 对象(Name 对应 product.Name)
○ 数据验证:检查 ModelState.IsValid(是否符合 Product 类的验证规则)
○ 业务逻辑:验证通过→添加到数据库→重定向到 Index 方法 - 响应内容:返回重定向指令(302 Found),告诉浏览器跳转到 Index 页面
- 浏览器行为:自动跳转到产品列表→用户看到新添加的产品
3. 数据传递的3种方式(项目实战)
(1)QueryString(查询字符串):GET 请求传少量数据
● 格式:URL?参数名1=值1&参数名2=值2
● 项目示例:筛选产品时,传递类别和价格条件
○ 浏览器 URL:https://localhost:5001/Products/Index?category=电子产品&minPrice=1000
○ 控制器接收:Index(string? category, decimal? minPrice)(参数名与 URL 参数名一致)
● 适用场景:筛选、分页、简单查询
(2)路径参数:GET 请求传唯一标识(ID)
● 格式:URL/参数值(符合路由规则 {controller}/{action}/{id?})
● 项目示例:查看产品详情时,传递产品 ID
○ 浏览器 URL:https://localhost:5001/Products/Details/1
○ 控制器接收:Details(int? id)(id 对应 URL 中的 1)
● 适用场景:传递唯一标识(详情、编辑、删除操作)
(3)请求体(Request Body):POST 请求传大量/敏感数据
● 格式:数据放在请求体中(不暴露在 URL),格式为 key=value(表单提交)或 JSON
● 项目示例:添加产品时,提交表单数据
○ 请求体数据:Name=无线耳机&Category=电子产品&Price=599.99&IsActive=true
○ 控制器接收:Create(Product product)(模型绑定自动组装 Product 对象)
● 适用场景:表单提交、上传文件、传递敏感数据
4. 控件与 Controller 方法的绑定机制
界面控件(链接、表单)能找到对应的 Controller 方法,核心靠“路由规则+标签助手”,流程如下:
绑定核心前提
● Controller 命名规则:类名以 Controller 结尾(ProductsController→别名 Products)
● 路由规则:{controller}/{action}/{id?}(URL 解析为“控制器/方法/参数”)
场景1:链接控件( 标签)绑定
控件代码(Index.cshtml):
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-info">详情</a>
绑定流程:
- 标签助手解析:asp-action=“Details” 识别当前控制器是 Products(视图在 Views/Products 文件夹),生成 URL:/Products/Details/1(item.Id=1)
- 浏览器点击链接:发送 GET 请求到该 URL
- 服务器解析 URL:Products→ProductsController,Details→Details 方法,1→id=1
- 绑定成功:执行 Details(1) 方法,返回详情页面
场景2:表单控件()绑定
控件代码(Create.cshtml):
<form asp-action="Create">
<input asp-for="Name" />
<input type="submit" value="创建" />
</form>
绑定流程:
- 标签助手解析:asp-action=“Create” 生成表单 action 属性:/Products/Create,表单默认 method=“post”
- 用户提交表单:发送 POST 请求到该 URL,请求体带表单数据
- 服务器解析:Products→ProductsController,Create→匹配 [HttpPost] 标记的 Create 方法
- 模型绑定:请求体数据自动组装成 Product 对象,传递给方法参数
- 绑定成功:执行方法,添加产品到数据库
场景3:下拉框筛选(触发表单提交)
控件代码(Index.cshtml):
<select name="isActive" class="form-control" onchange="this.form.submit()">
<option value="true">在售</option>
</select>
绑定流程:
- 用户选择“在售”:onchange 事件触发表单提交(method=“get”)
- 表单数据:isActive=true 拼接到 URL:/Products/Index?isActive=true
- 服务器解析:参数 isActive=true 映射到 Index 方法的 bool? isActive 参数
- 绑定成功:执行筛选逻辑,返回筛选后的产品列表
四、数据持久化:数据怎么“不丢失”?
1. 什么是数据持久化?
● 临时存储:数据存在内存中(比如程序中的变量),程序重启后数据丢失
● 持久化存储:数据存在硬盘上(比如数据库文件),程序重启后数据保留
本项目使用 SQLite 数据库文件(ProductDb.db)实现持久化,数据存储在硬盘上,关程序后不丢失。
2. 持久化核心:EF Core 与 SQLite 交互
EF Core 是“ORM(对象关系映射)框架”,作用是“翻译官”:
● 开发者写 C# 代码(_context.Products.Add(product))
● EF Core 自动翻译成 SQL 语句(INSERT INTO Products (Name, Category, Price) VALUES (…))
● SQLite 执行 SQL 语句,将数据写入 ProductDb.db 文件
3. 数据库初始化:迁移(Migration)操作(详细步骤)
首次运行项目时,需要通过“迁移”创建数据库和表,步骤如下:
步骤1:安装 EF Core 工具(全局安装)
打开命令提示符(管理员模式),执行:
# 清理 NuGet 缓存(避免安装失败)
dotnet nuget locals all --clear
# 安装 EF Core 命令行工具(与 .NET 8 匹配)
dotnet tool install --global dotnet-ef --version 8.0.0
步骤2:导航到项目根目录
# 示例:项目在桌面的 ProductMvcDemo 文件夹
cd C:\Users\你的用户名\Desktop\ProductMvcDemo\ProductMvcDemo
验证:执行 dir 命令,能看到 ProductMvcDemo.csproj 文件(项目文件)。
步骤3:创建迁移(记录模型到表的映射)
dotnet ef migrations add InitialCreate
● 作用:生成迁移文件,记录 Product 模型对应的数据库表结构
● 成功标志:项目中新增 Migrations 文件夹,包含迁移脚本
● 若失败:检查 Product 类是否有主键(Id 字段)、项目目录是否正确
步骤4:应用迁移(创建数据库和表)
dotnet ef database update
● 作用:执行迁移脚本,创建 ProductDb.db 数据库文件和 Products 表,并插入测试数据
● 成功标志:项目根目录出现 ProductDb.db 文件
步骤5:验证数据库
安装 SQLite 可视化工具(比如 DB Browser for SQLite):
- 打开工具→点击“打开数据库”→选择 ProductDb.db
- 左侧选择 Products 表→点击“浏览数据”→能看到 5 条测试数据,说明数据库创建成功
4. 数据操作对应关系(C# 代码 → SQL 语句)

五、项目运行与测试步骤
1. 运行前准备
● 安装 .NET 8 SDK(https://dotnet.microsoft.com/zh-cn/download/dotnet/8.0)
● 完成数据库迁移(生成 ProductDb.db 文件)
2. 运行项目
# 导航到项目根目录
cd 你的项目路径/ProductMvcDemo
# 启动项目
dotnet run
启动成功后,终端会显示监听 URL:
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
3. 测试功能(浏览器访问)
- 访问 https://localhost:5001→进入产品列表页面
- 测试筛选功能:输入类别“电子产品”→点击“筛选”→只显示电子产品
- 测试添加产品:点击“添加新产品”→填表单→点击“创建”→列表新增产品
- 测试编辑产品:点击某产品的“编辑”→修改价格→点击“保存”→产品价格更新
- 测试删除产品:点击某产品的“删除”→点击“删除”→产品从列表消失
- 验证持久化:关闭程序→重新运行→之前添加/修改/删除的记录仍存在
4. 常见问题排查

六、核心知识点总结
- MVC 架构:Model(数据结构+规则)→ Controller(业务逻辑+请求处理)→ View(页面展示),职责分离,易维护。
- HTTP 协议:GET 查数据,POST 改数据,数据传递方式不同(URL/请求体)。
- 路由与绑定:URL 按 {controller}/{action}/{id?} 解析,控件通过标签助手(asp-action)生成 URL,实现与 Controller 方法的绑定。
- 数据持久化:通过 EF Core 操作 SQLite 数据库,迁移功能实现模型到表的映射,数据存储在文件中不丢失。
- 验证机制:模型特性([Required])+ 客户端脚本 + 服务器验证,确保数据合法。
- .GET 请求:用于“查数据、显示页面”,数据通过 路径参数(ID 类数据)或 QueryString(筛选条件)传递,数据可见但长度有限。
- POST 请求:用于“改数据(增删改)”,数据通过 请求体(表单数据)传递,数据隐藏且无长度限制,需加 [ValidateAntiForgeryToken] 防攻击。
- 模型绑定:框架自动将“请求数据”(路径参数、QueryString、请求体)转换为 Controller 方法的“参数/对象”,无需手动解析。
- 响应逻辑:
○ 成功操作(增删改):重定向到列表页(RedirectToAction),避免重复提交。
○ 失败操作(数据验证不通过):返回原表单页面(View(数据)),回显错误数据和提示。
所有流程的核心都是“前端触发请求→数据按规则传递→Controller 处理→数据库交互→前端展示结果”,掌握这个规律就能理解所有 MVC 项目的交互逻辑!

6735

被折叠的 条评论
为什么被折叠?



