之一,之二,之三
之前講到,方法聲明為:
@SFCUI.AjaxValue(story.ID, effortValue.ToString(), Effort.EffortPlannedValues, "/SFC/Efforts/AjaxSetEffortPlanned?itemID=" + story.ID + "&value={0}", ajaxOnSuccess: "refreshLeftPad");
調用的例子:
@SFCUI.AjaxValue(story.ID, effortValue.ToString(), Effort.EffortPlannedValues, "/SFC/Efforts/AjaxSetEffortPlanned?itemID={0}&value={1}", ajaxOnSuccess: "refreshLeftPad");
感覺上toggle、快顯功能表、UpdateTargetID什麼的都沒有啊,怎麼能實現?
這就是設計的一種重要原則:把所有技術細節都隱藏在實現的內部。
函數內部的設計
下面的代碼僅作樣本,與我自己用的不太一樣,簡化了一下,只為說明問題。
public static MvcHtmlString AjaxValue(int id, string value, string[] values, string urlFormat, string ajaxOnSuccess = null) { string ajaxUpdateTargetID = "ValueOf" + id; string innerHtml = null; //Build the div to show the final value. TagBuilder theValue = new TagBuilder("div"); //這個是放置原始數值的DIV,日後更新的也是它,ID在前面產生好了。 theValue.InnerHtml = value; ... //Build the ajaxLink to popup the menu of values to be selected. TagBuilder ajaxLink = new TagBuilder("div"); //這個是在Value後面放一個可以點的小圖片,點一下就會快顯功能表 ... ajaxLink.AddCssClass("clickshow"); //這個是我自己定義的Class,說明點擊它,會彈出一樣東西,就是是$("#clickshowBodyOfAjaxLinkOf" + $(this).attr(id)).show()。 innerHtml += ajaxLink.ToString(); //Build the div to show the popup menu; and a "clickshowbody" container to hide the menu when mouse left. TagBuilder content = new TagBuilder("div"); //這個是平時隱藏,點擊後會彈出的那個菜單。 ... //這裡隱藏的比較多,因為我調用了很多我自己的函數比如ImageLink()等,寫出來也看不到細節。 ... //大致就是:在菜單裡邊foreach(var v in values),用id 和每個v,string.Format(urlFormat, id, v)出一個AjaxLink來。 ... //如果OnSuccess不是null,記得AjaxLink裡邊要放上OnSuccess。 ... //關鍵代碼:imglink(是個tagbuilder).MergeAttribute("onclick", "Sys.Mvc.AsyncHyperlink.handleClick(this, new Sys.UI.DomEvent(event), { insertionMode: Sys.Mvc.InsertionMode.replace, updateTargetId: '" + ajaxUpdateTargetID + "', onSuccess: Function.createDelegate(this, " + ajaxOnSuccess + ") });"); innerHtml += clickshowbody.ToString(); return new MvcHtmlString(innerHtml); }
這樣,函數裡邊就把放置Value的DIV(也就是日後會被更新的那個)、快顯功能表、快顯功能表上應該寫的東西、點擊後該幹什麼、彈出後該幹什麼等等都處理了,扔到哪裡都工作。
下面是點擊後彈出的效果:
更複雜的介面
上面的例子基本上解決了簡單的列表式快顯視窗的情況,如果彈出來的東西比較複雜怎麼辦?比如我想把一個任務分配給我負責的組或我參與的組的某個組員,而且不希望這些組員直接按姓名排列(假設我記不住這些人),希望能呈現出一個下面這樣的介面來:
圖中點一下人名,就能把這個任務分配給這個人。
這個時候千萬別先想技術實現手段,而是先要想一下:到底有幾個商務資訊要處理?其實就4個:
1. 哪個任務?任務的ID
2. 什麼資訊要改?“當前負責人”
3. 可選人員有哪些?“我的組(我負責管理的和我參與的)”
4. 設定好了要做點什嗎?“重新整理一下螢幕”(主要是為了讓更新後的內容接受一下Document.Ready裡邊的JS重新刷一下)
所以函式宣告是:
public static MvcHtmlString AjaxSDCValue(Item item, int sdcTypeValue, AjaxValue.ValuesKeys valuesKey, string ajaxOnSuccess = null)
item是那個任務
sdcTypeValue是之前定義好的自訂欄位的值(不是為了這個AjaxValue定義的,SDC=System Defined Column系統自訂欄位)
AjaxValue.ValuesKey是個enum稍微複雜一點。之前總是直接輸入string[]來代表未來的值,現在有可能是User[]……等等,所以乾脆傳遞一個enum由函數內部判斷這次是找什麼,裡邊再詳細處理。
urlFormat哪去了?由於AjaxSDCValue是我們專門用來處理系統自訂欄位的,所以urlFormat是統一的,請看函數內部:
public static MvcHtmlString AjaxSDCValue(Item item, int sdcTypeValue, AjaxValue.ValuesKeys valuesKey, string ajaxOnSuccess = null) { string value = item.ReadSDCValueOf(sdcTypeValue); string urlFormat = "/SFC/Items/AjaxSetSDCValue?<itemID={0}&sdcTypeValue=" + SDCType.USER_CURRENT_OWNER+ "&value={0}"; return AjaxValue(item.ID, value, valuesKey, urlFormat, ajaxOnSuccess); }
實際上完全使用原來的函數AjaxValue也能實現,但是向使用者暴露了過多的技術細節(尤其是形成urlFormat的式子太複雜),完全是使用者不應該關心的。
/SFC/items/AjaxSetSDCValue會把Item的USER_CURRENT_OWNER欄位設定為value的值。
彈出的複雜菜單哪來的?我們總結了一下,發現AjaxValue大致可能彈出以下幾種東西:
1. 最基本的一串字串(原來那個string[],可以處理可用的預估值、剩餘時間值、花費時間值……)
2. 我的組的人員(分配任務、分配Bug、安排值班、安排休假……)
3. ……
urlFormat不同點擊後的功能就不同,但上述菜單的結構和外觀每次都相同。
所以就做了一個AjaxController.PopupAjaxValuesMenu,會根據傳進來的AjaxValue.ValuesKey這個enum來判斷彈出哪個菜單(包括準備菜單中的資料),順便把urlFormat也傳進去。
回顧
設計和調試這一堆東西還是花了2天時間的,但是從此再也不用見到大約每次都要50行左右的C#/Html代碼了。
當未來的維護者看到這段代碼時:
@SFCUI.AjaxSDCValue(Story, SDCType.USER_CURRENT_OWNER, AjaxValue.ValuesKeys.UsersMyTeams, ajaxOnSuccess: "function() { refreshStoryPad(" + Story.ID + "); }", imageUrl: "/SFC/Users/Details16.png")
他會想:“哦,這裡要把Story的CurrentOwner調整為……我的Team中的一個人,如果成功了會來重新整理這條故事。而且,前面似乎還顯示了個頭像表徵圖!”
這比面對50行漫天飛的代碼要舒服多了。
寫到最後,突然想起來10年前總結過的另一句話:
封裝的最高境界,就是用和自然語言一樣多的文字寫完調用代碼,剩下的全部隱匿起來。