|
入门实战-文章管理之文章类别的管理
如果要做一个CMS系统,那么文章管理算是入门,文章管理附带一个类别管理,用来对文章进行类别区分。所以,本章简单讲一些类别管理,这也是一个数据操作。
(1).文章类别Sql表的建立

CREATE TABLE [dbo].[ArticleCategory](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Title] [varchar](128) NOT NULL,
[ParentId] [int] NOT NULL,
[ClassList] [varchar](128) NULL,
[ClassLayer] [int] NULL,
[Sort] [int] NOT NULL,
[ImageUrl] [varchar](128) NULL,
[SeoTitle] [varchar](128) NULL,
[SeoKeywords] [varchar](256) NULL,
[SeoDescription] [varchar](512) NULL,
[IsDeleted] [bit] NOT NULL,
CONSTRAINT [PK_ARTICLECATEGORY] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO(2).文章类别的视图

(2.1)Create视图


@{ ViewData["Title"] = "新建文章分类"; }
@model ArticleCategory
<form action=&#34;/ArticleCategory/Create&#34; method=&#34;post&#34;>
@Html.AntiForgeryToken()
<div>
<label asp-for=&#34;ParentId&#34;>父类别</label>
<div>
<label name=&#34;Parent_DisplayName&#34; id=&#34;Parent_DisplayName&#34;>@ViewData[&#34;DisplayName&#34;]</label>
<input type=&#34;hidden&#34; asp-for=&#34;ParentId&#34; id=&#34;ParentId&#34; name=&#34;ParentId&#34; value=&#34;@ViewData[&#34;ParentId&#34;]&#34; />
</div>
</div>
<div>
<label asp-for=&#34;Title&#34;>类别名称</label>
<div>
<input type=&#34;text&#34; asp-for=&#34;Title&#34; name=&#34;Title&#34; placeholder=&#34;请输入类别名&#34;>
</div>
</div>
<div>
<label asp-for=&#34;Sort&#34;>排序</label>
<div>
<input type=&#34;text&#34; asp-for=&#34;Sort&#34; name=&#34;Sort&#34; placeholder=&#34;排序&#34;>
</div>
</div>
<div>
<div>
<button type=&#34;submit&#34;>确定</button>
<button type=&#34;reset&#34;>重置</button>
</div>
</div>
</form>(2.2)Edit视图

