火星人代碼示例:系統設置(asp.net MVC3中View的複用示例)

功能簡介

最終功能如圖:
 html

上面一行兩張圖,是火星人的用戶故事樹配置界面,在每一個用戶故事的後面都有一個按鈕(懸停可見),點擊後出現操做菜單,其中一部分是新建下級故事菜單。web

若用戶選擇左側,菜單上只包括一個項目「通用故事」;若選擇右側,則包括不少故事(受當前故事類型的約束,這個比較複雜之後再說)。sql

這段代碼,等一下將會出現關鍵字「StoryTreeType」,左側叫作「Simple」(簡單樹),右側叫作「Leveled」(等級樹)。數據庫

 

下面一行兩張圖,是火星人的狀態鏈配置界面,在上面提到的操做菜單上,除了能新建故事以外,還能將當前故事轉移到另一個狀態。編程

若用戶選擇左側,菜單上只包括與開發相關的狀態(新建-待開發-開發中-開發完畢-部署完畢);作選擇右側,則會出現全部狀態(新建後有審批等環節,而部署過程也包括多個狀態)。ide

 

這段代碼,等一下將會出現關鍵字「StatusList」,左側叫作「DevelopmentOnly」(僅包含研發狀態),右側叫作「All」(全部)。學習


很顯然,不僅是這兩排界面很相似,這四個界面和背後的模型都很是相近,下面談談如何以最小代碼實現這個配置功能。測試


 

開發過程

Controller部分的代碼略過,重點看Model和View的封裝。this

第一步:開發出StoryTreeType部分的Model代碼

  
  
  
  
  1. public partial class Product  
  2. {  
  3.     public const string UserDefaultProductIDKey = "DefaultProductID";  
  4.  
  5.     //StoryTree type (Simple, Leveled, etc.)  
  6.     public const string StoryTreeTypeKey = "StoryTreeType";  
  7.     public enum StoryTreeTypes  
  8.     {  
  9.         Simple = 0,  
  10.         Leveled = 1  
  11.     }  
  12.  
  13.     public static readonly StoryTreeTypes[] StoryTreeTypeValues = { StoryTreeTypes.Simple, StoryTreeTypes.Leveled };  
  14.     public static readonly string[] StoryTreeTypeTexts = { "缺省(使用簡單父子關係造成故事樹)""使用系統定義的故事等級造成故事樹" };  
  15.  
  16.     public StoryTreeTypes StoryTreeType  
  17.     {  
  18.         get { return (StoryTreeTypes)Config.ReadValueAsInt(StoryTreeTypeKey, "$" + ID); }  
  19.     }  
  20.  
  21.     public string StoryTreeTypeText  
  22.     {  
  23.         get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StoryTreeTypeKey)]; }  
  24.     }  

注意這段代碼裏邊有一個叫作Config的類,它負責把不一樣的配置寫到數據庫中的一個公共表裏邊,所以爲了完成這個功能,咱們並不須要討論數據存儲問題。編碼

 

這得益於火星人以前已經封裝好的衆多功能。
 

第二步:實現StoryTreeType的View

 

注意下面的代碼,已經將StroyTreeType的兩種類型進行了Foreach循環處理,而不是寫死在裏邊。

