功能簡介
最終功能如圖:
上面一行兩張圖,是火星人的用戶故事樹配置界面,在每個用戶故事的後面都有一個按鈕(懸停可見),點擊後出現操作菜單,其中一部分是新建下級故事菜單。
若用戶選擇左側,菜單上只包括一個項目“通用故事”;若選擇右側,則包括很多故事(受當前故事類型的約束,這個比較複雜以後再說)。
這段代碼,等一下將會出現關鍵字“StoryTreeType”,左側叫做“Simple”(簡單樹),右側叫做“Leveled”(等級樹)。
下面一行兩張圖,是火星人的狀態鏈配置界面,在上面提到的操作菜單上,除了能新建故事之外,還能將當前故事轉移到另外一個狀態。
若用戶選擇左側,菜單上只包括與開發相關的狀態(新建-待開發-開發中-開發完畢-部署完畢);做選擇右側,則會出現所有狀態(新建後有審批等環節,而部署過程也包括多個狀態)。
這段代碼,等一下將會出現關鍵字“StatusList”,左側叫做“DevelopmentOnly”(僅包含研發狀態),右側叫做“All”(所有)。
很顯然,不只是這兩排界面很類似,這四個界面和背後的模型都非常相近,下面談談如何以最小代碼實現這個配置功能。
開發過程
Controller部分的代碼略過,重點看Model和View的封裝。
第一步:開發出StoryTreeType部分的Model代碼
- public partial class Product
- {
- public const string UserDefaultProductIDKey = "DefaultProductID";
- //StoryTree type (Simple, Leveled, etc.)
- public const string StoryTreeTypeKey = "StoryTreeType";
- public enum StoryTreeTypes
- {
- Simple = 0,
- Leveled = 1
- }
- public static readonly StoryTreeTypes[] StoryTreeTypeValues = { StoryTreeTypes.Simple, StoryTreeTypes.Leveled };
- public static readonly string[] StoryTreeTypeTexts = { "缺省(使用簡單父子關係形成故事樹)", "使用系統定義的故事等級形成故事樹" };
- public StoryTreeTypes StoryTreeType
- {
- get { return (StoryTreeTypes)Config.ReadValueAsInt(StoryTreeTypeKey, "$" + ID); }
- }
- public string StoryTreeTypeText
- {
- get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StoryTreeTypeKey)]; }
- }
- }
注意這段代碼裏邊有一個叫做Config的類,它負責把不同的配置寫到數據庫中的一個公共表裏邊,因此爲了完成這個功能,我們並不需要討論數據存儲問題。
這得益於火星人之前已經封裝好的衆多功能。
第二步:實現StoryTreeType的View
注意下面的代碼,已經將StroyTreeType的兩種類型進行了Foreach循環處理,而不是寫死在裏邊。
有時候會覺得只有兩種,還做什麼循環,但如果不循環就需要兩段很接近的代碼,調試和維護都很費勁。而且一旦養成這種習慣,很容易把整個軟件都寫散了。
- @foreach (var type in Product.StoryTreeTypeValues)
- {
- <td style = "border: none; ">
- <div class = "help-sample">
- <table>
- <tr>
- <td style = "border: none; width: 500px; ">
- @MFCUI.Image("", "/Products/StoryTree/Index16.png") <b>@Product.StoryTreeTypeTexts[(int)type] </b>
- @if (Model.StoryTreeType == type)
- {
- <b>[當前設置]</b>
- }
- else
- {
- @MFCUI.Link("[啓用]", "/MFC/Configs/AjaxSet?key=" + Product.StoryTreeTypeKey + "&value=" + (int)type + "&user=$" + Model.ID, returnTo: this)
- @:
- }
- <table>
- <tr>
- <td style = "border: none; width: 200px; ">
- @RenderPage("~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeTypes/_" + Model.StoryTreeType + ".cshtml")
- </td>
- <td style = "border: none; ">
- @MFCUI.Image("", "/Products/Products/ManagementMethods/_" + type + "Example.png") <br/><br/>
- </td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- </div>
- </td>
- }
注意
1. 這段代碼裏邊有一個叫做“/MFC/Configs/AjaxSet?..."的調用,這個調用將直接完成設置工作(寫入數據庫),並立刻刷新當前頁(注意有個“returnTo: this,是火星人中回到當前頁的封裝)。
2. 最上面的標題(“缺省(使用簡單父子關係形成故事樹)”和“使用系統定義的故事等級形成故事樹”)、圖片(最下面一個@MFCUI.Image())都是在這個頁面寫出來的
3. 兩個RenderPage用於顯示“優點”“缺點”“建議”這些差別比較大的文字,分別存儲在兩個文件裏邊,文件名是在RenderPage裏邊用Model.StoryTreeType拼裝出來的。
2和3表明了在MVC的View中的幾個很重要的封裝原則:
A. 相似的部分一定要For循環出來在一個View通過拼接中解決
B. 略微不同的參數使用變量拼接出來
C. 圖片、Partial View的命名要與變量對應,這樣方便拼接
D. 最大的不同,使用Partial View來處理。
第三步:爲StatusList的Model部分“打草稿”
(寫這篇博客的時候,我的代碼剛剛寫到這裏,爲了能拷貝到一點“草稿代碼”,不等編碼得到驗證就開始寫了)
做了很多年的封裝,感覺最快速的方法,仍然是試探性封裝,也就是先寫出一個部分(如上面的StoryTreeType),然後拷貝另外一個相似的部分(如下面的StatusList),然後觀察其相似點和不同點,然後才進行封裝。
與直接在開頭就設計封裝相比,這種方法比較容易學習和接受,對人員的要求也相對較低。本人編程這麼多年,還是沒把握在所有情況下都面對空屏幕直接先寫底層,然後派生出子類。
注意StatusList部分的代碼是直接拷貝、粘貼、修改出來的,它們是“草稿代碼”,用來觀察封裝要點的。日後將被取代。
- public partial class Product
- {
- public const string UserDefaultProductIDKey = "DefaultProductID";
- //StoryTree type (Simple, Leveled, etc.)
- public const string StoryTreeTypeKey = "StoryTreeType";
- public enum StoryTreeTypes
- {
- Simple = 0,
- Leveled = 1
- }
- public static readonly StoryTreeTypes[] StoryTreeTypeValues = { StoryTreeTypes.Simple, StoryTreeTypes.Leveled };
- public static readonly string[] StoryTreeTypeTexts = { "缺省(使用簡單父子關係形成故事樹)", "使用系統定義的故事等級形成故事樹" };
- public StoryTreeTypes StoryTreeType
- {
- get { return (StoryTreeTypes)Config.ReadValueAsInt(StoryTreeTypeKey, "$" + ID); }
- }
- public string StoryTreeTypeText
- {
- get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StoryTreeTypeKey)]; }
- }
- //Status list type (DevelopmentOnly, Allowed, etc.)
- public const string StatusListTypeKey = "StatusListType";
- public enum StatusListTypes
- {
- DevelopmentOnly = 0,
- Allowed = 1
- }
- public static readonly StatusListTypes[] StatusListTypeValues = { StatusListTypes.DevelopmentOnly, StatusListTypes.Allowed };
- public static readonly string[] StatusListTypeTexts = { "缺省(只顯示開發相關的狀態)", "使用用戶自定義的允許狀態" };
- public StatusListTypes StatusListType
- {
- get { return (StatusListTypes)Config.ReadValueAsInt(StatusListTypeKey, "$" + ID); }
- }
- public string StatusListTypeText
- {
- get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StatusListTypeKey)]; }
- }
- }
第四步:將StoryTreeType和StatusList改寫爲一個基類的派生類
說實話,這個改寫過程失敗了,5分鐘後發現,因爲每行代碼都有不同之處,即使改寫成功,初始化代碼不比這些代碼少。
而且還要冒着放棄enum的風險,所以終止了改寫計劃。
第五步:將處理StoryTreeType的View改寫爲同時可以處理StatusList的
1. 先開闢一個第二戰場:
- <table class = "noborder">
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")
- </tr>
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml", Product.StoryTreeTypeValues)
- </tr>
- </table>
下面的_StoryTreeType1.cshtml是拷貝出來的,將顯示在原來頁面的下面,這樣可以修改的同時可以觀察新舊代碼及其效果。
2. 一點點把StoryTreeType1中的StoryTreeType的影子抹掉
所謂影子,就是直接寫着“StoryTreetype”而非一個變量的地方。當然,每抹掉一個,就要多傳入一個參數。這裏用的是PageData[]參數(MVC3新出現的)。
注意抹一點測試一下,遇到問題越早越好。
最後View的內部變成(注意完全看不到任何和StoryTreeType相關的痕跡了):
- @foreach (var currentConfig in PageData[0])
- {
- <td style = "border: none; ">
- <div class = "help-sample">
- <table>
- <tr>
- <td style = "border: none; width: 500px; ">
- @MFCUI.Image("", PageData[1]) <b>@PageData[2][(int)currentConfig] </b>
- @if (PageData[3] == currentConfig)
- {
- <b>[當前設置]</b>
- }
- else
- {
- @MFCUI.Link("[啓用]", "/MFC/Configs/AjaxSet?key=" + PageData[4] + "&value=" + (int)currentConfig + "&user=$" + Model.ID, returnTo: this)
- @:
- }
- <table>
- <tr>
- <td style = "border: none; width: 200px; ">
- @RenderPage(PageData[5])
- </td>
- <td style = "border: none; ">
- @MFCUI.Image("", Page[6] + "_" + currentConfig + ".png") <br/><br/>
- </td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- </div>
- </td>
- }
而接口也變成:
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")
- </tr>
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml",
- Product.StoryTreeTypeValues, "/Products/StoryTree/Index16.png", Product.StoryTreeTypeTexts, Model.StoryTreeType, Product.StoryTreeTypeKey,
- "~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeType/_" + Model.StoryTreeType + ".cshtml",
- "/Products/Products/ManagementMethods/")
- </tr>
注意看下面“第二戰場”出現了很多輸入參數。
從外觀看,上下兩個View的顯示效果完全相同(就不貼圖了)。
第六步:加入對StatusList的處理
刪除第一個tr的代碼,拷貝出來一個處理StatusListType的tr,並逐步修改使之可以工作:
- <table class = "noborder">
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethod/_StoryTreeType1.cshtml",
- Product.StoryTreeTypeValues, "/Products/StoryTree/Index16.png", Product.StoryTreeTypeTexts, Model.StoryTreeType, Product.StoryTreeTypeKey,
- "~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeType/",
- "/Products/Products/ManagementMethod/StoryTreeType/")
- </tr>
- <tr>
- @RenderPage("~/Areas/Products/Views/Products/SetManagementMethod/_StoryTreeType1.cshtml",
- Product.StatusListTypeValues, "/MFC/Statuses/Index16.png", Product.StatusListTypeTexts, Model.StatusListType, Product.StatusListTypeKey,
- "~/Areas/DLC/Views/Products/ManagementMethod/StatusListType/",
- "/Products/Products/ManagementMethod/StatusListType/")
- </tr>
- </table>
有幾個小技巧:
1. 修改過程中,應該修改一個參數就查看一下是否還工作。
2. 優先修改那些不太會導致錯誤的數據,比如可以先修改"/MFC/Statuses/Index16.png", Product.StatusListTypeTexts這兩個參數,因爲他們是文字,基本上不會導致錯誤。
看看最後結果(因爲缺少兩個圖片,所以屏幕顯示有問題):
後續及總結
後續
總結
其實,整個火星人的產品,就是在這種積木代碼中產生的,有很多意想不到的地方都是隻要1~2行代碼就能調用出來(故事樹、組織結構圖、燃盡圖、所有菜單(每個菜單都是延遲加載的)……)
這種習慣一旦養成了,代碼就會越來越精練,而編寫過程也越來越簡單。