@{ ViewData[&#34;Title&#34;] = &#34;编辑菜单&#34;; }
@model ArticleCategory
<form action=&#34;/ArticleCategory/Edit&#34; method=&#34;post&#34;>
@Html.AntiForgeryToken()
<div>
<label asp-for=&#34;ParentId&#34;>父菜单</label>
<div>
<input type=&#34;text&#34; asp-for=&#34;ParentId&#34; name=&#34;ParentId&#34; />
<input type=&#34;hidden&#34; asp-for=&#34;Id&#34; />
</div>
</div>
<div>
<label asp-for=&#34;Title&#34;>类别名成</label>
<div>
<input type=&#34;text&#34; asp-for=&#34;Title&#34; name=&#34;Title&#34; placeholder=&#34;类别名成&#34;>
</div>
</div>
<div>
<label asp-for=&#34;Sort&#34;>排序</label>
<div>
<input type=&#34;text&#34; asp-for=&#34;Sort&#34; name=&#34;Sort&#34; placeholder=&#34;排序&#34;>
</div>
</div>
<div>
<div>
<button type=&#34;submit&#34;>确定</button>
<button type=&#34;reset&#34;>重置</button>
</div>
</div>
</form>(2.3)Index列表视图

单纯的列表在现实父类名称时,显示的还是Id值,看第一步的表结构可以发现,这个字段是ParentId,我们得显示成名称才行。
@using Humanizer;
@using RjWebCms.Db;
@using RjWebCms.Models.Articles;
@using RjWebCms.Common;
@model PaginatedList<ArticleCategoryView>
@{
ViewData[&#34;Title&#34;] = &#34;文章类别列表&#34;;
}
@section Scripts{
<script src=&#34;~/js/jquery-2.1.0.min.js&#34;></script>
<script type=&#34;text/javascript&#34;>
function DelAll() {
var ids = document.getElementsByName(&#34;#chk_ids&#34;);
var arrIds = &#34;&#34;;
var n = 0;
for (var i = 0; i < ids.length; i++) {
if (ids.checked == true) {
arrIds += ids.value + &#34;,&#34;;
n++;
}
}
if (n == 0) {
alert(&#34;请选择要删除的信息&#34;);
return;
}
arrIds = arrids.substr(0, arrIds.length - 1);
//
if (confirm(&#34;确定要全部删除选择信息吗&#34;)) {
$.ajax({
type: &#34;post&#34;,
url: &#34;/ArticleCategory/DeleteAll&#34;,
data: { ids: arrIds },
success: function (data, state) {
alert(&#39;删除成功!&#39;);
window.location.href = &#34;&#34;;
},
error: function (data, state) {
alert(&#39;删除失败&#39;);
}
});
}
}
</script>
}
<div class=&#34;panel panel-default todo-panel&#34;>
<div class=&#34;panel-heading&#34;>@ViewData[&#34;Title&#34;]</div>
@Html.AntiForgeryToken()
<form asp-action=&#34;Index&#34; method=&#34;get&#34;>
<table>
<tr><td><a asp-controller=&#34;ArticleCategory&#34; asp-action=&#34;Create&#34;>添加</a></td></tr>
<tr>
<td>查询关键词:<input type=&#34;text&#34; name=&#34;SearchString&#34; value=&#34;@ViewData[&#34;CurrentFilter&#34;]&#34; /></td>
<td><input type=&#34;submit&#34; value=&#34;查询&#34; /></td>
<td><a asp-action=&#34;Index&#34;>Back</a></td>
<td><a asp-action=&#34;DeleteAll&#34;>批量删除</a></td>
</tr>
</table>
</form>
<table class=&#34;table table-hover&#34;>
<thead>
<tr>
<td>&#x2714;</td>
<td><a asp-action=&#34;Index&#34; asp-route-sortOrder=&#34;@ViewData[&#34;NameSortParm&#34;]&#34; asp-route-currentFilter=&#34;@ViewData[&#34;CurrentFilter&#34;]&#34;>类别名称</a></td>
<td>父类名称</td>
<td><a asp-action=&#34;Index&#34; asp-route-sortOrder=&#34;@ViewData[&#34;DateSortParm&#34;]&#34; asp-route-currentFilter=&#34;@ViewData[&#34;CurrentFilter&#34;]&#34;>序号</a></td>
<td>操作</td>
</tr>
@foreach (var item in Model)
{
if (item.ParentId == 0)
{
<tr>
<td><input type=&#34;checkbox&#34; class=&#34;done-checkbox&#34; name=&#34;chk_ids&#34; value=&#34;@item.Id&#34;></td>
<td>@item.Title</td>
<td>\</td>
<td>@item.Sort</td>
<td>
<a asp-action=&#34;Create&#34; asp-route-id=&#34;@item.Id&#34;>Add</a>
<a asp-action=&#34;Edit&#34; asp-route-id=&#34;@item.Id&#34;>Edit</a>
<a asp-action=&#34;Delete&#34; asp-route-id=&#34;@item.Id&#34;>Delete</a>
</td>
</tr>
}
else
{
<tr>
<td><input type=&#34;checkbox&#34; class=&#34;done-checkbox&#34; name=&#34;chk_ids&#34; value=&#34;@item.Id&#34;></td>
<td>@item.Title</td>
@*<td>@Html.Action(&#34;GetParentName&#34;,new { id=item.ParentId})</td>*@
<td>@item.ParentName</td>
<td>@item.Sort</td>
<td>
<a asp-action=&#34;Create&#34; asp-route-id=&#34;@item.Id&#34;>Add</a>
<a asp-action=&#34;Edit&#34; asp-route-id=&#34;@item.Id&#34;>Edit</a>
<a asp-action=&#34;Delete&#34; asp-route-id=&#34;@item.Id&#34;>Delete</a>
</td>
</tr>
}
}
</thead>
</table>
@{
var prevDisabled = !Model.HasPreviousPage ? &#34;disabled&#34; : &#34;&#34;;
var nextDisabled = !Model.HasNextPage ? &#34;disabled&#34; : &#34;&#34;; ;
}
<a asp-action=&#34;Index&#34;
asp-route-sortOrder=&#34;@ViewData[&#34;CurrentSort&#34;]&#34;
asp-route-pageNumber=&#34;@(Model.PageIndex - 1)&#34;
asp-route-currentFilter=&#34;@ViewData[&#34;CurrentFilter&#34;]&#34;
class=&#34;btn btn-default @prevDisabled&#34;>
上一页
</a>
<a asp-action=&#34;Index&#34;
asp-route-sortOrder=&#34;@ViewData[&#34;CurrentSort&#34;]&#34;
asp-route-pageNumber=&#34;@(Model.PageIndex + 1)&#34;
asp-route-currentFilter=&#34;@ViewData[&#34;CurrentFilter&#34;]&#34;
class=&#34;btn btn-default @nextDisabled&#34;>
下一页
</a>
<div class=&#34;panel-footer add-item-form&#34;>
<!-- TODO: Add item form -->
</div>
</div>所以,需要编写ViewModel是实现,这在前面章节中详细讲过。

(3).Model对象的编码实现。

(3.1)表的直接映射对象ArticleCategory.cs
/// <summary>
/// 文章类别
/// </summary>
public class ArticleCategory
{
/// <summary>
/// 主键
/// </summary>
[Key]
public Int32 Id { get; set; }
/// <summary>
/// 分类标题
/// </summary>
[Required]
public String Title { get; set; }
/// <summary>
/// 父分类ID
/// </summary>
[Required]
public Int32 ParentId { get; set; }
/// <summary>
/// 类别ID列表(逗号分隔开)
/// </summary>
public String ClassList { get; set; }
/// <summary>
/// 类别深度
/// </summary>
public Int32? ClassLayer { get; set; }
/// <summary>
/// 排序
/// </summary>
[Required]
public Int32 Sort { get; set; }
/// <summary>
/// 分类图标
/// </summary>
public String ImageUrl { get; set; }
/// <summary>
/// 分类SEO标题
/// </summary>
public String SeoTitle { get; set; }
/// <summary>
/// 分类SEO关键字
/// </summary>
public String SeoKeywords { get; set; }
/// <summary>
/// 分类SEO描述
/// </summary>
public String SeoDescription { get; set; }
/// <summary>
/// 是否删除
/// </summary>
[Required]
public Boolean IsDeleted { get; set; }
}(3.2). ArticleCategoryView.cs 也就是文章类别列表页的ViewModel代码
public class ArticleCategoryView
{
public int Id { get; set; }
public string Title { get; set; }
public int ParentId { get; set; }
public string ParentName { get; set; }
public int Sort { get; set; }
}(4).Controller的编码实现
public class ArticleCategoryController : Controller
{
private readonly IArticleCategoryService _articleCategoryService;
private readonly AppDbContext _appDbContext;
public ArticleCategoryController(IArticleCategoryService articleCategoryService, AppDbContext appDbContext)
{
_appDbContext = appDbContext;
_articleCategoryService = articleCategoryService;
}
public async Task<IActionResult> Index(string sortOrder, string currentFilter, string searchString, int? pageNumber)
{
ViewData[&#34;CurrentSort&#34;] = sortOrder;
ViewData[&#34;NameSortParm&#34;] = String.IsNullOrEmpty(sortOrder) ? &#34;name_desc&#34; : &#34;&#34;;
ViewData[&#34;DateSortParm&#34;] = sortOrder == &#34;sort&#34; ? &#34;sort_desc&#34; : &#34;sort&#34;;
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData[&#34;CurrentFilter&#34;] = searchString;
var articleCategories = from s in _appDbContext.ArticleCategory.OrderBy(s=>s.Id)
join p in _appDbContext.ArticleCategory on s.ParentId equals p.Id into T1
from m in T1.DefaultIfEmpty()
select new ArticleCategoryView
{
Id = s.Id,
Title = s.Title,
ParentId = s.ParentId,
ParentName = m.Title,
Sort = s.Sort
};
if (!string.IsNullOrEmpty(searchString))
{
articleCategories = articleCategories.Where(s => s.Title.Contains(searchString));
}
switch (sortOrder)
{
case &#34;name_desc&#34;:
articleCategories = articleCategories.OrderByDescending(s => s.Title);
break;
case &#34;sort&#34;:
articleCategories = articleCategories.OrderBy(s => s.Sort);
break;
case &#34;sort_desc&#34;:
articleCategories = articleCategories.OrderByDescending(s => s.Sort);
break;
default:
articleCategories = articleCategories.OrderBy(s => s.Title);
break;
}
int pageSize = 4;
return View(await PaginatedList<ArticleCategoryView>.CreateAsync(articleCategories.AsNoTracking(), pageNumber ?? 1, pageSize));
}
[HttpGet]
public async Task<IActionResult> Create(int id)
{
if (id != 0)
{
var articleCategory = await _articleCategoryService.FindArticleCategoryAsync(id);
if (articleCategory != null)
{
ViewData[&#34;DisplayName&#34;] = articleCategory.Title;
ViewData[&#34;ParentId&#34;] = articleCategory.Id; //增加子目录时,以id作为子目录的parentid
}
}
else
{
ViewData[&#34;DisplayName&#34;] = &#34;顶级根目录&#34;;
ViewData[&#34;ParentId&#34;] = 0;
}
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(int id, ArticleCategory articleCategory)
{
//去掉对字段IsSystem的验证,IsSystem在数据库是bool类型,而前端是0和1,ModelState的验证总是报false,所以去掉对其验证
//ModelState.Remove(&#34;IsSystem&#34;);//在View端已经解决了了bool类型,那么此行代码可以不用
if (id != 0)
articleCategory.ParentId = id;
if (ModelState.IsValid)
{
var successful = await _articleCategoryService.AddArticleCategoryAysnc(articleCategory);
if (successful)
return RedirectToAction(&#34;Index&#34;);
else
return BadRequest(&#34;失败&#34;);
}
return View(articleCategory);
}
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
if (string.IsNullOrEmpty(id.ToString()))
return NotFound();
var articleCategory = await _articleCategoryService.FindArticleCategoryAsync(id);
if (articleCategory == null)
return NotFound();
return View(articleCategory);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, ArticleCategory articleCategory)
{
if (id != articleCategory.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
var result = await _articleCategoryService.UpdateArticleCategoryAsync(id, articleCategory);
}
catch (Exception ex)
{
return BadRequest(&#34;编辑失败&#34;);
}
return RedirectToAction(&#34;Index&#34;);
}
return View(articleCategory);
}
[HttpGet]
public async Task<IActionResult> Delete(int id)
{
var result = await _articleCategoryService.DeleteArticleCategoryAsync(id);
if (result)
return RedirectToAction(&#34;Index&#34;);
else
return BadRequest(&#34;删除失败&#34;);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteAll(string[] ids)
{
int countSuccessDel = 0;//记录删除成功的数据条数
int countFailedDel = 0;//记录删除成功的数据条数
foreach (string id in ids)
{
if (await _articleCategoryService.DeleteArticleCategoryAsync(int.Parse(id)))
countSuccessDel++;
else
countFailedDel++;
}
if (countSuccessDel > 0)
return RedirectToAction(&#34;Index&#34;);
else
return BadRequest(&#34;删除失败&#34;);
}
public async Task<IActionResult> Details(int id)
{
if (string.IsNullOrEmpty(id.ToString()))
return NotFound();
var articleCategory = await _articleCategoryService.FindArticleCategoryAsync(id);
if (articleCategory == null)
return NotFound();
return View(articleCategory);
}
[HttpGet]
public async Task<IActionResult> GetCategory()
{
var items = await _articleCategoryService.GetArticleCategory();
return View(items);
}
public IActionResult GetParentName(int id)
{
var category = _appDbContext.ArticleCategory.Where(x => x.Id == id).FirstOrDefault();
if (category != null)
return Content(category.Title);
else
return Content(&#34;&#34;);
}
}注意,在Index的Action中的我用了个简单的Linq关联查询

因为,父Id与Id都是存在一张表中,关联子查询在使用时要用到:from m in T1.DefaultIfEmpty() 这一句,可以将空也计算在内,否则,会排除掉最前面2行(下图),我的本意sql语句是:
Select a.Id,a.Title,a.parentId,b.Title as ParentName,a.sort from ArticleCategory a
Left join ArticleCategory b on a.ParentId = b.Id ,这样可以将父Id的名称作为一个新列放在查询表中。

要注意在关键地方的改变,其他内容与将菜单功能时类似。

(5).服务层代码实现
(5.1)接口代码
public interface IArticleCategoryService
{
Task<ArticleCategory[]> GetArticleCategory();
Task<ArticleCategory[]> GetArticleCategoryByParentId(int pId);
Task<ArticleCategory> FindArticleCategoryAsync(int Id);
Task<bool> AddArticleCategoryAysnc(ArticleCategory menu);
Task<bool> UpdateArticleCategoryAsync(int id, ArticleCategory menu);
Task<bool> DeleteArticleCategoryAsync(int Id);
}(5.2)实现代码
public class ArticleCategoryService : IArticleCategoryService
{
private readonly AppDbContext _appDbContext;
public ArticleCategoryService(AppDbContext appDbContext)
{
_appDbContext = appDbContext;
}
public async Task<bool> AddArticleCategoryAysnc(ArticleCategory articleCategory)
{
articleCategory.IsDeleted = false;
if (articleCategory.ParentId == 0)
articleCategory.ClassLayer = 1;
else
articleCategory.ClassLayer = 2;
await _appDbContext.ArticleCategory.AddAsync(articleCategory);
var result = await _appDbContext.SaveChangesAsync();
return result == 1;
}
public async Task<bool> DeleteArticleCategoryAsync(int Id)
{
var articleCategory = await _appDbContext.ArticleCategory.FirstOrDefaultAsync(x => x.Id == Id);
if (articleCategory != null)
{
_appDbContext.ArticleCategory.Remove(articleCategory);
}
var result = await _appDbContext.SaveChangesAsync();
return result == 1; //注意(result==1 如果等式成立,则返回true,说明删除成功)
}
public async Task<ArticleCategory> FindArticleCategoryAsync(int Id)
{
var item = await _appDbContext.ArticleCategory.Where(x => x.Id == Id).FirstOrDefaultAsync();
return item;
}
public async Task<ArticleCategory[]> GetArticleCategory()
{
var items = await _appDbContext.ArticleCategory.Where(x => x.IsDeleted==false).OrderByDescending(x => x.Sort).ToArrayAsync();
return items;
}
public async Task<ArticleCategory[]> GetArticleCategoryByParentId(int pId)
{
var items = await _appDbContext.ArticleCategory.Where(x => x.ParentId == pId).OrderByDescending(x => x.Sort).ToArrayAsync();
return items;
}
public async Task<bool> UpdateArticleCategoryAsync(int id, ArticleCategory articleCategory)
{
var oldArticleCategory = await FindArticleCategoryAsync(id); //找出旧对象
//将新值赋到旧对象上
oldArticleCategory.Title = articleCategory.Title;
oldArticleCategory.ParentId = articleCategory.ParentId;
oldArticleCategory.Sort = articleCategory.Sort;
//对旧对象执行更新
_appDbContext.Entry(oldArticleCategory).State = EntityState.Modified;
var result = await _appDbContext.SaveChangesAsync();
return result == 1;
}
}

至此,文章管理功能编写完成。通篇只有2个主要知识点:linq子查询和ViewModel显示 |
|