VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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)

外或末尾添加:
html
@Html.AntiForgeryToken()
场景3:视图组件(复用状态开关)
把状态开关做成视图组件,其他页面直接调用,避免重复代码。
步骤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(users, page, PageSize, totalCount) → 封装分页数据,包含页码、总页数、是否第一页等信息;
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。
基础知识拓展

  1. 控制器核心概念
    控制器类型:
    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 异步调用(比如视图组件)
  1. 布局视图与部分视图
    布局视图(_Layout.cshtml):统一页面结构(页眉、页脚、导航),所有视图共享;
    部分视图(_Partial.cshtml):复用HTML片段(比如搜索框、分页导航),用@Html.Partial("_SearchBox")调用;
    视图组件 vs 部分视图:视图组件带业务逻辑(比如状态修改的权限验证),部分视图只渲染HTML。
  2. 性能优化技巧
    异步动作:用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


相关教程