MVC3+EF4.1學習系列(十)----MVC+EF處理樹形結構
通過前幾篇文章 我們處理了 一對一, 一對多,多對多關系 很好的發揮了ORM框架的做用 但是 少說了一種 樹形結構的處理, 而這種樹形關系 我們也經常遇到,常見的N級類別的處理, 以及經常有數據與類別掛鉤。今天主要寫下EF處理樹形結構以及 MVC如何展示樹形結構。 前面幾篇的例子 一直用的是一個例子,內容是連貫的。這篇是完全單獨的~
先來說下工作中會遇到的常見場景 針對這幾個場景來處理~
1.類別
a.類別可以有無限級別
b.類別的最末端 不確定是第幾級 某個節點 可以到二級 其他的節點 有可能到四級
c.tree型展示整個類別 并可以對tree進行CRUD (可以一次遞歸全部加載 也可以異步加載 )
d.面包屑型展示類別
e.刪除父類 應把下面所有的子類刪除
2.與類別掛鉤的數據 (本文是文章)
a. 可以根據任意級別的類別 查看文章
b. 合并兩個類別的文章
上面這些場景 基本覆蓋了類別操作的常見情況 如果大家覺得還有什么要處理 可以給我說 我補充上去~~
下面開始講解~
一.準備工作
1.如何建立類別實體類 來展示樹形結構
上代碼
/// <summary>
/// 類別
/// </summary>
public class Category
{
/// <summary>
/// 主鍵
/// </summary>
public int CategoryId { get; set; }
/// <summary>
/// 類別名字
/// </summary>
[Required()]
[StringLength(5)]
public string CategoryName { get; set; }
/// <summary>
/// 父ID
/// </summary>
public Nullable<int> ParentId { get; set; }
/// <summary>
/// 上面的父節點
/// </summary>
public virtual Category Parent { get; set; }
/// <summary>
/// 下面的子節點
/// </summary>
[ForeignKey("ParentId")]
public virtual ICollection<Category> ChildKeys { get; set; }
/// <summary>
/// 該類別的文章集合
/// </summary>
public virtual ICollection<Article> articleList { get; set; }
/// <summary>
/// 編號
/// </summary>
public string Note { get; set; }
/// <summary>
/// 狀態
/// </summary>
public string State
{
get;
set;
}
/// <summary>
/// 級別
/// </summary>
public Nullable<int> Lev
{
get;
set;
}
/// <summary>
/// 排序
/// </summary>
public int Sort
{
get;
set;
}
}
這樣的設計 很好的展示了樹形結構 一個節點有一個父類 多個子類 一個類別可以有多個文章 這里說下 后面的四個屬性 不是必要的~
2.文章實體類
上代碼 這個比較好理解 不解釋了~
文章實體類
/// <summary>
/// 文章
/// </summary>
public class Article
{
/// <summary>
/// 文章ID
/// </summary>
public int ArticleId { get; set; }
/// <summary>
/// 文章名
/// </summary>
public string ArticleName { get; set; }
/// <summary>
/// 創建時間
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 文章順序
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 類別ID
/// </summary>
public int CategoryId { get; set; }
/// <summary>
/// 類別
/// </summary>
[ForeignKey("CategoryId")]
public Category Category { get; set; }
}
3.建立Context
上代碼
public class TreeDemoContext : DbContext
{
private readonly static string CONNECTION_STRING = "name=WlfSys_EFCF_ConnString";
public DbSet<Category> Category { get; set; }
public DbSet<Article> Article { get; set; }
public TreeDemoContext()
: base(CONNECTION_STRING)
{
// this.Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();//移除復數表名的契約
}
}
這里不需要使用Fluent API 來映射實體類與數據庫的關系 里面也沒什么亮點( 其實我一直想知道 怎么用Fluent API 映射 來解決下面的問題 有知道的高人指點下~~ 感激 )
4.數據庫初始化
這是我這種方法 用ef處理樹形結構最關鍵的一點 熟練使用ef的人 看了我上面的類別實體類的建立 就會發現這是錯誤的 因為這會造成自引用 看下圖生成的數據庫表結構

