上一章,我們實現了用戶的註冊和登錄,登錄之後展示的是我們的主頁,頁面的左側是多級的導航菜單,定位並展示用戶需要訪問的不同頁面。目前導航菜單是寫死的,考慮以後菜單管理的便捷性,我們這節實現下可視化配置菜單的功能,這樣以後我們可以動態的配置導航菜單,不用再編譯發佈網站程式了。 增加後臺管理模塊 第1步, ...
上一章,我們實現了用戶的註冊和登錄,登錄之後展示的是我們的主頁,頁面的左側是多級的導航菜單,定位並展示用戶需要訪問的不同頁面。目前導航菜單是寫死的,考慮以後菜單管理的便捷性,我們這節實現下可視化配置菜單的功能,這樣以後我們可以動態的配置導航菜單,不用再編譯發佈網站程式了。
增加後臺管理模塊
第1步,左側導航菜單中,添加後臺管理模塊,用作管理員登錄後,可以進行一些後臺管理的操作,當然,目前還沒有許可權控制(後期加入),所以對所有用戶可見。大概菜單結構如下:
有了菜單項,我們還需要控制視圖的跳轉,所以,接下來需要寫對應的控制器和視圖。
為了將相關功能組織成一組單獨命名空間(路由)和文件夾結構(視圖),解決方案中右鍵添加區域(Area),取名後臺管理(Configuration),代表後臺管理模塊,.Net Core腳手架(scaffold)自動幫我們實現了目錄劃分:控制器(Controllers)、模型(Models)、視圖(Views)
菜單模型定義
菜單的基本屬性有:菜單名稱、菜單類型、菜單的圖標樣式、菜單url路徑。另外,菜單在邏輯上是樹狀結構,但是要在物理資料庫中存儲,需要進行扁平化處理,每個菜單項有個父菜單屬性(根節點的父菜單為空),還有同一父節點底下,在組類的排序屬性,定義如下:
/// <summary> /// 菜單 /// </summary> public class Menu { /// <summary> /// 主鍵ID /// </summary> [DatabaseGenerated(DatabaseGeneratedOption.None)] [Required(ErrorMessage = "請輸入菜單編號")] public string Id { get; set; } /// <summary> /// 菜單名稱 /// </summary> [Required(ErrorMessage = "請輸入菜單名稱")] [StringLength(256)] public string Name { get; set; } /// <summary> /// 父級ID /// </summary> [DisplayFormat(NullDisplayText = "無")] public string ParentId { get; set; } /// <summary> /// 菜單組內排序 /// </summary> [Range(0, 99, ErrorMessage = "請選擇1-99範圍內的整數")] public int IndexCode { get; set; } /// <summary> /// 菜單路徑 /// </summary> [StringLength(256)] [DisplayFormat(NullDisplayText = "無")] public string Url { get; set; } /// <summary> /// 類型:0導航菜單;1操作按鈕。 /// </summary> [Required(ErrorMessage = "請選擇菜單類型")] public MenuTypes? MenuType { get; set; } /// <summary> /// 菜單圖標名稱 /// </summary> [Required(ErrorMessage = "請輸入菜單圖標")] [StringLength(50)] public string Icon { get; set; } /// <summary> /// 菜單備註 /// </summary> public string Remarks { get; set; } } /// <summary> /// 菜單類型 /// </summary> public enum MenuTypes { /// <summary> /// 導航菜單 /// </summary> 導航菜單, /// <summary> /// 操作菜單 /// </summary> 操作菜單 }View Code
有了我們的菜單模型,在控制器目錄中,我們右鍵建立第1個自己的控制器,取名MenuController,用來菜單管理,上下文選取定義好的Menu模型,還是利用腳手架,自動幫我們生成增刪改查對應的後來邏輯和視圖。此時,我們把菜單導向該控制器,其實是可以正常訪問的,不過還遠遠達不到我們的要求,所以我們還得完善下自動生成的代碼。
菜單控制器改寫
為了方便今後的拓展,新增一個AppController控制器,繼承Controller,以後所有的控制器,都繼承於AppController,方便一些公共的方法調用。
.Net Core有個比較方便的一點,就是實現了構造器的依賴註入,這樣我們不用像以前那樣手工New一個DBContext對象,直接在控制器將需要的DBContext註入,調用的時候,直接訪問註入的對象即可,有關依賴註入的知識,這裡就不在多說了,有興趣大家可以瞭解一下:.Net Core依賴註入
首先,在ApplicationDbContext添加Menu數據集
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); } public DbSet<ApplicationUser> ApplicationUsers { get; set; } public DbSet<Menu> Menus { get; set; } }View Code
這裡我們修改下MenuController構造器:
private readonly ApplicationDbContext _context; public MenuController(ApplicationDbContext context, INavMenuService navMenuService) { _context = context; _NavMenuService = navMenuService; }View Code
為了後面方便統一提供下拉框選擇,這裡實現一個下拉框初始化方法:
/// <summary> /// 初始化下拉選擇框 /// </summary> /// <param name="menu"></param> private void UpdateDropDownList(Menu menu = null) { var menusParent = _context.Menus.AsNoTracking().Where(s => s.MenuType == MenuTypes.導航菜單); List<SelectListItem> listMenusParent = new List<SelectListItem>(); foreach (var menuParent in menusParent) { listMenusParent.Add(new SelectListItem { Value = menuParent.Id, Text = menuParent.Id + $"({menuParent.Name})", Selected = (menu != null && menuParent.Id == menu.ParentId) }); } ViewBag.ParentIds = listMenusParent; if (menu == null) { ViewBag.MenuTypes = MenuTypes.導航菜單.GetSelectListByEnum(); } else { ViewBag.MenuTypes = MenuTypes.導航菜單.GetSelectListByEnum(Convert.ToInt32(menu.MenuType)); } }View Code
列表頁改寫
控制器調整:增加查詢傳入參數,根據參數篩選查詢結果;
/// <summary> /// 列表頁 /// </summary> /// <param name="query"></param> /// <returns></returns> public async Task<IActionResult> Index(MenuIndexQuery query) { var menus = _context.Menus.AsNoTracking(); if (!string.IsNullOrEmpty(query.QName)) { menus= menus.Where(s => s.Name.Contains(query.QName.Trim())); } if (!string.IsNullOrEmpty(query.QId)) { menus = menus.Where(s => s.Id.Contains(query.QId.Trim())); } if (!string.IsNullOrEmpty(query.QParentId)) { menus = menus.Where(s => s.ParentId == query.QParentId.Trim()); } if (query.QMenuType != null) { menus = menus.Where(s => s.MenuType == query.QMenuType); } UpdateDropDownList(); return View(new MenuIndexVM { Menus = await menus.ToListAsync(), Query = query }); }View Code
視圖調整:用戶點擊刪除時,彈出確認框,調用Ajax方式刪除數據,不再通過頁面跳轉;
@using MyWebSite.ViewModels @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @model MyWebSite.Areas.Configuration.ViewModels.MenuIndexVM @{ ViewData["Title"] = "菜單列表"; var breadcrumb = new BreadCrumb("菜單列表", "Version 2.0", new List<NavCrumb> { new NavCrumb(name:"菜單管理",url: "/Configuration/Menu"), new NavCrumb(name:"菜單列表"), }); ViewBag.BreadCrumb = breadcrumb; Layout = "~/Views/Shared/_Layout.cshtml"; } <div class="row"> <div class="col-xs-12"> <div class="box with-border"> <form class="form" asp-action="Index"> <div class="box-header"> <h3 class="box-title"><i class="fa fa-search margin-r-5">查詢條件</i></h3> <div class="box-tools pull-right"> <button type="submit" class="btn btn-success margin-r-5"><i class="fa fa-search margin-r-5"></i>查詢</button> <a class="btn btn-primary" href="/Configuration/Menu/Create"><i class="fa fa-plus margin-r-5"></i>新建</a> </div> <div asp-validation-summary="All" class="text-danger"></div> <div class="row"> <div class="col-md-3"> <div class="form-group"> <label asp-for="Query.QName">菜單名稱:</label> <input asp-for="Query.QName" class="form-control input-sm"> </div> </div> <div class="col-md-3"> <div class="form-group"> <label asp-for="Query.QId">菜單編碼:</label> <input asp-for="Query.QId" class="form-control input-sm"> </div> </div> <div class="col-md-3"> <div class="form-group"> <label asp-for="Query.QParentId">父級菜單:</label> <select asp-for="Query.QParentId" class="form-control input-sm select2" asp-items="ViewBag.ParentIds"> <option value="">-- 請選擇 --</option> </select> </div> </div> <div class="col-md-3"> <div class="form-group"> <label asp-for="Query.QMenuType">菜單類型:</label> <select asp-for="Query.QMenuType" class="form-control input-sm select2" asp-items="ViewBag.MenuTypes"> <option value="">-- 請選擇 --</option> </select> </div> </div> </div> </div> </form> <div class="box-body"> <table class="table table-bordered table-hover" style="width: 100%"> <thead> <tr> <th>#</th> <th>菜單名稱</th> <th>菜單編號</th> <th>父級編號</th> <th>組內排序</th> <th>菜單類型</th> <th>菜單圖標</th> <th>菜單路徑</th> <th>操作</th> </tr> </thead> <tbody> @{ var index = 0; } @foreach (var item in Model.Menus) { index++; <tr> <td> @index.ToString("D3") </td> <td> @Html.ActionLink(@item.Name, "Details", new { id = @item.Id }) </td> <td> <span>@item.Id</span> </td> <td> <span>@Html.DisplayFor(modelItem => item.ParentId)</span> </td> <td> <span>@item.IndexCode</span> </td> <td> <span>@item.MenuType</span> </td> <td> <i class="fa @item.Icon" data-toggle="tooltip" data-placement="right" title="@item.Icon"></i> </td> <td> <i class="fa fa-ellipsis-h" data-toggle="tooltip" data-placement="top" title="@Html.DisplayFor(modelItem => item.Url)"></i> </td> <td> @Html.ActionLink("編輯", "Edit", new { id = @item.Id })| @Html.ActionLink("詳情", "Details", new { id = @item.Id })| <a href="#" onclick="onDelete('@item.Id', '@item.Name');">刪除</a> </td> </tr> } </tbody> </table> </div> </div> </div> </div> @section Scripts{ @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} <script> function onDelete(id, name) { BootstrapDialog.show({ message: '確認刪除菜單-' + name + '[' + id + ']?', size: BootstrapDialog.SIZE_SMALL, draggable: true, buttons: [ { icon: 'fa fa-check', label: '確定', cssClass: 'btn-primary', action: function (dialogRef) { dialogRef.close(); $.ajax({ type: 'POST', url: '/Configuration/Menu/Delete', data: { id: id }, success: function () { location.reload(); } }); } }, { icon: 'fa fa-close', label: '取消', action: function (dialogRef) { dialogRef.close(); } } ] }); } </script> }View Code
新建頁改寫
控制器調整:這裡控制器有2個Create方法,一個是Http Get類型,用戶列表頁點新建時,跳轉到該方法,另外一個是Http Post類型,用戶填完新建的菜單信息後,點擊保存,跳轉到該方法。在Http Post方法中,為了防止頁面over post,需要指定綁定的屬性Bind("Id,Name,ParentId,IndexCode,Url,MenuType,Icon,Remarks"),當然,也可以用TryUpdateModel()實現,以後再介紹;
/// <summary> /// 新建空白頁面 /// </summary> /// <returns></returns> public IActionResult Create() { var model = new Menu { Id = "MXX_XX_XX", IndexCode = 1, Icon = "fa-circle-o" }; UpdateDropDownList(); return View(model); } /// <summary> /// 新建保存頁面 /// </summary> /// <param name="menu"></param> /// <returns></returns> [HttpPost] public async Task<IActionResult> Create([Bind("Id,Name,ParentId,IndexCode,Url,MenuType,Icon,Remarks")] Menu menu) { if (ModelState.IsValid) { if (!MenuExists(menu.Id)) { _context.Add(menu); await _context.SaveChangesAsync(); _NavMenuService.InitOrUpdate(); return RedirectToAction(nameof(Index)); } else { ModelState.AddModelError("Id", "菜單編號已存在,請修改菜單編號."); } } UpdateDropDownList(menu); return View(menu); }View Code
視圖調整: 引入前端數據驗證,並增加一些數據控制,比如菜單類型非操作菜單時,菜單路徑不可編輯等等;
@using MyWebSite.ViewModels @using MyWebSite.Areas.Configuration.Models @model MyWebSite.Areas.Configuration.Models.Menu @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "菜單新建"; var breadcrumb = new BreadCrumb("菜單新建", "Version 2.0", new List<NavCrumb> { new NavCrumb(name:"菜單管理",url: "/Configuration/Menu"), new NavCrumb(name:"菜單新建"), }); ViewBag.BreadCrumb = breadcrumb; Layout = "~/Views/Shared/_Layout.cshtml"; } <section class="content"> <div class="row"> <div class="col-md-8"> <div class="box"> <div class="box-header with-border"> <h3 class="box-title">新建</h3> </div> <form asp-action="Create"> <div asp-validation-summary="All" class="text-danger"></div> <div class="box-body"> <div class="form-group col-md-6"> <label asp-for="Id">菜單編號</label> <input asp-for="Id" class="form-control input-sm"> </div> <div class="form-group col-md-6"> <label asp-for="Name">菜單名稱</label> <input asp-for="Name" class="form-control input-sm"> </div> <div class="form-group col-md-6"> <label asp-for="ParentId">父級菜單</label> <select asp-for="ParentId" class="form-control input-sm select2" asp-items="ViewBag.ParentIds"> <option value="">-- 請選擇 --</option> </select> </div> <div class="form-group col-md-6"> <label asp-for="IndexCode">組內排序</label> <input asp-for="IndexCode" class="form-control input-sm"> </div> <div class="form-group col-md-6"> <label asp-for="MenuType">菜單類型</label> <select asp-for="MenuType" class="form-control input-sm" asp-items="ViewBag.MenuTypes"> <option value="">-- 請選擇 --</option> </select> </div> <div class="form-group col-md-6"> <label asp-for="Icon">菜單圖標</label> <div class="input-group"> <span class="input-group-addon"><i id="IconfShow" class="fa @Model.Icon"></i></span> <input asp-for="Icon" class="form-control input-sm"> </div> </div> <div class="form-group col-md-6"> <label asp-for="Url">菜單路徑</label> @if (Model.MenuType == MenuTypes.操作菜單) { <input asp-for="Url" class="form-control input-sm"> } else { <input asp-for="Url" class="form-control input-sm" readonly> } </div> <div class="form-group col-md-6"> <label asp-for="Remarks">備註</label> <input asp-for="Remarks" class="form-control input-sm"> </div> </div> <div class="box-footer"> <button type="submit" class="btn btn-primary"><i id="IconfShow" class="fa fa-save"></i> 保存</button> <a asp-action="Index" class="btn btn-default"><i id="IconfShow" class="fa fa-undo"></i> 返回</a> </div> </form> </div> </div> </div> </section> @section Scripts { <script src="~/js/Configuration/Menu.js"></script> @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }View Code
詳情頁改寫
控制器調整:不用大的調整,只是增加了下拉框的初始化工作 ;
/// <summary> /// 詳情頁 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<IActionResult> Details(string id) { if (id == null) { return