有時候會以爲只有兩種,還作什麼循環,但若是不循環就須要兩段很接近的代碼,調試和維護都很費勁。並且一旦養成這種習慣,很容易把整個軟件都寫散了。

  
  
  
  
  1. @foreach (var type in Product.StoryTreeTypeValues)  
  2. {  
  3.     <td style = "border: none; ">  
  4.         <div class = "help-sample">  
  5.             <table>  
  6.                 <tr>  
  7.                     <td style = "border: none; width: 500px; ">  
  8.                         @MFCUI.Image("""/Products/StoryTree/Index16.png") <b>@Product.StoryTreeTypeTexts[(int)type] &nbsp;&nbsp;&nbsp;&nbsp;</b>  
  9.                         @if (Model.StoryTreeType == type)  
  10.                         {  
  11.                             <b>[當前設置]</b>  
  12.                         }  
  13.                         else 
  14.                         {  
  15.                             @MFCUI.Link("[啓用]""/MFC/Configs/AjaxSet?key=" + Product.StoryTreeTypeKey + "&value=" + (int)type + "&user=$" + Model.ID, returnTo: this)  
  16.                             @: &nbsp; &nbsp; &nbsp; &nbsp;  
  17.                         }  
  18.                         <table>  
  19.                             <tr>  
  20.                                 <td style = "border: none; width: 200px; ">  
  21.                                     @RenderPage("~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeTypes/_" + Model.StoryTreeType + ".cshtml")  
  22.                                 </td>  
  23.                                 <td style = "border: none; ">  
  24.                                     @MFCUI.Image("""/Products/Products/ManagementMethods/_" + type + "Example.png") <br/><br/>  
  25.                                 </td>  
  26.                             </tr>  
  27.                         </table>  
  28.                     </td>  
  29.                 </tr>  
  30.             </table>  
  31.         </div>  
  32.     </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部分的代碼是直接拷貝、粘貼、修改出來的,它們是「草稿代碼」,用來觀察封裝要點的。往後將被取代。

  
  
  
  
  1. public partial class Product  
  2. {  
  3.     public const string UserDefaultProductIDKey = "DefaultProductID";  
  4.  
  5.     //StoryTree type (Simple, Leveled, etc.)  
  6.     public const string StoryTreeTypeKey = "StoryTreeType";  
  7.     public enum StoryTreeTypes  
  8.     {  
  9.         Simple = 0,  
  10.         Leveled = 1  
  11.     }  
  12.  
  13.     public static readonly StoryTreeTypes[] StoryTreeTypeValues = { StoryTreeTypes.Simple, StoryTreeTypes.Leveled };  
  14.     public static readonly string[] StoryTreeTypeTexts = { "缺省(使用簡單父子關係造成故事樹)""使用系統定義的故事等級造成故事樹" };  
  15.  
  16.     public StoryTreeTypes StoryTreeType  
  17.     {  
  18.         get { return (StoryTreeTypes)Config.ReadValueAsInt(StoryTreeTypeKey, "$" + ID); }  
  19.     }  
  20.  
  21.     public string StoryTreeTypeText  
  22.     {  
  23.         get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StoryTreeTypeKey)]; }  
  24.     }  
  25.  
  26.     //Status list type (DevelopmentOnly, Allowed, etc.)  
  27.     public const string StatusListTypeKey = "StatusListType";  
  28.     public enum StatusListTypes  
  29.     {  
  30.         DevelopmentOnly = 0,  
  31.         Allowed = 1  
  32.     }  
  33.  
  34.     public static readonly StatusListTypes[] StatusListTypeValues = { StatusListTypes.DevelopmentOnly, StatusListTypes.Allowed };  
  35.     public static readonly string[] StatusListTypeTexts = { "缺省(只顯示開發相關的狀態)""使用用戶自定義的容許狀態" };  
  36.  
  37.     public StatusListTypes StatusListType  
  38.     {  
  39.         get { return (StatusListTypes)Config.ReadValueAsInt(StatusListTypeKey, "$" + ID); }  
  40.     }  
  41.  
  42.     public string StatusListTypeText  
  43.     {  
  44.         get { return StoryTreeTypeTexts[Config.ReadValueAsInt(StatusListTypeKey)]; }  
  45.     }  

第四步:將StoryTreeType和StatusList改寫爲一個基類的派生類

說實話,這個改寫過程失敗了,5分鐘後發現,由於每行代碼都有不一樣之處,即便改寫成功,初始化代碼不比這些代碼少。

 

並且還要冒着放棄enum的風險,因此終止了改寫計劃。
 

第五步:將處理StoryTreeType的View改寫爲同時能夠處理StatusList的

不少新手在這個時候可能會直接開始動手,但下面介紹一下一個小技巧:

1. 先開闢一個第二戰場:

  
  
  
  
  1. <table class = "noborder">  
  2.     <tr>  
  3.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")  
  4.     </tr>  
  5.     <tr>  
  6.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml", Product.StoryTreeTypeValues)  
  7.     </tr>  
  8. </table

下面的_StoryTreeType1.cshtml是拷貝出來的,將顯示在原來頁面的下面,這樣能夠修改的同時能夠觀察新舊代碼及其效果。

2. 一點點把StoryTreeType1中的StoryTreeType的影子抹掉

所謂影子,就是直接寫着「StoryTreetype」而非一個變量的地方。固然,每抹掉一個,就要多傳入一個參數。這裏用的是PageData[]參數(MVC3新出現的)。

注意抹一點測試一下,遇到問題越早越好。


 

