【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(7.2) 模块管理,模块的添加、修改、删除
各位朋友,大家好!我们继续我们的ASP.NET MVC框架搭建之旅。在上一章节中,我们完成了菜单动态加载,其数据来源正是我们今天要深入探讨的核心——模块(Module)。
模块管理是任何后台管理系统的基石。它定义了系统的功能结构,是权限控制的载体。一个设计良好的模块管理功能,能够为后续的权限分配、角色管理打下坚实的基础。本篇将详细介绍如何从零开始,实现模块的添加、修改、删除 这三个核心操作,并融入一些最佳实践。
目录#
1. 核心概念与模型设计#
首先,我们需要明确“模块”是什么。在我们的框架中,一个模块通常对应系统的一个功能页面或一个操作单元。例如,“用户管理”、“角色管理”、“文章列表”都可以是独立的模块。
模块模型(Module)的设计至关重要,它需要包含足够的信息来描述一个模块,并支持树形结构(用于菜单层级)。
最佳实践: 使用 ParentId 字段实现无限层级的树形结构,这是组织复杂菜单系统的常见做法。
示例模型(Models/Module.cs):
using System;
using System.ComponentModel.DataAnnotations;
namespace YourProjectName.Models
{
public class Module
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "模块名称不能为空")]
[StringLength(50, ErrorMessage = "模块名称长度不能超过50个字符")]
[Display(Name = "模块名称")]
public string Name { get; set; }
[Display(Name = "控制器名称")]
[StringLength(100, ErrorMessage = "控制器名称长度不能超过100个字符")]
public string ControllerName { get; set; }
[Display(Name = "动作方法名称")]
[StringLength(100, ErrorMessage = "动作方法名称长度不能超过100个字符")]
public string ActionName { get; set; }
[Display(Name = "图标CSS类")]
[StringLength(50, ErrorMessage = "图标CSS类长度不能超过50个字符")]
public string IconClass { get; set; } = "fa fa-circle-o"; // 默认图标
[Display(Name = "排序码")]
[Range(0, 999, ErrorMessage = "排序码必须在0-999之间")]
public int SortCode { get; set; } = 0;
[Display(Name = "父级模块")]
public int? ParentId { get; set; } // Nullable<int> 表示根节点
// 导航属性 - 父模块
public virtual Module ParentModule { get; set; }
// 导航属性 - 子模块集合
public virtual ICollection<Module> ChildModules { get; set; }
[Display(Name = "是否激活")]
public bool IsActive { get; set; } = true;
[Display(Name = "创建时间")]
public DateTime CreateTime { get; set; } = DateTime.Now;
[Display(Name = "描述信息")]
[DataType(DataType.MultilineText)]
[StringLength(500, ErrorMessage = "描述信息长度不能超过500个字符")]
public string Description { get; set; }
}
}说明:
ParentId: 为NULL时,表示该模块是顶级模块(根菜单)。- 导航属性
ParentModule和ChildModules方便我们使用EF Core进行关联查询。 - 数据注解(如
[Required],[Display])用于后端验证和前端显示,是MVC开发中的最佳实践。
2. 数据访问层(Repository)实现#
我们使用Repository模式来封装所有数据访问逻辑,使Controller层与EF Core解耦。
通用仓储接口(IRepository.cs):
using System;
using System.Linq;
using System.Linq.Expressions;
namespace YourProjectName.Data
{
public interface IRepository<T> where T : class
{
T Get(int id);
IQueryable<T> GetAll();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
void SaveChanges();
}
}模块专属仓储接口(IModuleRepository.cs):
using YourProjectName.Models;
using System.Collections.Generic;
namespace YourProjectName.Data
{
public interface IModuleRepository : IRepository<Module>
{
// 获取所有顶级模块及其子模块(用于树形列表)
IEnumerable<Module> GetModulesWithChildren();
// 检查是否存在子模块(删除前校验)
bool HasChildren(int moduleId);
}
}模块仓储实现(ModuleRepository.cs):
using YourProjectName.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
namespace YourProjectName.Data
{
public class ModuleRepository : Repository<Module>, IModuleRepository
{
public ModuleRepository(ApplicationDbContext context) : base(context)
{
}
public IEnumerable<Module> GetModulesWithChildren()
{
// 使用Include进行贪婪加载,获取所有顶级模块及其子模块
return _context.Modules
.Include(m => m.ChildModules)
.Where(m => m.ParentId == null)
.OrderBy(m => m.SortCode)
.ToList();
}
public bool HasChildren(int moduleId)
{
return _context.Modules.Any(m => m.ParentId == moduleId);
}
}
}3. 业务逻辑层(Service)实现#
Service层处理核心业务逻辑,协调多个Repository的操作,并进行业务规则验证。
模块服务接口(IModuleService.cs):
using YourProjectName.Models;
using System.Collections.Generic;
namespace YourProjectName.Services
{
public interface IModuleService
{
IEnumerable<Module> GetAllModules();
Module GetModuleById(int id);
void CreateModule(Module module);
void UpdateModule(Module module);
void DeleteModule(int id);
// 获取模块树形结构数据(用于下拉选择父模块)
List<Module> GetModuleTree();
// 检查模块名称是否唯一
bool IsModuleNameUnique(string name, int? id = null);
}
}模块服务实现(ModuleService.cs):
using YourProjectName.Data;
using YourProjectName.Models;
using System.Collections.Generic;
using System.Linq;
namespace YourProjectName.Services
{
public class ModuleService : IModuleService
{
private readonly IModuleRepository _moduleRepository;
private readonly IUnitOfWork _unitOfWork;
public ModuleService(IModuleRepository moduleRepository, IUnitOfWork unitOfWork)
{
_moduleRepository = moduleRepository;
_unitOfWork = unitOfWork;
}
public IEnumerable<Module> GetAllModules()
{
return _moduleRepository.GetModulesWithChildren();
}
public Module GetModuleById(int id)
{
return _moduleRepository.Get(id);
}
public void CreateModule(Module module)
{
// 业务规则验证:模块名称唯一性
if (!IsModuleNameUnique(module.Name))
{
throw new System.Exception($"模块名称“{module.Name}”已存在,请更换。");
}
_moduleRepository.Add(module);
_unitOfWork.SaveChanges();
}
public void UpdateModule(Module module)
{
// 业务规则验证:模块名称唯一性(排除自身)
if (!IsModuleNameUnique(module.Name, module.Id))
{
throw new System.Exception($"模块名称“{module.Name}”已存在,请更换。");
}
var existingModule = _moduleRepository.Get(module.Id);
if (existingModule != null)
{
// 使用AutoMapper或手动赋值来更新实体
existingModule.Name = module.Name;
existingModule.ControllerName = module.ControllerName;
existingModule.ActionName = module.ActionName;
existingModule.IconClass = module.IconClass;
existingModule.SortCode = module.SortCode;
existingModule.ParentId = module.ParentId;
existingModule.IsActive = module.IsActive;
existingModule.Description = module.Description;
_moduleRepository.Update(existingModule);
_unitOfWork.SaveChanges();
}
}
public void DeleteModule(int id)
{
var module = _moduleRepository.Get(id);
if (module == null)
{
throw new System.Exception("未找到要删除的模块。");
}
// 业务规则验证:如果模块有子模块,则不允许删除
if (_moduleRepository.HasChildren(id))
{
throw new System.Exception("该模块下存在子模块,无法直接删除。请先删除或移动其子模块。");
}
_moduleRepository.Delete(module);
_unitOfWork.SaveChanges();
}
public List<Module> GetModuleTree()
{
var allModules = _moduleRepository.GetAll().OrderBy(m => m.SortCode).ToList();
// 这里可以递归构建一个更完整的树形结构,简单起见返回平铺列表用于下拉框
return allModules;
}
public bool IsModuleNameUnique(string name, int? id = null)
{
var query = _moduleRepository.Find(m => m.Name == name);
if (id.HasValue)
{
query = query.Where(m => m.Id != id.Value);
}
return !query.Any();
}
}
}说明: Service层是业务逻辑的集中地。在这里进行唯一性校验、删除前的关联性检查 等操作,是保证数据完整性的最佳实践。
4. 控制器(Controller)与视图(View)实现#
4.1 列表页(Index)#
控制器(ModuleController.cs):
using System.Web.Mvc;
using YourProjectName.Services;
namespace YourProjectName.Web.Controllers
{
[Authorize] // 需要授权访问
public class ModuleController : Controller
{
private readonly IModuleService _moduleService;
// 依赖注入(在Startup或Program.cs中配置)
public ModuleController(IModuleService moduleService)
{
_moduleService = moduleService;
}
// GET: Module
public ActionResult Index()
{
var modules = _moduleService.GetAllModules();
return View(modules);
}
// ... 其他Action方法(Create, Edit, Delete)见下文
}
}视图(Views/Module/Index.cshtml):
@model IEnumerable<YourProjectName.Models.Module>
@{
ViewBag.Title = "模块管理";
}
<h2>@ViewBag.Title</h2>
<p>
@Html.ActionLink("添加新模块", "Create", null, new { @class = "btn btn-primary" })
</p>
<table class="table table-striped table-bordered">
<tr>
<th>模块名称</th>
<th>控制器</th>
<th>动作方法</th>
<th>图标</th>
<th>排序</th>
<th>状态</th>
<th>操作</th>
</tr>
@foreach (var module in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => module.Name)</td>
<td>@Html.DisplayFor(modelItem => module.ControllerName)</td>
<td>@Html.DisplayFor(modelItem => module.ActionName)</td>
<td><i class="@module.IconClass"></i> @module.IconClass</td>
<td>@Html.DisplayFor(modelItem => module.SortCode)</td>
<td>@(module.IsActive ? "激活" : "禁用")</td>
<td>
@Html.ActionLink("编辑", "Edit", new { id = module.Id }, new { @class = "btn btn-xs btn-default" })
@Html.ActionLink("删除", "Delete", new { id = module.Id }, new { @class = "btn btn-xs btn-danger", onclick = "return confirm('确定要删除吗?');" })
</td>
</tr>
// 递归显示子模块(这里简化,实际可用部分视图递归)
if (module.ChildModules != null && module.ChildModules.Any())
{
foreach (var child in module.ChildModules.OrderBy(m => m.SortCode))
{
<tr>
<td style="padding-left: 40px;">└─ @child.Name</td>
<td>@child.ControllerName</td>
<td>@child.ActionName</td>
<td><i class="@child.IconClass"></i> @child.IconClass</td>
<td>@child.SortCode</td>
<td>@(child.IsActive ? "激活" : "禁用")</td>
<td>
@Html.ActionLink("编辑", "Edit", new { id = child.Id }, new { @class = "btn btn-xs btn-default" })
@Html.ActionLink("删除", "Delete", new { id = child.Id }, new { @class = "btn btn-xs btn-danger", onclick = "return confirm('确定要删除吗?');" })
</td>
</tr>
}
}
}
</table>4.2 添加页(Create)#
控制器(ModuleController.cs - Create Actions):
// GET: Module/Create
public ActionResult Create()
{
// 准备父模块下拉列表数据
ViewBag.ParentId = new SelectList(_moduleService.GetModuleTree(), "Id", "Name");
return View();
}
// POST: Module/Create
[HttpPost]
[ValidateAntiForgeryToken] // 防止CSRF攻击
public ActionResult Create(Module module)
{
try
{
if (ModelState.IsValid)
{
_moduleService.CreateModule(module);
TempData["Message"] = "模块添加成功!"; // 使用TempData传递成功消息
return RedirectToAction("Index");
}
}
catch (Exception ex)
{
ModelState.AddModelError("", "添加失败: " + ex.Message);
}
// 如果失败,重新绑定下拉列表
ViewBag.ParentId = new SelectList(_moduleService.GetModuleTree(), "Id", "Name", module.ParentId);
return View(module);
}视图(Views/Module/Create.cshtml):
@model YourProjectName.Models.Module
@{
ViewBag.Title = "添加模块";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ControllerName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ControllerName, new { htmlAttributes = new { @class = "form-control", placeholder = "例如:Home" } })
@Html.ValidationMessageFor(model => model.ControllerName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ActionName, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ActionName, new { htmlAttributes = new { @class = "form-control", placeholder = "例如:Index" } })
@Html.ValidationMessageFor(model => model.ActionName, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ParentId, "父级模块", htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("ParentId", null, "-- 作为顶级模块 --", htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.ParentId, "", new { @class = "text-danger" })
</div>
</div>
<!-- 其他字段:IconClass, SortCode, IsActive, Description ... -->
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="创建" class="btn btn-success" />
@Html.ActionLink("返回列表", "Index", null, new { @class = "btn btn-default" })
</div>
</div>
</div>
}4.3 编辑页(Edit)#
编辑页与添加页非常相似,主要区别在于:
GETAction需要通过ID获取现有数据。POSTAction调用的是更新服务方法。
控制器(ModuleController.cs - Edit Actions):
// GET: Module/Edit/5
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Module module = _moduleService.GetModuleById(id.Value);
if (module == null)
{
return HttpNotFound();
}
ViewBag.ParentId = new SelectList(_moduleService.GetModuleTree(), "Id", "Name", module.ParentId);
return View(module);
}
// POST: Module/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Module module)
{
try
{
if (ModelState.IsValid)
{
_moduleService.UpdateModule(module);
TempData["Message"] = "模块修改成功!";
return RedirectToAction("Index");
}
}
catch (Exception ex)
{
ModelState.AddModelError("", "修改失败: " + ex.Message);
}
ViewBag.ParentId = new SelectList(_moduleService.GetModuleTree(), "Id", "Name", module.ParentId);
return View(module);
}4.4 删除操作(Delete)#
最佳实践: 删除操作务必使用POST(或DELETE)请求,避免通过GET请求直接删除,以防止CSRF攻击和搜索引擎误操作。
控制器(ModuleController.cs - Delete Actions):
// GET: Module/Delete/5 - 用于确认删除的页面
public ActionResult Delete(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Module module = _moduleService.GetModuleById(id.Value);
if (module == null)
{
return HttpNotFound();
}
// 再次检查业务规则,提示用户
if (_moduleService.HasChildren(id.Value))
{
ViewBag.ErrorMessage = "该模块下存在子模块,无法删除。";
}
return View(module);
}
// POST: Module/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
try
{
_moduleService.DeleteModule(id);
TempData["Message"] = "模块删除成功!";
}
catch (Exception ex)
{
TempData["ErrorMessage"] = "删除失败: " + ex.Message;
return RedirectToAction("Delete", new { id = id }); // 失败则返回确认页
}
return RedirectToAction("Index");
}5. 最佳实践与常见问题#
最佳实践总结:
- 分层架构: 严格遵循Controller-Service-Repository分层,职责分离,便于测试和维护。
- 依赖注入(DI): 使用ASP.NET Core内置的DI容器,解耦各层组件。
- 输入验证: 前后端同时验证。后端使用数据注解,前端使用jQuery Unobtrusive Validation。
- 防CSRF攻击: 在所有修改数据的
POSTAction上使用[ValidateAntiForgeryToken]。 - 异常处理: 在Service层抛出业务异常,在Controller层捕获并友好地提示用户。
- 用户体验: 使用
TempData传递操作结果消息,删除前进行确认。
常见问题(FAQ):
- Q: 如何实现更优美的树形显示?
- A: 可以使用部分视图(Partial View)进行递归渲染,或者使用前端组件如jQuery Treeview、jsTree等。
- **Q: 模块删除时,如何实现“软删除”?
- A: 在
Module模型中添加一个IsDeleted布尔字段。删除时并非真正从数据库移除,而是标记为已删除。查询时过滤掉IsDeleted == true的记录。
- A: 在
- **Q: 模块的
ControllerName和ActionName如何管理?- A: 可以创建一个常量类来统一管理,避免拼写错误。或者通过反射程序集自动扫描所有Controller和Action,供选择。
6. 总结#
本章我们完整地实现了ASP.NET MVC框架中的模块管理功能,涵盖了从模型设计、数据访问、业务逻辑到控制器和视图的整个流程。我们不仅实现了功能,更融入了分层架构、依赖注入、输入验证、异常处理等企业级开发的最佳实践。
模块管理是权限系统的核心数据源。有了它,我们的动态菜单才能正常工作。在接下来的章节中,我们将以此为基础,深入实现角色管理和用户权限分配,敬请期待!
参考资料#
- ASP.NET Core 官方文档 - MVC
- Entity Framework Core 官方文档
- Repository Pattern in ASP.NET MVC
- jQuery Validation Plugin
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。