由于自引用 插入時會出現 INSERT 語句與 FOREIGN KEY SAME TABLE 約束"Category_ChildKeys"沖突。
我的解決辦法是 在初始化數據庫時 刪除這個外鍵約束 上代碼
public class TreeDemoInitializer : DropCreateDatabaseIfModelChanges<TreeDemoContext>
{
protected override void Seed(TreeDemoContext context)
{
//刪除關聯
context.Database.ExecuteSqlCommand("ALTER TABLE [dbo].[Category] DROP CONSTRAINT [Category_ChildKeys]");
//必須加上ID
var Category = new List<Category>
{
new Category{ CategoryId=1, CategoryName="亞洲", Lev=1, ParentId=0, Note="001",ChildKeys=new List<Category>{
new Category{CategoryId=2,CategoryName="中國",Lev=2,Note="00101",ChildKeys=new List<Category>{ new Category{CategoryId=6, CategoryName="河南", Lev=3, Note="0010101" },new Category{CategoryId=7, CategoryName="廣州", Lev=3, Note="0010102" } }},
new Category{CategoryId=3,CategoryName="日本",Lev=2,Note="00102",ChildKeys=new List<Category>{ new Category{CategoryId=8, CategoryName="日本省1", Lev=3, Note="0010201" },new Category{CategoryId=9, CategoryName="日本省2", Lev=3, Note="0010202" } }
} }
},
new Category { CategoryId=4, CategoryName="歐洲", Lev=1, ParentId=0, Note="002", ChildKeys=new List<Category>{
new Category{CategoryId=5,CategoryName="荷蘭",Lev=2,Note="00201"}
} }
};
Category.ForEach(c => context.Category.Add(c));
var Articles = new List<Article>{
new Article{ ArticleName="小說13", CreateTime=DateTime.Now, CategoryId=5},
new Article{ ArticleName="小說14", CreateTime=DateTime.Now, CategoryId=5},
new Article{ ArticleName="小說15", CreateTime=DateTime.Now, CategoryId=5},
new Article{ ArticleName="小說1", CreateTime=DateTime.Now, CategoryId=6},
new Article{ ArticleName="小說2", CreateTime=DateTime.Now, CategoryId=6},
new Article{ ArticleName="小說3", CreateTime=DateTime.Now, CategoryId=6},
new Article{ ArticleName="小說4", CreateTime=DateTime.Now, CategoryId=7},
new Article{ ArticleName="小說5", CreateTime=DateTime.Now, CategoryId=7},
new Article{ ArticleName="小說6", CreateTime=DateTime.Now, CategoryId=7},
new Article{ ArticleName="小說7", CreateTime=DateTime.Now, CategoryId=8},
new Article{ ArticleName="小說8", CreateTime=DateTime.Now, CategoryId=8},
new Article{ ArticleName="小說9", CreateTime=DateTime.Now, CategoryId=8},
new Article{ ArticleName="小說10", CreateTime=DateTime.Now, CategoryId=9},
new Article{ ArticleName="小說11", CreateTime=DateTime.Now, CategoryId=9},
new Article{ ArticleName="小說12", CreateTime=DateTime.Now, CategoryId=9}
};
Articles.ForEach(a => context.Article.Add(a));
context.SaveChanges();
}
}
并初始化一些數據進去 這個初始化 就算是類別的添加了 在這添加時 遇到個小問題 我們的數據庫類別ID默認是自增長的 按理說不用指定主鍵ID 但是不指定ID 像我上面 一下次插入多條時 插入時卻報錯 ~~ 無法確定“ContosoUniversity.DAL.Category_ChildKeys”關系的主體端。添加的多個實體可能主鍵相同。指定了ID 才解決了這個問題
5. 搭建基本項目結構
依然使用 unit of work +Repository ( 項目大的話 加入Iservice, Service 再加上IOC,這里只是個簡單的demo) 如圖

二.關于類別的操作以及展示
1.tree型展示整個類別
在webfrom時代 實現tree展示很容易 因為我們有犀利的控件 treeview 用treeview控件 再加個遞歸綁定 就很簡單的完成了 這是webfrom的好處 但也是不好的地方 比如 treeview生成出來的 是 table 嵌套table的 我如果想換成ul li怎么辦 控件開發 造成了耦合度過高 題外話說多了 回歸正題~
這里 我用兩種方法實現 treeview的展示
A方法 擴展一個 HtmlHelper
實現 HTML.Tree(類別) 就能展示出treeview
用這個方法前 再說幾句 不喜歡這個方法 因為這有點像用控件了 代碼與視圖依然在一起 第二 這里我使用了遞歸, 但是小項目的話 沒什么問題 直接上code
public static MvcHtmlString Tree(this HtmlHelper html, Category treeModel)
{
return BindTree(treeModel);
}
private static MvcHtmlString BindTree(Category treeModel)
{
StringBuilder sb = new StringBuilder();
if (treeModel != null)
{
sb.Append("<ul>");
List<Category> list = treeModel.ChildKeys.ToList();
foreach (var item in list)
{
sb.Append("<li>");
sb.Append(item.CategoryName);
sb.Append("</li>");
sb.Append(BindTree(item));
}
sb.Append("</ul>");
}
MvcHtmlString mstr = new MvcHtmlString(sb.ToString());
return mstr;
}
上面實現了最最簡單的展示樹形結構 無非就是遞歸的運用 這個可以擴展 是否展示 checkbox 啊 上來默認展示幾級啊 后面的增刪改連接啊 and so on~~
b. 利用ajax 實現異步加載 ( 個人喜歡的方法 ) 先上一個實現后的圖 我沒做任何美工 樣子很難看~ 大家將就看下

