功能简介

最终功能如图:
 

火星人代码示例:系统设置(asp.net MVC3中View的复用示例)_第1张图片

上面一行两张图,是火星人的用户故事树配置界面,在每个用户故事的后面都有一个按钮(悬停可见),点击后出现操作菜单,其中一部分是新建下级故事菜单。

若用户选择左侧,菜单上只包括一个项目“通用故事”;若选择右侧,则包括很多故事(受当前故事类型的约束,这个比较复杂以后再说)。

这段代码,等一下将会出现关键字“StoryTreeType”,左侧叫做“Simple”(简单树),右侧叫做“Leveled”(等级树)。

 

下面一行两张图,是火星人的状态链配置界面,在上面提到的操作菜单上,除了能新建故事之外,还能将当前故事转移到另外一个状态。

若用户选择左侧,菜单上只包括与开发相关的状态(新建-待开发-开发中-开发完毕-部署完毕);做选择右侧,则会出现所有状态(新建后有审批等环节,而部署过程也包括多个状态)。

 

这段代码,等一下将会出现关键字“StatusList”,左侧叫做“DevelopmentOnly”(仅包含研发状态),右侧叫做“All”(所有)。


很显然,不只是这两排界面很类似,这四个界面和背后的模型都非常相近,下面谈谈如何以最小代码实现这个配置功能。


 

开发过程

Controller部分的代码略过,重点看Model和View的封装。

第一步:开发出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.     "border: none; ">  
  4.         "help-sample">  
  5.             <table>  
  6.                   
  7.                     "border: none; width: 500px; ">  
  8.                         @MFCUI.Image("""/Products/StoryTree/Index16.png"@Product.StoryTreeTypeTexts[(int)type]       
  9.                         @if (Model.StoryTreeType == type)  
  10.                         {  
  11.                             [当前设置]  
  12.                         }  
  13.                         else 
  14.                         {  
  15.                             @MFCUI.Link("[启用]""/MFC/Configs/AjaxSet?key=" + Product.StoryTreeTypeKey + "&value=" + (int)type + "&user=$" + Model.ID, returnTo: this)  
  16.                             @:          
  17.                         }  
  18.                         <table>  
  19.                               
  20.                                 "border: none; width: 200px; ">  
  21.                                     @RenderPage("~/Areas/DLC/Views/Products/ManagementMethod/StoryTreeTypes/_" + Model.StoryTreeType + ".cshtml")  
  22.                                   
  23.                                 "border: none; ">  
  24.                                     @MFCUI.Image("""/Products/Products/ManagementMethods/_" + type + "Example.png"

     
  25.                                   
  26.                               
  27.                         table>  
  28.                       
  29.                   
  30.             table>  
  31.         
 
  •       
  • 注意

     

    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.       
    3.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType.cshtml")  
    4.       
    5.       
    6.         @RenderPage("~/Areas/Products/Views/Products/SetManagementMethods/_StoryTreeType1.cshtml", Product.StoryTreeTypeValues)  
    7.       
    8. table

    下面的_StoryTreeType1.cshtml是拷贝出来的,将显示在原来页面的下面,这样可以修改的同时可以观察新旧代码及其效果。

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

    所谓影子,就是直接写着“StoryTreetype”而非一个变量的地方。当然,每抹掉一个,就要多传入一个参数。这里用的是PageData[]参数(MVC3新出现的)。

    注意抹一点测试一下,遇到问题越早越好。


     

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

       
       
       
       
    1. @foreach (var currentConfig in PageData[0])  
    2. {  
    3.     "border: none; ">  
    4.         "help-sample">  
    5.             <table>  
    6.                   
    7.                     "border: none; width: 500px; ">  
    8.                         @MFCUI.Image("", PageData[1]) @PageData[2][(int)currentConfig]       
    9.                         @if (PageData[3] == currentConfig)  
    10.                         {  
    11.                             [当前设置]  
    12.                         }  
    13.                         else 
    14.                         {  
    15.                             @MFCUI.Link("[启用]""/MFC/Configs/AjaxSet?key=" + PageData[4] + "&value=" + (int)currentConfig + "&user=$" + Model.ID, returnTo: this)  
    16.                             @:          
    17.                         }  
    18.                         <table>  
    19.                               
    20.                                 "border: none; width: 200px; ">  
    21.                                     @RenderPage(PageData[5])  
    22.                                   
    23.                                 "border: none; ">  
    24.                                     @MFCUI.Image("", Page[6] + "_" + currentConfig + ".png"

       
    25.                                   
    26.                               
    27.                         table>  
    28.                       
    29.                   
    30.             table>  
    31.