-
C#中的控制器与视图——用户列表展示
第三部分:Web应用开发
2. 控制器与视图——用户列表展示
实例介绍
之前做电商后台时,用户列表全靠硬拼HTML:控制器里用StringBuilder写表格,改个样式要找半天,加个分页得写几十行重复代码;用户状态修改要刷新整个页面,慢得要死。后来用ASP.NET Core MVC重构:控制器专注查数据、处理业务,视图用Razor语法渲染页面,分页用PagedList一键搞定,状态修改用AJAX异步刷新,代码量少了一半,维护起来vb.net教程C#教程python教程SQL教程access 2010教程
轻松太多。这节就带你做后台用户列表,彻底搞懂控制器和视图的协作逻辑。
需求分析
用户列表要解决“高效展示、便捷交互、易于维护”的问题,具体需求如下:
1.核心展示:用户ID、用户名、邮箱、注册时间、状态(启用/禁用);
2.交互功能:分页(每页10条)、搜索(按用户名/邮箱)、排序(按注册时间)、状态开关(点击直接修改);
3.样式统一:用Bootstrap做响应式布局,表格交替行颜色、hover效果;
4.数据验证:搜索关键词长度限制(2-20字符),状态修改时验证权限;
5.性能优化:分页查询(避免一次性加载所有用户)、异步请求(状态修改不刷新页面);
6.错误处理:搜索无结果时显示提示,状态修改失败时弹出错误信息;
7.代码复用:状态开关做成可复用组件,其他页面直接调用。
代码实现
前置条件:.NET 6+、EF Core(或Dapper)、Bootstrap 5;延续电商后台场景,定义User模型、IUserRepository数据访问层;需安装NuGet包 PagedList.Core(分页)、Microsoft.AspNetCore.Mvc.TagHelpers(Tag Helper)。
场景1:基础用户列表(控制器传数据到视图)
从数据库查询用户列表,控制器把数据传给视图,视图用Razor语法渲染表格。
步骤1:定义User模型和数据访问层
csharp
// Models/User.cs:用户模型
public class User
{
public int Id { get; set; } // 用户ID
public string UserName { get; set; } // 用户名
public string Email { get; set; } // 邮箱
public DateTime RegisterTime { get; set; } // 注册时间
public bool IsEnabled { get; set; } // 状态(启用/禁用)
}
// Repositories/IUserRepository.cs:数据访问接口
public interface IUserRepository
{
IQueryable<User> GetAll(); // 返回IQueryable支持后续过滤/分页
Task<User> GetByIdAsync(int id);
Task UpdateAsync(User user);
}
// Repositories/UserRepository.cs:EF Core实现
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context)
{
_context = context;
}
public IQueryable<User> GetAll() => _context.Users.AsNoTracking(); // AsNoTracking提升查询性能
public async Task<User> GetByIdAsync(int id) => await _context.Users.FindAsync(id);
public async Task UpdateAsync(User user)
{
_context.Users.Update(user);
await _context.SaveChangesAsync();
}
}
步骤2:注册服务(Program.cs)
csharp
// 注册数据访问服务(Scoped:每个请求实例化一次)
builder.Services.AddScoped<IUserRepository, UserRepository>();
// 注册EF Core数据库上下文
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
步骤3:Users控制器(Areas/Admin/Controllers/UsersController.cs)
csharp
using Microsoft.AspNetCore.Mvc;
using ECommerceWeb.Models;
using ECommerceWeb.Repositories;
using PagedList.Core;
namespace ECommerceWeb.Areas.Admin.Controllers
{
[Area("Admin")]
public class UsersController : Controller
{
private readonly IUserRepository _userRepo;
private const int PageSize = 10; // 每页10条
// 构造函数注入数据访问服务
public UsersController(IUserRepository userRepo)
{
_userRepo = userRepo;
}
// 基础用户列表:对应URL /Admin/Users/Index
public async Task<IActionResult> Index(int page = 1, string search = "")
{
// 1. 构建查询
var query = _userRepo.GetAll();
// 2. 搜索过滤
if (!string.IsNullOrWhiteSpace(search))
{
// 验证搜索关键词长度
if (search.Length < 2 || search.Length > 20)
{
TempData["Error"] = "搜索关键词长度必须在2-20字符之间!";
return View(new PagedList<User>(new List<User>(), page, PageSize));
}
query = query.Where(u => u.UserName.Contains(search) || u.Email.Contains(search));
}
// 3. 分页查询(按注册时间倒序)
var totalCount = await query.CountAsync();
var users = await query.OrderByDescending(u => u.RegisterTime)
.Skip((page - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
// 4. 封装分页数据
var pagedUsers = new PagedList<User>(users, page, PageSize, totalCount);
// 5. 把搜索关键词传给视图(保持搜索状态)
ViewBag.Search = search;
return View(pagedUsers); // 把分页数据传给视图
}
}
}
步骤4:Razor视图(Areas/Admin/Views/Users/Index.cshtml)
html
@model PagedList.IPagedList<ECommerceWeb.Models.User>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <!-- 启用Tag Helper -->
@{
ViewData["Title"] = "用户列表";
Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; // 后台布局页
}
<div class="container mt-4">
<h2 class="mb-4">用户列表</h2>
<!-- 错误提示 -->
@if (TempData["Error"] != null)
{
<div class="alert alert-danger" role="alert">@TempData["Error"]</div>
}
<!-- 搜索表单 -->
<form asp-action="Index" method="get" class="mb-4">
<div class="input-group">
<input type="text" name="search" class="form-control"
placeholder="搜索用户名/邮箱" value="@ViewBag.Search">
<button type="submit" class="btn btn-primary">搜索</button>
</div>
</form>
<!-- 用户表格 -->
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>注册时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@if (Model.Any())
{
@foreach (var user in Model)
{
<tr>
<td>@user.Id</td>
<td>@user.UserName</td>
<td>@user.Email</td>
<td>@user.RegisterTime.ToString("yyyy-MM-dd HH:mm")</td>
<td>
<!-- 状态开关(后续场景3改为AJAX) -->
<input type="checkbox" class="form-check-input"
id="status-@user.Id" @(user.IsEnabled ? "checked" : "")
onclick="changeStatus(@user.Id, this.checked)">
<label class="form-check-label" for="status-@user.Id">
@(user.IsEnabled ? "启用" : "禁用")
</label>
</td>
<td>
<a asp-action="Edit" asp-route-id="@user.Id" class="btn btn-sm btn-secondary">编辑</a>
<a asp-action="Delete" asp-route-id="@user.Id" class="btn btn-sm btn-danger">删除</a>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="6" class="text-center text-muted">暂无用户数据</td>
</tr>
}
</tbody>
</table>
<!-- 分页导航 -->
@if (Model.PageCount > 1)
{
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<!-- 上一页 -->
<li class="page-item @(Model.IsFirstPage ? "disabled" : "")">
<a class="page-link" asp-action="Index"
asp-route-page="@(Model.PageNumber - 1)"
asp-route-search="@ViewBag.Search">上一页</a>
</li>
<!-- 页码 -->
@for (int i = 1; i <= Model.PageCount; i++)
{
<li class="page-item @(i == Model.PageNumber ? "active" : "")">
<a class="page-link" asp-action="Index"
asp-route-page="@i"
asp-route-search="@ViewBag.Search">@i</a>
</li>
}
<!-- 下一页 -->
<li class="page-item @(Model.IsLastPage ? "disabled" : "")">
<a class="page-link" asp-action="Index"
asp-route-page="@(Model.PageNumber + 1)"
asp-route-search="@ViewBag.Search">下一页</a>
</li>
</ul>
</nav>
}
</div>
<!-- 状态修改JS -->
<script>
function changeStatus(userId, isEnabled) {
fetch(`/Admin/Users/ChangeStatus?userId=${userId}&isEnabled=${isEnabled}`, {
method: "POST",
headers: {
"X-CSRF-TOKEN": document.querySelector('input[name="__RequestVerificationToken"]').value
}
})
.then(res => res.json())
.then(data => {
if (!data.success) {
alert(data.message);
// 恢复原状态
document.getElementById(`status-${userId}`).checked = !isEnabled;
}
})
.catch(err => {
alert("状态修改失败,请稍后重试!");
document.getElementById(`status-${userId}`).checked = !isEnabled;
});
}
</script>
场景2:状态修改(AJAX异步请求)
控制器添加ChangeStatus动作,处理AJAX请求,修改用户状态后返回JSON结果。
步骤1:Users控制器添加ChangeStatus动作
csharp
[HttpPost] // 只接受POST请求
[ValidateAntiForgeryToken] // 防止CSRF攻击
public async Task<IActionResult> ChangeStatus(int userId, bool isEnabled)
{
try
{
var user = await _userRepo.GetByIdAsync(userId);
if (user == null)
{
return Json(new { success = false, message = "用户不存在!" });
}
// 模拟权限验证(后续讲身份验证时替换为真实逻辑)
if (user.UserName == "admin") // 超级管理员不能禁用
{
return Json(new { success = false, message = "超级管理员状态不可修改!" });
}
user.IsEnabled = isEnabled;
await _userRepo.UpdateAsync(user);
return Json(new { success = true, message = "状态修改成功!" });
}
catch (Exception ex)
{
// 记录日志(实际项目中用ILogger)
return Json(new { success = false, message = "服务器内部错误:" + ex.Message });
}
}
步骤2:布局页添加CSRF令牌(Areas/Admin/Views/Shared/_Layout.cshtml)
在
把状态开关做成视图组件,其他页面直接调用,避免重复代码。
步骤1:创建UserStatusViewComponent(Areas/Admin/ViewComponents/UserStatusViewComponent.cs)
csharp
using Microsoft.AspNetCore.Mvc;
namespace ECommerceWeb.Areas.Admin.ViewComponents
{
public class UserStatusViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(int userId, bool isEnabled)
{
// 传递参数到视图
ViewBag.UserId = userId;
ViewBag.IsEnabled = isEnabled;
return View();
}
}
}
步骤2:视图组件的视图(Areas/Admin/Views/Shared/Components/UserStatus/Default.cshtml)
html
<input type="checkbox" class="form-check-input"
id="status-@ViewBag.UserId" @(ViewBag.IsEnabled ? "checked" : "")
onclick="changeStatus(@ViewBag.UserId, this.checked)">
<label class="form-check-label" for="status-@ViewBag.UserId">
@(ViewBag.IsEnabled ? "启用" : "禁用")
</label>
步骤3:在用户列表视图中调用组件
替换原来的状态开关代码:
html
<td>
@await Component.InvokeAsync("UserStatus", new { userId = user.Id, isEnabled = user.IsEnabled })
</td>
逐行讲解
场景1:基础用户列表核心代码
1.控制器构造函数注入:public UsersController(IUserRepository userRepo) → 依赖注入,解耦控制器和数据访问层,方便单元测试;
2.分页查询:Skip((page - 1) * PageSize).Take(PageSize) → 分页逻辑,Skip跳过前面的页,Take取当前页数据;
3.PagedList:PagedList
4.Razor视图:
1.@model PagedList.IPagedList
2.asp-action="Index" → Tag Helper,自动生成URL,避免硬编码(比如修改路由模板后,所有链接自动更新);
3.@foreach (var user in Model) → 循环渲染用户行,@user.Id直接输出模型属性;
4.TempData["Error"] → 临时数据,用于跨请求传递提示信息(比如搜索验证失败后,刷新页面仍显示提示)。
场景2:AJAX状态修改
1.[HttpPost]特性:标记动作只接受POST请求,避免GET请求修改数据;
2.[ValidateAntiForgeryToken]:验证CSRF令牌,防止跨站请求伪造攻击;
3.fetch API:浏览器原生AJAX API,发送POST请求时携带CSRF令牌;
4.JsonResult:返回JSON格式结果,视图用JS处理返回值,更新UI。
场景3:视图组件
1.ViewComponent:继承自ViewComponent,InvokeAsync方法接收参数,返回视图;
2.组件调用:@await Component.InvokeAsync("UserStatus", new { userId = user.Id, isEnabled = user.IsEnabled }) → 异步调用视图组件,传递参数;
3.代码复用:其他页面需要状态开关时,直接调用组件,无需重复写HTML和JS。
基础知识拓展
-
控制器核心概念
控制器类型:
Controller:带视图支持(返回ViewResult),适合MVC页面;
ControllerBase:无视图支持(返回JsonResult/NotFoundResult等),适合API;
IActionResult返回类型:
返回类型 用途说明
ViewResult 返回Razor视图
JsonResult 返回JSON数据
RedirectToActionResult 重定向到其他控制器动作
NotFoundResult 返回404错误
BadRequestResult 返回400错误(参数验证失败)
模型绑定:自动把URL参数、表单数据、JSON数据绑定到控制器参数(比如int page = 1自动绑定URL中的page参数)。
2. Razor视图核心语法
| 语法 | 用途说明 |
|---|---|
| @model | 强类型视图,指定模型类型 |
| @foreach/@if | 循环、条件判断 |
| @Html.DisplayFor | 显示模型属性(支持数据注解) |
| @Html.EditorFor | 生成编辑控件(如输入框) |
| @addTagHelper | 启用Tag Helper(自动生成URL) |
| @await | 异步调用(比如视图组件) |
-
布局视图与部分视图
布局视图(_Layout.cshtml):统一页面结构(页眉、页脚、导航),所有视图共享;
部分视图(_Partial.cshtml):复用HTML片段(比如搜索框、分页导航),用@Html.Partial("_SearchBox")调用;
视图组件 vs 部分视图:视图组件带业务逻辑(比如状态修改的权限验证),部分视图只渲染HTML。 -
性能优化技巧
异步动作:用async Task,避免阻塞线程;
AsNoTracking:EF Core查询时用AsNoTracking,提升查询性能(不需要跟踪实体变化时);
缓存:用[ResponseCache(Duration = 60)]缓存页面,减少数据库查询;
懒加载:表格数据量大时,用懒加载(滚动到底部加载下一页),避免一次性加载所有数据。
总结
控制器和视图是ASP.NET Core MVC的核心,控制器负责处理请求、业务逻辑、数据查询,视图负责渲染页面、用户交互,两者通过模型传递数据,分工明确,易于维护。关键要点:
1.强类型视图:用@model指定模型类型,避免运行时错误;
2.依赖注入:控制器通过构造函数注入服务,解耦代码;
3.异步编程:用async/await提升性能,避免阻塞;
4.代码复用:用视图组件、部分视图减少重复代码;
5.安全防护:用[ValidateAntiForgeryToken]防止CSRF攻击,用参数验证避免无效请求。
比如后台用户列表,控制器处理分页、搜索、状态修改的业务逻辑,视图专注于页面渲染,状态开关做成视图组件在其他页面复用,代码清晰,维护轻松。掌握这些技巧,你可以轻松构建高效、易维护的Web应用!
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49472.html