前面有小箭頭表示可以打開~~ 打開后 變成打開的狀態~
下面上視圖 解釋和思路 直接加在里面了
<script type="text/javascript">
$(
function () {
var clickLi = function () {
$(this).children("ul").toggle(); // 切換隱藏和顯示li下面的ul
//切換img圖標是 選中還是未選中
if ($(this).children("img").attr("src") == "http://www.rzrgm.cn/Content/img/selectNode.jpg") {
$(this).children("img").attr("src", "http://www.rzrgm.cn/Content/img/noselectNode.jpg");
}
else {
$(this).children("img").attr("src", "http://www.rzrgm.cn/Content/img/selectNode.jpg");
}
//什么時候發送加載下面節點的請求呢?
//在img 屬性不為空 證明下面有節點 因為沒有節點是不會有img 樹形的 并且他的下面的ul個數為0
if ($(this).children("img").attr("src") != undefined && $(this).children("ul").length == 0) {
var cid = $(this).attr("id");
var li = $(this);
$.post("GetCategoryById", { id: cid }, function (data) {
if (data == "-1") {
alert("失敗");
}
else {
li.append(data);
}
});
}
return false;
}
//為什么用 live 不是直接click?
//因為 ajax請求加載的 click事件是不管用的 要用live才可以 ~~切記
$("#CategoryTree li").live("click", null, clickLi);
}
);
</script>
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
@*@Html.Tree(Model);*@
<ul id="CategoryTree">
@foreach (var item in Model.ChildKeys)
{
<li id="@item.CategoryId">
@if(item.ChildKeys.Count > 0)
{
<img src="http://www.rzrgm.cn/Content/img/noselectNode.jpg" />
}
<span class="name">@item.CategoryName</span>
@Html.ActionLink("添加", "Create", new { id=@item.CategoryId})
@Html.ActionLink("修改", "Edit", new { id=@item.CategoryId})
</li>
}
</ul>
這里說下jquery ajax請求時 我們經常返回json 然后來構建 這里說下mvc另一種方法 返回一個部分視圖~~ 我很喜歡這種方法 上code~ 不解釋啦~
ajax返回部分視圖代碼
[HttpPost]
public ActionResult GetCategoryById(int id)
{
if (Request.IsAjaxRequest())
{
Category CategoryModel = unitOfWOrk.CategoryRepository.GetTEntityByID(id);
if (CategoryModel != null)
{
return PartialView(CategoryModel);
}
else
{
return Content("-1");
}
}
else
{
return Content("-1");
}
}
部分視圖的視圖
@model TreeDemoCore.Model.Category
<ul>
@foreach (var childitem in Model.ChildKeys)
{
<li id=@childitem.CategoryId>
@if (childitem.ChildKeys.Count > 0)
{
<img src="http://www.rzrgm.cn/Content/img/noselectNode.jpg" />
}
<span class="name">@childitem.CategoryName </span>
@Html.ActionLink("添加", "Create")
@Html.ActionLink("修改", "Edit")
</li>
}
</ul>
2.展示面包屑
樹形結構的展示 我們經常遇到treeview型的 還會遇到另一種 面包屑這樣的 如

