火星人代码示例:系统设置(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行代码就能调用出来(故事树、组织结构图、燃尽图、全部菜单(每一个菜单都是延迟加载的)……)

这种习惯一旦养成了,代码就会愈来愈精练,而编写过程也愈来愈简单。

相关文章
相关标签/搜索