團隊切換器
在/Teams/Details?id=xxx的頁面,有這樣一個控制項,使得不需要回到/Teams/Index就能輕鬆切換團隊:
由於這種團隊切換控制項比比皆是,比如在團隊故事板中(以及其他地方若干處):
所以希望開發一個控制項。
這個控制項應該能:
1. 裡邊所包含的連結自動跟隨頁面的連結。
比如第一張圖裡邊,指向/Teams/Details?id=XXX;而第二張圖裡邊,則是指向/Agile/TeamStoryBoards/Details?teamID=xxx
2. 下拉式功能表裡邊的所有連結,會因為id或teamID不同而指向指定團隊的頁面。
設計思路調用代碼:
<div class = "link-span spliter"> 切換團隊:@Team.TeamsDropdownList(this, "id") </div>
調用代碼總是很像自然語言,他說:在這個頁面(this)的連結裡有個欄位("id")有差異,隨Team而變化。在調用代碼中,任何比自然語言多的內容基本上都是垃圾內容。
實際上可以看出,Url本身不能寫入程式碼寫進去,而是應該從頁面自動擷取;擷取後,把不同連結中的id=xxx換成不同Team的ID。
實際代碼:
public partial class Team : Department { public static HelperResult TeamsDropdownList(WebViewPage page, string urlKey) //urlKey就是"id"或"teamID"這兩個,用來表示需要替換URL中的哪個參數的 { var currentTeamID = page.ParameterOf(urlKey); //提取id或teamID的實際值,裡邊利用了page.Request.RawUrl,也可以用page.Request.Queries(我們以前不知道有這個)。 var program = Program.Default(); //取得預設部門,部門是團隊Team的上一個層級。 var currentTeam = string.IsNullOrEmpty(currentTeamID) ? null : program.Teams().Single(i => i.ID.ToString() == currentTeamID); //下拉式功能表中的當前團隊。 return MFCUI.DropdownListHtml(page, currentTeam == null ? new MvcHtmlString("選擇團隊") : MFCUI.ImageLink(currentTeam.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page), program.Teams().Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()), displayAsText: currentTeam != null && i.ID == currentTeam.ID))); }MFCUI.DropdownListHtml是另外一個產生下拉式功能表的控制項,不多說了,關鍵是其中的一句話:
page.MergeParameter(urlKey, i.ID.ToString())
這句話把page.Request.RawUrl中的"id"或"teamID"替換為program.Teams()中不同團隊的ID,產生其新連結。
這樣,這個方法就能在任何頁面,利用一個id/teamID等區分欄位名來自動產生團隊切換的效果,而連結到底指向哪裡不關心。擴充之前說的這個東西不錯,可是在另外一種情境中不能直接用,比如下面這個“切換產品”:調用代碼長得很像:
<div class="link-span spliter"> 切換產品:@Product.ProductsDropdownList(this, "id") </div>
拷貝代碼改寫的結果是給Product增加一個類似函數:
public static HelperResult ProductsDropdownList(WebViewPage page, string urlKey) { var currentTeamID = page.ParameterOf(urlKey); var productLine = ProductLine.Default(); var currentProduct = string.IsNullOrEmpty(currentTeamID) ? null : productLine.Products().Single(i => i.ID.ToString() == currentTeamID); return MFCUI.DropdownListHtml(page, currentProduct == null ? new MvcHtmlString("選擇團隊") : MFCUI.ImageLink(currentProduct.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page), productLine.Products().Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()), displayAsText: currentProduct != null && i.ID == currentProduct.ID))); }
這兩個方法的內容幾乎完全相同,這是代碼壞味道的先兆;比如以後修改了MFCUI.DropdownListHtml,就要回來修改兩個地方。幸虧Product和Team都繼承自基類Item(用於處理樹狀結構,如Father,Brothers,SubItems之類的),這樣就可以用基類進行改寫:不過,改寫的時候最好遵循某些步驟,否則很容易在半路上撂挑子。步驟1:改寫其中一個函數,去掉具體的類型我先拿Product裡邊那個動刀,在原來函數的下面(不要直接給改了),增加一個新的函數:
public static HelperResult BrothersDropdownList(WebViewPage page, string defaultText, Item father, string whattype, string urlKey) { var currentId = page.ParameterOf(urlKey); var currentItem = string.IsNullOrEmpty(currentId) ? null : father.SubItems().Single(i => i.ID.ToString() == currentId); return MFCUI.DropdownListHtml(page, currentItem == null ? new MvcHtmlString(defaultText) : MFCUI.ImageLink(currentItem.Title, page.Request.RawUrl, displayAsBoldTextOnPage: page), father.SubItems().Where(i => i.WhatType == whattype).Select(i => MFCUI.ImageLink(i.Title, page.MergeParameter(urlKey, i.ID.ToString()), displayAsText: currentItem != null && i.ID == currentItem.ID))); }所有不同點,都被提取成一個參數:
defaultText用來處理“選擇團隊”和“選擇產品”;father用來處理Program.Default()和ProductLine().Default();
whattype是我們用於在
Item基類層面上區分不同的類型的,比如產品有產品線(ProductLine)-產品(Product)-版本-發布等層級;而團隊有公司-子公司-部門(Program)-團隊(Team)等層級。
father.SubItems().Where(i => i.WhatType == whattype)
就是問father的下一層級中的whattype類型的。這樣如果傳入father=ProductLine.Default()和whattype=ItemWhatType.ProductProduct,就能得到所有產品,和原來代碼結果相同。
現在,
這個方法雖然還在Products下,但裡邊已經和Product完全無關了,這是步驟1的目標。在網頁上測試一下:
<div class="link-span spliter"> 切換產品:@Product.ProductsDropdownList(this, "id") 切換產品:@Product.BrothersDropdownList(this, "切換產品", ProductLine.Default(), ItemWhattype.ProductProduct, "id") </div>
最好的測試方法,就是像上面這樣做“對比測試”,然後看兩者有何不同;這個很像敏捷開發裡邊提到重構時,要驗證產品的外部功能沒有發生變化一樣。步驟2:挪到基類中基類叫Item,函數名不變還是BrothersDropdownList,代碼不寫了。重新做對比測試:
<div class="link-span spliter"> 切換產品:@Product.ProductsDropdownList(this, "id") 切換產品:@Product.BrothersDropdownList(this, "切換產品", ProductLine.Default(), ItemWhattype.ProductProduct, "id") 切換產品:@Item.BrothersDropdownList(this, "切換產品", ProductLine.Default(), ItemWhattype.ProductProduct, "id") </div>
結果是三個控制項功能完全相同,好了,刪除前兩個;再去Team那邊把Team的方法刪除,調用也做相應修改。以後任何Item衍生類別都能在案頭上扔這樣一個控制項來切換了。在火星人裡邊所有東西都是Item,所以複用的次數非常多。總結1. 任何相似代碼都是壞味道2. 調用代碼的資訊量應該與自然語言相同3. 重構時做對比測試。一些其他前提:凡是用兩次的東西都可能值得做個積累。這個“積累”或許是個方法,或許是個基類。比如上面提到的MFCUI.DropdownList()/this.ParameterOf()/this.MergeParamegter()這些,都是以前為了別的事情做好了的。如果沒有積累,每次想複用的時候就會發現要做很多準備工作,會情不自禁地想“算了這次先這樣吧”。時間長了破罐子破摔,代碼就越來越爛了。