天天看點

火星人代碼示例:系統設定(asp.net MVC3中View的複用示例)功能簡介 開發過程 後續及總結

最終功能如圖:

<a href="http://blog.51cto.com/attachment/201212/223213225.png" target="_blank"></a>

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

若使用者選擇左側,菜單上隻包括一個項目“通用故事”;若選擇右側,則包括很多故事(受目前故事類型的限制,這個比較複雜以後再說)。

這段代碼,等一下将會出現關鍵字“StoryTreeType”,左側叫做“Simple”(簡單樹),右側叫做“Leveled”(等級樹)。

下面一行兩張圖,是火星人的狀态鍊配置界面,在上面提到的操作菜單上,除了能建立故事之外,還能将目前故事轉移到另外一個狀态。

若使用者選擇左側,菜單上隻包括與開發相關的狀态(建立-待開發-開發中-開發完畢-部署完畢);做選擇右側,則會出現所有狀态(建立後有審批等環節,而部署過程也包括多個狀态)。

這段代碼,等一下将會出現關鍵字“StatusList”,左側叫做“DevelopmentOnly”(僅包含研發狀态),右側叫做“All”(所有)。

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

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

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的類,它負責把不同的配置寫到資料庫中的一個公共表裡邊,是以為了完成這個功能,我們并不需要讨論資料存儲問題。

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

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

有時候會覺得隻有兩種,還做什麼循環,但如果不循環就需要兩段很接近的代碼,調試和維護都很費勁。而且一旦養成這種習慣,很容易把整個軟體都寫散了。

@foreach (var type in Product.StoryTreeTypeValues)  

    &lt;td style = "border: none; "&gt;  

        &lt;div class = "help-sample"&gt;  

            &lt;table&gt;  

                &lt;tr&gt;  

                    &lt;td style = "border: none; width: 500px; "&gt;  

                        @MFCUI.Image("", "/Products/StoryTree/Index16.png") &lt;b&gt;@Product.StoryTreeTypeTexts[(int)type] &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;  

                        @if (Model.StoryTreeType == type)  

                        {  

                            &lt;b&gt;[目前設定]&lt;/b&gt;  

                        }  

                        else 

                            @MFCUI.Link("[啟用]", "/MFC/Configs/AjaxSet?key=" + Product.StoryTreeTypeKey + "&amp;value=" + (int)type + "&amp;user=$" + Model.ID, returnTo: this)  

                            @: &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;  

                        &lt;table&gt;  

                            &lt;tr&gt;  

                                &lt;td style = "border: none; width: 200px; "&gt;  

                                    @RenderPage("~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeTypes/_" + Model.StoryTreeType + ".cshtml")  

                                &lt;/td&gt;  

                                &lt;td style = "border: none; "&gt;  

                                    @MFCUI.Image("", "/Products/Products/ManagementMethods/_" + type + "Example.png") &lt;br/&gt;&lt;br/&gt;  

                            &lt;/tr&gt;  

                        &lt;/table&gt;  

                    &lt;/td&gt;  

                &lt;/tr&gt;  

            &lt;/table&gt;  

        &lt;/div&gt;  

    &lt;/td&gt;  

注意

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來處理。

(寫這篇部落格的時候,我的代碼剛剛寫到這裡,為了能拷貝到一點“草稿代碼”,不等編碼得到驗證就開始寫了)

做了很多年的封裝,感覺最快速的方法,仍然是試探性封裝,也就是先寫出一個部分(如上面的StoryTreeType),然後拷貝另外一個相似的部分(如下面的StatusList),然後觀察其相似點和不同點,然後才進行封裝。

與直接在開頭就設計封裝相比,這種方法比較容易學習和接受,對人員的要求也相對較低。本人程式設計這麼多年,還是沒把握在所有情況下都面對空螢幕直接先寫底層,然後派生出子類。

注意StatusList部分的代碼是直接拷貝、粘貼、修改出來的,它們是“草稿代碼”,用來觀察封裝要點的。日後将被取代。

    //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)]; }  

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

而且還要冒着放棄enum的風險,是以終止了改寫計劃。

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

&lt;table class = "noborder"&gt;  

    &lt;tr&gt;  

        @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")  

    &lt;/tr&gt;  

        @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml", Product.StoryTreeTypeValues)  

&lt;/table&gt; 

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

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

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

最後View的内部變成(注意完全看不到任何和StoryTreeType相關的痕迹了):

@foreach (var currentConfig in PageData[0])  

                        @MFCUI.Image("", PageData[1]) &lt;b&gt;@PageData[2][(int)currentConfig] &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/b&gt;  

                        @if (PageData[3] == currentConfig)  

                            @MFCUI.Link("[啟用]", "/MFC/Configs/AjaxSet?key=" + PageData[4] + "&amp;value=" + (int)currentConfig + "&amp;user=$" + Model.ID, returnTo: this)  

                                    @RenderPage(PageData[5])  

                                    @MFCUI.Image("", Page[6] + "_" + currentConfig + ".png") &lt;br/&gt;&lt;br/&gt;  

而接口也變成:

&lt;tr&gt;  

    @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")  

&lt;/tr&gt;  

    @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/")  

&lt;/tr&gt; 

注意看下面“第二戰場”出現了很多輸入參數。

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

删除第一個tr的代碼,拷貝出來一個處理StatusListType的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/")  

            Product.StatusListTypeValues, "/MFC/Statuses/Index16.png", Product.StatusListTypeTexts, Model.StatusListType, Product.StatusListTypeKey,  

            "~/Areas/DLC/Views/Products/ManagementMethod/StatusListType/",  

            "/Products/Products/ManagementMethod/StatusListType/")  

有幾個小技巧:

1. 修改過程中,應該修改一個參數就檢視一下是否還工作。

2. 優先修改那些不太會導緻錯誤的資料,比如可以先修改"/MFC/Statuses/Index16.png", Product.StatusListTypeTexts這兩個參數,因為他們是文字,基本上不會導緻錯誤。

看看最後結果(因為缺少兩個圖檔,是以螢幕顯示有問題):

<a href="http://blog.51cto.com/attachment/201212/223250504.png" target="_blank"></a>

做完這些,_StoryTreeType1.cshtml這個檔案名已經不妥了,因為它可以1行代碼完成任何系統配置(包含一個标題,若幹可選項,一組描述,一張圖檔),是以下一步需要修改它的名字,并且放到合适的地方。

不過就我們自己的習慣而言,我們會先改動名字,然後就放在原來使用它的地方。直到下一次真正被重新使用,再商量放在什麼地方合适。

這句話的官方說法叫做“Use it before reusing it“,如果它現在就管這四個界面,就讓它好好管着,等日後再說。

整個過程包括寫作本部落格,大約耗時2小時。

可能有人會問:不過就是4個頁面嘛,就是拷貝粘貼,也用不了1個小時啊,為什麼要這麼費勁?

有這麼幾個原因:

1. 未來我們大約會有20多個這樣的設定頁面,按上面介紹的封裝,每生成一頁隻需要在View中添加一行代碼。

2. 由于我們還沒有美術人員,是以未來可能會改動頁面效果,而這些頁面都需要保持相同的風格。

3. 未來技術上可能也會做一些改動,比如那個Config,這些改動都不希望去修改很多代碼。

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

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

本文轉自火星人陳勇 51CTO部落格,原文連結:http://blog.51cto.com/cheny/1101507