最後View的內部變成(注意徹底看不到任何和StoryTreeType相關的痕跡了):

  
  
  
  
  1. @foreach (var currentConfig in PageData[0])  
  2. {  
  3.     <td style = "border: none; ">  
  4.         <div class = "help-sample">  
  5.             <table>  
  6.                 <tr>  
  7.                     <td style = "border: none; width: 500px; ">  
  8.                         @MFCUI.Image("", PageData[1]) <b>@PageData[2][(int)currentConfig] &nbsp;&nbsp;&nbsp;&nbsp;</b>  
  9.                         @if (PageData[3] == currentConfig)  
  10.                         {  
  11.                             <b>[當前設置]</b>  
  12.                         }  
  13.                         else 
  14.                         {  
  15.                             @MFCUI.Link("[啓用]""/MFC/Configs/AjaxSet?key=" + PageData[4] + "&value=" + (int)currentConfig + "&user=$" + Model.ID, returnTo: this)  
  16.                             @: &nbsp; &nbsp; &nbsp; &nbsp;  
  17.                         }  
  18.                         <table>  
  19.                             <tr>  
  20.                                 <td style = "border: none; width: 200px; ">  
  21.                                     @RenderPage(PageData[5])  
  22.                                 </td>  
  23.                                 <td style = "border: none; ">  
  24.                                     @MFCUI.Image("", Page[6] + "_" + currentConfig + ".png") <br/><br/>  
  25.                                 </td>  
  26.                             </tr>  
  27.                         </table>  
  28.                     </td>  
  29.                 </tr>  
  30.             </table>  
  31.         </div>  
  32.     </td>  

而接口也變成:

  
  
  
  
  1. <tr>  
  2.     @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")  
  3. </tr>  
  4. <tr>  
  5.     @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml",   
  6.         Product.StoryTreeTypeValues, "/Products/StoryTree/Index16.png", Product.StoryTreeTypeTexts, Model.StoryTreeType, Product.StoryTreeTypeKey,  
  7.         "~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeType/_" + Model.StoryTreeType + ".cshtml",  
  8.         "/Products/Products/ManagementMethods/")  
  9. </tr> 

注意看下面「第二戰場」出現了不少輸入參數。

 

從外觀看,上下兩個View的顯示效果徹底相同(就不貼圖了)。


 

第六步:加入對StatusList的處理

刪除第一個tr的代碼,拷貝出來一個處理StatusListType的tr,並逐步修改使之能夠工做:

  
  
  
  
  1. <table class = "noborder">  
  2.     <tr>  
  3.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethod/_StoryTreeType1.cshtml",   
  4.             Product.StoryTreeTypeValues, "/Products/StoryTree/Index16.png", Product.StoryTreeTypeTexts, Model.StoryTreeType, Product.StoryTreeTypeKey,  
  5.             "~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeType/",  
  6.             "/Products/Products/ManagementMethod/StoryTreeType/")  
  7.     </tr>  
  8.     <tr>  
  9.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethod/_StoryTreeType1.cshtml",   
  10.             Product.StatusListTypeValues, "/MFC/Statuses/Index16.png", Product.StatusListTypeTexts, Model.StatusListType, Product.StatusListTypeKey,  
  11.             "~/Areas/DLC/Views/Products/ManagementMethod/StatusListType/",  
  12.             "/Products/Products/ManagementMethod/StatusListType/")  
  13.     </tr>  
  14. </table

有幾個小技巧:
1. 修改過程當中,應該修改一個參數就查看一下是否還工做。

2. 優先修改那些不太會致使錯誤的數據,好比能夠先修改"/MFC/Statuses/Index16.png", Product.StatusListTypeTexts這兩個參數,由於他們是文字,基本上不會致使錯誤。

看看最後結果(由於缺乏兩個圖片,因此屏幕顯示有問題):

 

後續及總結

後續

作完這些,_StoryTreeType1.cshtml這個文件名已經不妥了,由於它能夠1行代碼完成任何系統配置(包含一個標題,若干可選項,一組描述,一張圖片),因此下一步須要修改它的名字,而且放到合適的地方。
不過就咱們本身的習慣而言,咱們會先改動名字,而後就放在原來使用它的地方。直到下一次真正被從新使用,再商量放在什麼地方合適。
這句話的官方說法叫作「Use it before reusing it「,若是它如今就管這四個界面,就讓它好好管着,等往後再說。

總結

整個過程包括寫做本博客,大約耗時2小時。
可能有人會問:不過就是4個頁面嘛,就是拷貝粘貼,也用不了1個小時啊,爲何要這麼費勁?
有這麼幾個緣由:
1. 將來咱們大約會有20多個這樣的設置頁面,按上面介紹的封裝,每生成一頁只須要在View中添加一行代碼。
2. 因爲咱們尚未美術人員,因此將來可能會改動頁面效果,而這些頁面都須要保持相同的風格。
3. 將來技術上可能也會作一些改動,好比那個Config,這些改動都不但願去修改不少代碼。

其實,整個火星人的產品,就是在這種積木代碼中產生的,有不少意想不到的地方都是隻要1~2行代碼就能調用出來(故事樹、組織結構圖、燃盡圖、全部菜單(每一個菜單都是延遲加載的)……)

這種習慣一旦養成了,代碼就會愈來愈精練,而編寫過程也愈來愈簡單。

相關文章
相關標籤/搜索