NerdDinner分析
NerdDinner的一个优点就是接口设计非常恰当,我觉得编程最有搞头的可能就是接口设计,很多“问题代码”都是接口设计不当造成的。
创建与编辑的方法接口对比:
创建:public ActionResult Create(Dinner dinner)
编辑:public ActionResult Edit(int id, FormCollection collection)
为什么Edit不用Edit (Dinner dinner)这种看起来更简洁的写法呢?首先,Edit页最后是要保存数据的,但这些数据往往不是在页面上完全包含,比如创建日期一般就不会放在编辑页面上。所以Edit方法不能用类似Create的那种接口,而是先根据id来从数据源取出Dinner对象,然后用UpdateModel(dinner);方法更新那些在编辑页面上有的字段,然后再保存到数据源。
接口举例
比如public ActionResult ViewErrorLog(ViewErrorSearchOption searchOption)就是很好的接口设计,因为一进入这个方法就能直接用参数了,public ActionResult CalendarAdmin(FormCollection formCollection)可能是不太好的,是可以进一步改进的接口,因为首先,这个方法的功能不单一,是一碗意大利面(意大利面主要原因并不是某种编程语言(比如Basic)造成的,而是编程者的编程思想,高手即使用汇编也能写出比较容易读懂的代码),里面用了大量的formCollection["XXX"]形式的代码来一个一个获取参数,缺乏对输入参数的抽象。解决方法是从2方面,一是“拆”,即页面改造,用Ajax+PartialView对功能做拆分,二是抽象,对输入和输出参数做抽象,使参数获取和使用简单化。
当一个页面简单的时候,这个页面可能适合整体做,不需要做什么功能拆分和Ajax,比如ViewErrorLog页面。但当页面包含了搜索条件、显示搜索结果、编辑保存等众多功能时,必须拆。拆了会变成几个方块,比如BusinessBranchAdmin。不拆就会变成意大利面,比如CalendarAdmin没有拆。注意BusinessBranchAdmin代码仍然较多,主要是页面上有级联下拉框和较多的页面元素,这里面有很多都是可以进一步优化的,要解决的主要问题是功能重复的时候,代码没有设计成可重用,这其实在开发过程中是正常现象,因为不可能一下子就设计出一个最优的系统,但一直保持这样,一直不去优化,就会给进一步开发及后期维护工作带来巨大工作量和Bug,经常否定自我,优化老代码才能走的顺。
接口代码分析
BusinessBranchAdmin里分成3个方法:
1. public ActionResult BusinessBranchAdmin()方法处理带有BusinessId参数的Get请求,这是一种容错的写法,当没有参数的时候,它会显示NotFound页面,现在感觉是多此一举了,用public ActionResult BusinessBranchAdmin(decimal businessId)更合适,因为这是后台代码,后台一般人不会乱弄个Url乱搞的。正常情况下return View(formModel);
2. public ActionResult Edit(int BusinessBranchId)处理获取Get请求,它由客户端的Ajax代码showEdit方法触发,正常情况下它返回的是一个PartialView:return PartialView("BusinessBranchForm" , info);不过我检查代码时发现了笔误,写的是return View("BusinessBranchForm", info);不过两种写法都能正常运行,不过因为BusinessBranchForm是用户控件,所以用PartialView更合适。数据返回浏览器后由onGetDetailComplete方法处理,显示在divEdit里。
3. public ActionResult Edit(int BusinessBranchId, FormCollection formValues)处理Post请求,由客户端postEditForm方法触发,这个方法里用到了通用提交表单的postForm方法。服务器端处理成功后会return PartialView("EditSuccess");失败会设置错误信息并return PartialView("BusinessBranchForm", info);
整个页面的JS只有非常简短的,一目了然的,可望词生义的5个函数,共25行。
CalendarAdmin里有很多方法,拿最重要的前2个方法为例:
1. public ActionResult CalendarAdmin(),这个方法处理Get请求,既处理最初显示搜索条件的任务,又处理后续的根据参数查询数据的任务,以及分页逻辑等等众多功能,非常不容易看明白。有几个编码问题比较突出:一个是获取参数的GetParamters方法,里面全部是out参数,还有些方法里会改变某些参数的值,这都不是一种好的接口设计,要解决这个问题,最管用的还是“拆”字诀,然后对输入输出数据做抽象。
2. public ActionResult CalendarAdmin(FormCollection formCollection)处理Post请求,处理首次检索任务,里面有非常多与第一个方法相同的逻辑功能代码。我发现里面有一个意大利面的铁证:Goto语句,可见逻辑之复杂。解决方法还是“拆”字诀,拆才能简单化,拆才能清晰易懂。
Html结构分析
结构良好的:
CSS:
<style type="text/css">
.outputTable
{
background-color: White;
width: 100%;
}
.outputTable tr
{
height: 25px;
}
.outputTable th
{
background-color: #C0D6F6;
}
.outputTable td
{
background-color: #ECF3FD;
}
</style>
Html:
<table border="0" cellpadding="3" cellspacing="2" id="divCalendarList" class="outputTable">
<%--<caption>
Search Result</caption>--%>
<thead>
<tr>
<th>dgtStartDateTime
</th>
</tr>
</thead>
<tbody>
<% foreach (SIL.RealStew.DAL.Entities.BusinessBranch c in Model.BusinessBranches)
{ %>
<tr>
<td>
<a href="javascript:showEdit(<%= c.BuBrId %>)">BuBrName</a>
</td>
</tr>
<% } %></tbody>
</table>
结构不好的(class="bold", height="25",bgcolor="#C0D6F6"之类的完全可以写在CSS里):
<table border="0" cellpadding="3" bgcolor="#ffffff" cellspacing="2" id="divCalendarList" style="width: 100%">
<tr class="bold">
<td height="25" bgcolor="#C0D6F6">
<%= Html.Resource("dgtStartDateTime")%>
</td>
</tr>
<% foreach (SIL.RealStew.BLL.Home.Model.CalendarViewEntry c in Model.CalendarList)
{ %>
<tr bgcolor="#ECF3FD" >
<td height="25">
<%= c.CaEnStartDate == null ? "" : ConvertDateTime.ConvertToCurrentDatetimeFormat(c.CaEnStartDate.Value.Date.ToString()) + (c.CaEnStartTime == "00:00" ? "" : "<br>" + c.CaEnStartTime)%>
</td>
</tr>
<% } %>
</table>
多语言代码分析
1. Global.asax里的protected void Application_BeginRequest(object sender, EventArgs e)方法
2. Language类EnvironmentLanguage属性
3. EnquiryLoadManager类的GetTradeDetail方法的language参数可以省去,方法里用语言的时候用Language.EnvironmentLanguage就行了,不需要从外面传进来。需要用Culture的地方,比如格式化日期、货币的地方,用CultureInfo.CurrentCulture就行了。
编程风格分析
1. 最常见的问题是超长的行,比如memberCallCentreModel.CountryList = new SelectList(countryEntityList as IEnumerable, "CountryID", "CountryName", memberCallCentreModel.SearchCountryID);
如果写成以下风格会更有可读性:
memberCallCentreModel.CountryList =
new SelectList(countryEntityList as IEnumerable
, "CountryID"
, "CountryName"
, memberCallCentreModel.SearchCountryID);
2. 代码(特别是Html)的缩进和空行特别乱,就不举例了
3. JS的存放,我并不认为所有的JS都应该放到单独的JS文件,首先有很多JS代码都会用到服务器端变量,直接在页面里写会更方便,假如代码太多,放在页面上就会影响视线了,不容易弄清楚页面结构,适合放在单独的JS文件里。少量代码且用到服务器端变量的适合直接放在页面上,如果不论什么都放到JS文件里,则会增加文件的数量,万一有什么修改的话,还需要打开2个文件对比着看,增加工作量。JS文件缓存可以节省流量,提高速度,但现在带宽都挺高的,一小段JS并不会带来性能问题,反而可以减少请求数,至少能在首次载入时增加速度。
4. int pageSize = int.Parse(ConfigurationManager.AppSettings["PageSize"]);这是典型的HardCoding形式,不利于维护,用我写的SolutionConfig.PageSize可以避免之。
5. 无效代码:即被注释掉的代码,是很影响视线的。我的习惯是在重构时将过时的代码注释掉,当重构完成时,删掉其中的大部分,只留极少数有参考价值的代码。然后每次看到漏删的完全没用的无效代码时,彻底删除之。
通用级连下拉框代码的分析
1. 本示例代码的数据源是Xml文件,集成到数据库里需要做适当改动
2. 在Web.config里配置级联下拉框的Key与Key的层次结构以及Key与数据提供类的映射。
3. 顶级下拉框基础数据提供类需要继承SelectListProviderBase,
比如public class Countries : SelectListProviderBase
4. 有父亲的数据提供类要继承HasParentBase,
比如Cities,Suburbs,Streets
5. 取下拉框数据用DdlFormModel.CreateInstance方法,这个方法有3个重载,一个用于顶级下拉框,一个用于中间级下拉框,一个用于末级下拉框
6. 页面上引入commonddl.js
7. 页面上引用通用级联下拉框用户控件,例如:
<% Html.RenderPartial("GetDdl", Model.Countries); %>
8. 下拉框外层元素的Id命名约定:为内层下拉框Id后加Wrapper。
通用分页存储过程分析
SP名称:Utility_GetOnePage
核心代码:ROW_NUMBER() OVER (order by '+@OrderBy+') as Pos
其它:都是一些拼SQL的语句
注意事项:调用这个存储过程的存储过程必须在最后有Select语句,正常有的没有问题,如果没有,需要加上(一般写成Select 1),因为要兼容Netail工具让其生成返回IDataReader的方法,否则会生成返回void的方法。
调用方法:参考SILCustom_Business_Search存储过程