【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目(7.2) 模块管理,模块的添加、修改、删除

各位朋友,大家好!我们继续我们的ASP.NET MVC框架搭建之旅。在上一章节中,我们完成了菜单动态加载,其数据来源正是我们今天要深入探讨的核心——模块(Module)

模块管理是任何后台管理系统的基石。它定义了系统的功能结构,是权限控制的载体。一个设计良好的模块管理功能,能够为后续的权限分配、角色管理打下坚实的基础。本篇将详细介绍如何从零开始,实现模块的添加、修改、删除 这三个核心操作,并融入一些最佳实践。

目录#

  1. 核心概念与模型设计
  2. 数据访问层(Repository)实现
  3. 业务逻辑层(Service)实现
  4. 控制器(Controller)与视图(View)实现
  5. 最佳实践与常见问题
  6. 总结
  7. 参考资料

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 时,表示该模块是顶级模块(根菜单)。
  • 导航属性 ParentModuleChildModules 方便我们使用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)#

编辑页与添加页非常相似,主要区别在于:

  1. GET Action需要通过ID获取现有数据。
  2. POST Action调用的是更新服务方法。

控制器(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. 最佳实践与常见问题#

最佳实践总结:

  1. 分层架构: 严格遵循Controller-Service-Repository分层,职责分离,便于测试和维护。
  2. 依赖注入(DI): 使用ASP.NET Core内置的DI容器,解耦各层组件。
  3. 输入验证: 前后端同时验证。后端使用数据注解,前端使用jQuery Unobtrusive Validation。
  4. 防CSRF攻击: 在所有修改数据的POST Action上使用[ValidateAntiForgeryToken]
  5. 异常处理: 在Service层抛出业务异常,在Controller层捕获并友好地提示用户。
  6. 用户体验: 使用TempData传递操作结果消息,删除前进行确认。

常见问题(FAQ):

  • Q: 如何实现更优美的树形显示?
    • A: 可以使用部分视图(Partial View)进行递归渲染,或者使用前端组件如jQuery Treeview、jsTree等。
  • **Q: 模块删除时,如何实现“软删除”?
    • A:Module模型中添加一个IsDeleted布尔字段。删除时并非真正从数据库移除,而是标记为已删除。查询时过滤掉IsDeleted == true的记录。
  • **Q: 模块的ControllerNameActionName如何管理?
    • A: 可以创建一个常量类来统一管理,避免拼写错误。或者通过反射程序集自动扫描所有Controller和Action,供选择。

6. 总结#

本章我们完整地实现了ASP.NET MVC框架中的模块管理功能,涵盖了从模型设计、数据访问、业务逻辑到控制器和视图的整个流程。我们不仅实现了功能,更融入了分层架构、依赖注入、输入验证、异常处理等企业级开发的最佳实践

模块管理是权限系统的核心数据源。有了它,我们的动态菜单才能正常工作。在接下来的章节中,我们将以此为基础,深入实现角色管理用户权限分配,敬请期待!

参考资料#

  1. ASP.NET Core 官方文档 - MVC
  2. Entity Framework Core 官方文档
  3. Repository Pattern in ASP.NET MVC
  4. jQuery Validation Plugin

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。