StarBlog Blog Development Notes (8): Displaying Category Hierarchy
Series articles Developing a Blog Project with .NetCore StarBlog - (1) Why Do You Need to Write Your Own Blog? Developing a Blog Project with .NetCore StarBlog - (2) Environment Setup and Creating the Project Developing a Blog Project with .NetCore StarBlog - (3) Model Design Developing a Blog Project with .NetCore StarBlog - (4) Batch Importing Markdown Blog Posts Developing a Blog Project with .NetCore StarBl
目录8 个章节
Series Articles
- Developing a Blog Project with .NetCore StarBlog - (1) Why Do You Need to Write Your Own Blog?
- Developing a Blog Project with .NetCore StarBlog - (2) Environment Setup and Creating the Project
- Developing a Blog Project with .NetCore StarBlog - (3) Model Design
- Developing a Blog Project with .NetCore StarBlog - (4) Batch Importing Markdown Blog Posts
- Developing a Blog Project with .NetCore StarBlog - (5) Starting to Build the Web Project
- Developing a Blog Project with .NetCore StarBlog - (6) Page Development: Blog Post List
- Developing a Blog Project with .NetCore StarBlog - (7) Page Development: Post Detail Page
- Developing a Blog Project with .NetCore StarBlog - (8) Displaying Category Hierarchy
- ...
Preface
The previous article (6) Page Development: Blog Post List covered the development of the post list. On the page, the left side is the category list and the right side shows the posts under that category. This layout looks decent at first glance, but considering that this project supports multi-level categories, the category list simply displays all categories mechanically, which cannot reflect the category hierarchy and takes up a lot of vertical page space. Therefore, this article will refactor the category list so that it can represent multi-level categories and save page space.
Regarding tree structure components, I looked around, and components that work with bootstrap (based on jQuery) are hard to find; most of them are very old. I only found bootstrap-treeview, which is a bit more usable. Looking at its GitHub project homepage, it similarly hasn't been updated for a long time. It even targets bootstrap version 3.x. It's already 2022 now, and bootstrap has been updated to version 5.x, but I couldn't find anything better, so I'll make do with it~ (If it really doesn't work I can still clone its code and hack it)
Installation
This component is fairly old
It depends on bower. If you don't have bower, you need to install it first
npm install -g bower
Then run the following command in the StarBlog.Web directory to install the dependency
npm install bootstrap-treeview
Because all our static assets are under wwwroot, frontend resources installed via npm still need to be automatically copied into wwwroot using the gulp tool. This was covered in a previous article. If you've forgotten, you can check this earlier post: Developing a Blog Project with .NetCore StarBlog - (5) Starting to Build the Web Project
Edit the gulpfile.js file and add a line in the const libs configuration
//使用 npm 下载的前端组件包
const libs = [
// ...
{name: "bootstrap-treeview", dist: "./node_modules/bootstrap-treeview/dist/**/*.*"},
];
Then just run the gulp task
gulp move
After completion, you can see that a bootstrap-treeview directory has been added under wwwroot/lib
Next we can reference it in the page
Usage
Before we officially start, let's first understand how this component is used
Include the dependencies
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
Place a container in the web page
<div id="categories">
According to the official example, activate the component with js
const instance = $('#categories').treeview({
data: collections,
});
The format of collections is as follows
const collections = [
{
text: 'Parent 1',
href: '#parent1',
nodes: [
{
text: 'Child 1',
href: '#child1',
nodes: [
{
text: 'Grandchild 1',
href: '#grandchild1',
},
{
text: 'Grandchild 2',
href: '#grandchild2',
}
]
},
{
text: 'Child 2',
href: '#child2',
}
]
},
{
text: 'Parent 2',
href: '#parent2',
},
{
text: 'Parent 3',
href: '#parent3',
},
{
text: 'Parent 4',
href: '#parent4',
},
{
text: 'Parent 5',
href: '#parent5',
}
];
The default effect from the official site

However, after my testing, this official example has some issues under bootstrap5 — the default icons don't display at all. We need to customize it a bit by adding icon configuration. The icons used are the FontAwesome Icons we installed earlier
const instance = $('#categories').treeview({
data: collections,
collapseIcon: "fa fa-caret-down",
expandIcon: "fa fa-caret-right",
emptyIcon: 'fa fa-circle-o',
});
Processing Category Data
To make it convenient to use this component, we need to wrap the category hierarchy on the backend into the form this component requires.
First, define a node class
public class CategoryNode {
public string text { get; set; } = "";
public string href { get; set; } = "";
public List<CategoryNode>? nodes { get; set; }
}
Then add a new method in Services/CategoryyService.cs to generate the category tree structure. For convenience of writing the code, I implemented it directly with recursion.
public List<CategoryNode>? GetNodes(int parentId = 0) {
var categories = _cRepo.Select
.Where(a => a.ParentId == parentId).ToList();
if (categories.Count == 0) return null;
return categories.Select(category => new CategoryNode {
text = category.Name,
nodes = GetNodes(category.Id)
}).ToList();
}
The output data looks like this
[
{
"text": "Android开发",
"href": "",
"nodes": null
},
{
"text": "AspNetCore",
"href": "",
"nodes": [
{
"text": "Asp-Net-Core学习笔记",
"href": "",
"nodes": null
},
{
"text": "Asp-Net-Core开发笔记",
"href": "",
"nodes": null
}
]
}
]
Oh, almost forgot we still need to add the href parameter to each node
Hardcoding is out of the question. The ControllerBase instance comes with a Url property of type IUrlHelper by default, which can be used with its Link() method to resolve address routing.
However, this method is written in the Service and there is no ControllerBase instance, so in this case we can only use dependency injection. But I saw a statement on Stack Overflow that after AspNetCore 3.x, using LinkGenerator is better.
Here's the code. First register the service
builder.Services.AddHttpContextAccessor();
Then dependency injection
private readonly IHttpContextAccessor _accessor;
private readonly LinkGenerator _generator;
public CategoryService(IHttpContextAccessor accessor, LinkGenerator generator) {
_accessor = accessor;
_generator = generator;
}
Modify the GetNodes method above, and add this in the CategoryNode initializer
href = _generator.GetUriByAction(
_accessor.HttpContext!,
nameof(BlogController.List),
"Blog",
new {categoryId = category.Id}
)
The specific code can be seen on GitHub: https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Services/CategoryService.cs
The generated link looks like this:
{
"text": "Android开发",
"href": "http://localhost:5038/Blog/List?categoryId=2",
"nodes": null
}
Frontend Rendering
The data is ready. Now we run into a problem: the data needs to be processed in js, so should I use an async request like fetch to get the category data and then display the tree categories? The benefit of this is that it's more straightforward to write. However, our project's blog site is server-side rendered. Mixing async requests into the blog list page now would cause a sense of disconnection — the post list on the right is server-rendered and displayed in the browser, while the categories on the left still need to be requested asynchronously.
After some thought, I decided that this category list should also use server-side rendering. Although it's a bit counterintuitive, according to the bootstrap-treeview component documentation, it can render categories using json. So I just need to serialize the category data into json format on the backend, then render it into the js code in the view.
Let's get started~
Edit the StarBlog.Web/ViewModels/BlogListViewModel.cs file and add two fields
public List<CategoryNode> CategoryNodes { get; set; }
// 将上面的分类层级数据转换成Json字符串
public string CategoryNodesJson => JsonSerializer.Serialize(
CategoryNodes,
new JsonSerializerOptions {Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping}
);
Then modify the Controller, StarBlog.Web/Controllers/BlogController.cs. First inject CategoryService
Then modify the List method
public IActionResult List(int categoryId = 0, int page = 1, int pageSize = 5) {
var categories = _categoryRepo.Where(a => a.Visible)
.IncludeMany(a => a.Posts).ToList();
categories.Insert(0, new Category {Id = 0, Name = "All", Posts = _postRepo.Select.ToList()});
return View(new BlogListViewModel {
CurrentCategory = categoryId == 0 ? categories[0] : categories.First(a => a.Id == categoryId),
CurrentCategoryId = categoryId,
Categories = categories,
// 增加这一行
CategoryNodes = _categoryService.GetNodes(),
Posts = _postService.GetPagedList(new PostQueryParameters {
CategoryId = categoryId,
Page = page,
PageSize = pageSize,
OnlyPublished = true
})
});
}
The last step is to modify the View, StarBlog.Web/Views/Blog/List.cshtml, adding JS references and some JS code at the bottom. I've already wrapped the treeview component configuration into an initTreeView method, which can be used directly.
@section bottom {
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap-treeview/dist/bootstrap-treeview.min.js"></script>
<script src="~/js/blog-list.js"></script>
<script>
const categories = '@Html.Raw(Model.CategoryNodesJson)'
initTreeView(categories);
</script>
}
The key code in the View is just these few lines. The complete code can be found at: https://github.com/Deali-Axy/StarBlog/blob/master/StarBlog.Web/Views/Blog/List.cshtml
Final Result
The final result after completion is as follows. Category hierarchy is now supported, but it's still not perfect—there are a few issues:
- Cannot highlight the currently selected category
- Category post counts are not displayed (the previous version had this)
- Cannot customize the
list-group-itemstyle; the underlines look unattractive - ...
I'll leave these issues for later optimization~ That's enough tinkering for now…

Development of the blog project is basically complete. The project code is fully open source—if you're interested, feel free to give it a star~
- Blog backend + frontend project: https://github.com/Deali-Axy/StarBlog
- Admin dashboard frontend project: https://github.com/Deali-Axy/StarBlog-Admin
References
- Bower official website: https://bower.io/
- bootstrap-treeview project homepage: https://github.com/jonmiles/bootstrap-treeview
- https://onelib.biz/blog/a/602b8b65906abf3c8f946fd7
- https://stackoverflow.com/questions/37322076/injection-of-iurlhelper-in-asp-net-core
_EOF_
StarBlog Blog Development Notes (8): Displaying Category Hierarchy
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
StarBlog博客开发笔记(1):项目介绍
前言 OK,我也来造轮子了 博客系统从一开始用WordPress,再到后来用hexo、hugo之类的静态博客生成放github托管,一直在折腾 折腾是为了更好解决问题,最终还是打算自己花时间搞一个好了 本系列文章将记录博客的开发过程~ 后端基于最新的.Net6和Asp.Net Core框架,遵循RESTFul接口规范,前端基于Vue+ElementUI开发,可作为 .Net Core 入门项目学习
2022-04-05 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(19):Markdown渲染方案探索
前言 笔者认为,一个博客网站,最核心的是阅读体验。 在开发StarBlog的过程中,最耗时的恰恰也是文章的展示部分功能。 最开始还没研究出来如何很好的使用后端渲染,所以只能先用Editor.md组件做前端渲染,过渡一下。前端渲染我是不满意的,因为性能较差,页面加载出来还会闪一下,有割裂感,影响体验。 现在我已经做出了比较完善的后端渲染方案,前端渲染就可以直接退休了。本文介绍StarBlog博客开发
2022-10-28 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(27):使用JWT保护接口
前言 这是StarBlog系列在2023年的第二篇更新😂 这几个月都在忙,更新变得很不勤快,但是拖着不更新我的心里更慌,很久没写,要开头就变得很难😑 说回正题,之前的文章里,我们已经把博客关键的接口都开发完成了,但还少了一个最关键的「认证授权」,少了这东西,网站就跟筛子一样,谁都可以来添加和删除数据,乱套了~ 关于「认证授权」的知识,会比较复杂,要学习这块的话,建议分几步: 基础概念 AspN
2023-05-03 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(24):统一接口数据返回格式
前言 开发接口,是给客户端(Web前端、App)用的,前面说的RESTFul,是接口的规范,有了统一的接口风格,客户端开发人员在访问后端功能的时候能更快找到需要的接口,能写出可维护性更高的代码。 而接口的数据返回格式也是接口规范的重要一环,不然一个接口返回JSON,一个返回纯字符串,客户端对接到数据时一脸懵逼,没法处理啊。 合格的接口返回值应该包括状态码、提示信息和数据。 就像这样: { st
2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟
讨论
共 0 条加载评论中…