我們這里要做的就是 根據當前類別 向上一層層推到最上面 并把最后一個加粗顯示~ 我的思路是這樣的 比如當前所在的類別 為最小的 廣州 根據這個 一層層推到最上面 通過遞歸得到 廣州>中國>亞洲 在通過反轉字符串 并給加粗就行了~~ 代碼如下
/// <summary>
/// 根據當前類別建造面包屑
/// </summary>
/// <returns></returns>
public static MvcHtmlString Menu(this HtmlHelper html, Category treeModel)
{
return new MvcHtmlString(MenuReverse(BindMenu(treeModel)));
}
/// <summary>
/// 遞歸調用 得到 廣州>中國>亞洲
/// </summary>
/// <returns></returns>
private static string BindMenu(Category Model)
{
StringBuilder sb = new StringBuilder();
sb.Append(Model.CategoryName);
if (Model.Parent != null)
{
sb.Append(">");
sb.Append(BindMenu(Model.Parent));
}
return sb.ToString();
}
/// <summary>
/// 反轉字符串 并給最后一個加上黑體字標簽
/// </summary>
/// <returns></returns>
private static string MenuReverse(string menu)
{
return string.Join(">", menu.Split('>').Select((s, i) => i == 0 ? string.Format("<strong>{0}</Strong>", s) : s).Reverse().ToArray());
}
3.刪除父類要把下面的子類全部刪除
這里就涉及到一個樹形結構的重要方法 通過當前類 得到該類的所有子類子子類等的ID集合 這時刪除時用 delete in(子類集合 ) 就行了 ~~ 忘了 怎么直接執行SQL語句的~~ 去看上一篇文章.. 通過現在類 獲得下面子類集合的方法 如下
/// <summary>
/// 獲得父類下所有子類的集合
/// </summary>
/// <returns></returns>
private List<int> GetCidbyPid(int pid)
{
List<int> cidList = new List<int>();
Category CategoryModel = unitOfWork.CategoryRepository.GetTEntityByID(pid);
foreach (var item in CategoryModel.ChildKeys)
{
cidList.Add(item.CategoryId);
}
foreach (var item in CategoryModel.ChildKeys)
{
cidList.AddRange(GetCidbyPid(item.CategoryId));
}
return cidList;
// 獲得 1,2,3,4,5 tring strcid= string.Join(",", cidList);
}
依然是通過遞歸 獲得所有的 子節點集合 再通過string.Join(",", cidList) 得到 delete in ( ) 里需要的的 就行了~~
三.與類別掛鉤的數據的展示
一. 可以根據任意級別的類別查看文章
依然提供兩個方法~
1.根據類別 查詢文章 利用遞歸
不過這個效率就太糾結了~~
/// <summary>
/// 通過任意類別 獲得下面的全部文章
/// </summary>
/// <param name="cid"></param>
/// <returns></returns>
private List<Article> GetArticleByCid(int cid)
{
List<Article> ArticleList=new List<Article>();
Category CategoryModel=unitOfWork.CategoryRepository.GetTEntityByID(cid);
if (CategoryModel.ChildKeys.Count == 0)
{
ArticleList.AddRange(CategoryModel.articleList.ToList());
}
foreach (var item in CategoryModel.ChildKeys)
{
ArticleList.AddRange(GetArticleByCid(item.CategoryId));
}
return ArticleList;
}
2. 利用前面說到的 通過當前類 得到該類的所有子類子子類等的ID集合 再查詢的時候 IN 就行了
這里有個小知識~ 用linq 執行 sql in的操作~ 用 Contains就行了 代碼如下
public ActionResult Index()
{
//準備測試數據 測試不同情況 GetCidbyPid為根據ID獲得所有子類以及子子類等的集合
//測試最高級
List<int> CidList = GetCidbyPid(1);
// var CidList = GetCidbyPid(2);
//測試最低級
//var CidList = GetCidbyPid(6);
// 貪婪加載類別 為了顯示類別名字~
var ArticleList = unitOfWork.ArticleRepository.Get(c => CidList.Contains(c.CategoryId), includeProperties: "Category");
return View(ArticleList);
}
二.合并兩個類別的文章
這個很簡單啦~ 合并兩個類 就是把一個類下的文章id 都變成另一個 也就是說批量操作 不要用EF 的一個個更新就行 太慢了~還要發送多條更新語句
用context.Database.SqlQuery 直接執行 update 這個就不寫代碼啦~
四.通過第三方工具Telerik更加酷炫的展示tree
我上面寫的東西 都是小打小鬧 自娛自樂的玩下 有很多地方不完善 不合理,而MVC實現Tree 第三方工具已經有了幫我們做的非常優秀的了
這里給大家推薦個開源的 是Telerik的 tree------介紹與連接
把該有的操作基本全部都封裝在了里面啦~非常方便 而且里面還有很多其他的控件~
還有,推薦大家看看源碼 第三方工具一定要多看看實現 不要只會用
五.總結
這篇是完全獨立的 和前面幾篇沒什么關系~ 代碼貼的都是核心片段~ 聰明的大家看看就能明白了 而且應該有更好的實現
希望大家分享下EF MVC處理 tree的經驗以及遇到的問題~~
我也提個問題 因為tree結構的操作 很多都用到了遞歸 在EF 高并發 大數據量處理時 我覺得會出現一些問題 希望大家說說怎么解決
總體內容沒有太多難度~ 大家自己敲敲練練吧
實在太懶的同學 留個郵件 我把demo發給你們~
(ps:文章寫在自己的22歲生日, 學習.net3年多了 希望自己通過不斷的學習 再次進步~ 也希望在園子里 得到更多的人的指點與幫助 祝福自己! )

浙公網安備 33010602011771號