一直以来很少写Web开发的具体代码,以前是专门有人写这些,我只管管就行了。后来呢,也很少做Web类型项目了,最近有两个项目,牵扯到Web开发,而现在一个人单干,也找不到人来写这些了,只得自己憋手蹩脚的摸索。下面是这段时间的摸索总结。
一、代码生成器确实好用
以前是很反代码生成器的,但真的使用过后觉得很好用——简单、直接且控制力强。我用的是园子里李天平(http://ltp.cnblogs.com/)的动软代码生成器。在此,先感谢一下。不过,在感谢李天平之前,我必须先感谢一下郭嘉。
动软的好处是简单、直接、开源。针对我的应用,我做了以下改进:
(1)Image的处理问题。代码生成器生成的操作Image部分的代码有小问题,不能插入全部的Image对象,这个手动修改了下。
(2)生成的代码没有 partial 关键字,不方便扩展。我修改成产生的所有类都是partial类。这样做有什么好处呢?就是对于每个类,你可以在不修改生成的源文件的情况下,申明某个类实现了某些接口,以对代码进行复用。
例如,假设Album、News、Knowledge这三个类都具有某些相同的属性(ID,Title,AccountId,ViewCount,CreateTime,UpdateTime,等),在代码生成器生成的文件之外,另建一个目录,放一个Interface.cs的文件:
interface
1
public
interface
IContent
2
{
3
Guid Id {
get
;
set
; }
4
String Title {
get
;
set
; }
5
String AccountId {
get
; }
6
String Caption {
get
; }
7
Int32 ViewCount {
get
;
set
; }
8
DateTime CreateTime {
get
;
set
; }
9
DateTime UpdateTime {
get
;
set
; }
10
}
11
12
public
interface
IContentVoteable : IContent
13
{
14
Int32 VoteCount {
get
;
set
; }
15
}
16
17
public
partial
class
Album : IContent
18
{
19
public
String Caption {
get
{
return
"
相册
"
; } }
20
}
21
22
public
partial
class
News : IContent
23
{
24
public
String Caption {
get
{
return
"
资讯
"
; } }
25
}
26
27
public
partial
class
Knowledge : IContent
28
{
29
public
String Caption {
get
{
return
"
手记
"
; } }
30
}
31
这样就可以对生成的类进行类型“指派”,可用于泛型代码来简化编程。并且这种“指派”是强类型的,可在编译器检查错误的。
二、分页
这是一个老话题了。以前我直接用GridView和ObjectDataSource,结果是当需求变化了痛苦不堪。比如,客户要求用 VS2005 ……
本着拿来主义的原则,拿来了园子里张子阳(http://www.cnblogs.com/jimmyzhang/)的分页代码。感谢张子阳。在感谢张子阳之前,再感谢一下郭嘉。
具体的开发过程中做了一些思考,做出了以下改变:
(1)没有使用分页存储过程,而是在C#代码中进行封装,纳入我的基础库里面。这样做的理由是:
(a)分页代码比较简单,分页的数据库操作比较耗时,作为存储过程对性能提高不明显;
(b)存储过程使用前需要添加进数据库,而在C#端使用则可以省略这一步骤。
(c)方便调用、修改和封装。以下是我写的C#端的数据库分页代码:
代码
1
public
static
DataTable GetPagerData(String tableName, String returnColumns, String
where
, String orderColumnName, Boolean orderDesc, String keyColumnName, Int32 pageSize, Int32 pageIndex,
params
SqlParameter[] whereParams)
2
{
3
if
(pageIndex
==
1
)
4
{
5
return
GetTopData(pageSize, tableName, returnColumns,
where
, orderColumnName, orderDesc, RemoveNull(whereParams));
6
}
7
else
8
{
9
String sql
=
String.Format(
"
select top {0} {1} from {2} where {3} and {6} not in ( select top {7} {6} from {2} where {3} order by {4} {5} ) order by {4} {5}
"
, pageSize, returnColumns, tableName,
where
, orderColumnName, orderDesc
==
true
?
"
desc
"
: String.Empty, keyColumnName, pageSize
*
(pageIndex
-
1
));
10
return
Query(sql, RemoveNull(whereParams)).Tables[
0
];
11
}
12
}
13
14
public
static
Int32 GetDataCount(String tableName, String
where
,
params
SqlParameter[] whereParams)
15
{
16
String sql
=
String.Format(
"
select count(*) from {0} where {1}
"
, tableName,
where
);
17
Int32 result
=
(Int32)GetSingle(sql, RemoveNull(whereParams));
18
return
result;
19
}
20
21
public
static
DataTable GetTopData(Int32 top, String tableName, String returnColumns, String
where
, String orderColumnName, Boolean orderDesc,
params
SqlParameter[] whereParams)
22
{
23
String sql
=
String.Format(
"
select top {0} {1} from {2} where {3} order by {4} {5}
"
, top, returnColumns, tableName,
where
, orderColumnName, orderDesc
==
true
?
"
desc
"
: String.Empty);
24
return
Query(sql, whereParams).Tables[
0
];
25
}
26
private
static
SqlParameter[] RemoveNull(SqlParameter[] whereParams)
27
{
28
List
<
SqlParameter
>
list
=
new
List
<
SqlParameter
>
();
29
foreach
(SqlParameter sq
in
whereParams)
30
{
31
if
(sq
!=
null
) list.Add(sq);
32
}
33
return
list.ToArray();
34
}
在这段代码中,我直接将分页和Top集成在一起了,取第一页时,使用的是Top,以优化性能。同时,也省掉Top调用。这样做的好处,在下面能体现出来。
(2)对于分页部分的代码,我默认进行一下处理:
(a)url的page参数指现在的pageIndex;
(b)url的count参数指查询结果的总数量;
“count” 和 “page ”也可以指定为其它字符串。当是第一页时,去调用 GetDataCount 方法,获得count参数,这个参数直接传递给第二页,第三页……的url,避免查看第二页第三页时,重复获得count,影响性能。
三、封装和复用
通过上面的处理,就很方便对代码进行封装和复用了。
还是以上面的Album,News和Knowledge来说,这三个,编辑都可以从后台加精、推荐、设为热点、锁定、审核通过与否等操作。如何用最简单的代码来实现前台和后台所需要的以下需求呢:
(1)获得全部加精的对象;获得N天内加精的对象;以上可以按不同的排序方式排序;获得审核通过的对象,获得N天内审核通过的对象;
(2)获得全部推荐的对象;获得N天内推荐的对象;以上可以按不同的排序方式排序;获得审核通过的对象,获得N天内审核通过的对象;
(3)获得全部热点的对象;获得N天内热点的对象;以上可以按不同的排序方式排序;获得审核通过的对象,获得N天内审核通过的对象;
(4)获得最新的对象;获得N天内审核通过的对象;
(5)获得浏览量最多的对象;获得N天内浏览量最多的对象;获得审核通过的对象,获得N天内审核通过的对象;
(6)以上检索可指定帐号,也可以是全体帐号。
……
这里假设Album,News和Knowledge在数据库里相关列的列名都是一致的,EnableStatus代表审核通过与否,0代表待审核,1代表审核通过,-1代表审核未通过;IsLocked,IsHot,IsChoiced,IsMarked等编辑的操作项,分别代表是否锁定,是否热门,是否推荐和是否精华。News和Knowledge还有个CategoryId的列,代表所属类别。
以News为例子,代码为:
代码
1
public
partial
class
NewsService : Orc.HairFashion.BLL.News
2
{
3
public
static
NewsService Instance;
4
5
static
NewsService()
6
{
7
Instance
=
new
NewsService();
8
}
9
10
public
static
void
UpdateColumnStatus(ICollection
<
Guid
>
ids, String columName,
int
status)
11
{
12
DbHelper.UpdateColumnStatus(ids,
"
News
"
, columName, status);
13
}
14
15
#region
分页查找
16
17
protected
static
DataTable GetPagerData(String
where
, String orderColumnName, Int32 pageSize, Int32 pageIndex,
params
SqlParameter[] whereParams)
18
{
19
return
DbHelper.GetPagerData(
"
News
"
,
"
*
"
,
where
, orderColumnName,
true
,
"
Id
"
, pageSize, pageIndex, whereParams);
20
}
21
22
protected
static
Int32 GetDataCount(String
where
,
params
SqlParameter[] whereParams)
23
{
24
return
DbHelper.GetDataCount(
"
News
"
,
where
, whereParams);
25
}
26
27
public
static
List
<
News
>
SelectData(String accountId, String mark, String mode, Int32 categoryId, Int32 inDays, String orderColumnName, Int32 pageSize, Int32 pageIndex)
28
{
29
DataTable table
=
GetPagerData(
"
1=1
"
+
Util.BuildSqlByMark(mark)
+
Util.BuildSqlByMode(mode)
+
Util.BuildSqlByCategoryId(categoryId)
+
Util.BuildSqlByCreateInDays(inDays)
+
Util.BuildSqlByAccountId(accountId), orderColumnName, pageSize, pageIndex, Util.BuildSqlParameterByAccountId(accountId));
30
return
NewsService.Instance.DataTableToList(table);
31
}
32
33
public
static
Int32 SelectCount(String accountId, String mark, String mode, Int32 categoryId, Int32 inDays)
34
{
35
return
GetDataCount(
"
1=1
"
+
Util.BuildSqlByMark(mark)
+
Util.BuildSqlByMode(mode)
+
Util.BuildSqlByCategoryId(categoryId)
+
Util.BuildSqlByCreateInDays(inDays)
+
Util.BuildSqlByAccountId(accountId), Util.BuildSqlParameterByAccountId(accountId));
36
}
37
38
#endregion
39
}
就只用这简简单单的代码就可以了。这里各个查询选项是“正交”的。accountId 为 null或Empty,则检索全部。mark代表编辑的查询类型,是全部,还是精华还是热门还是最新……,实际上这里最好是传入enum。mode代表审核状态,最好使用enum。这两处没用enum是历史遗留问题。categoryId代表类别id,如果为负数则检索全部的类别,inDays代表检索多少天内的数据,负数代表全部,orderColumnName 是排序列,我默认全部desc了,pageSize 是页大小,pageIndex是页序号。
这部分代码是在我动软代码生成器的源代码,使它生成带partial的类之前修改的。如果现在写,使用泛型的话,可以将上面的代码进一步简化。
然后,写一个公用的控件类,以复用共有代码:
BaseListControl
1
public
abstract
class
BaseListControl : System.Web.UI.UserControl
2
{
3
private
Boolean m_showPager
=
false
;
4
private
Int32 m_pageSize
=
12
;
5
private
Boolean m_orderByVoteCount
=
true
;
6
private
Int32 m_inDays
=
7
;
7
private
String m_caption
=
""
;
8
private
String m_viewCaption
=
"点击
"
;
9
private
String m_voteCaption
=
"票数
"
;
10
private
String m_orderBy
=
"
CreateTime
"
;
11
private
String m_mark
=
""
;
12
private
String m_mode
=
""
;
13
14
public
Int32 Count
=
0
;
15
public
Boolean ShowPager {
get
{
return
m_showPager; }
set
{ m_showPager
=
value; } }
16
public
Int32 PageSize {
get
{
return
m_pageSize; }
set
{ m_pageSize
=
value; } }
17
public
String Caption {
get
{
return
m_caption; }
set
{ m_caption
=
value; } }
18
public
String ViewCaption {
get
{
return
m_viewCaption; }
set
{ m_viewCaption
=
value; } }
19
public
String VoteCaption {
get
{
return
m_voteCaption; }
set
{ m_voteCaption
=
value; } }
20
public
String OrderBy {
get
{
return
m_orderBy; }
set
{ m_orderBy
=
value; } }
21
public
String Mark {
get
{
return
m_mark; }
set
{ m_mark
=
value; } }
22
public
String Mode {
get
{
return
m_mode; }
set
{ m_mode
=
value; } }
23
public
Int32 InDays {
get
{
return
m_inDays; }
set
{ m_inDays
=
value; } }
24
25
protected
void
Page_Load(
object
sender, EventArgs e)
26
{
27
if
(Page.IsPostBack
==
false
)
28
{
29
OnPageLoad();
30
InitPager();
31
BindData();
32
}
33
}
34
35
protected
abstract
PagerControl GetPager();
36
protected
abstract
Int32 GetResultCount();
37
protected
virtual
void
OnPageLoad()
38
{
39
}
40
41
protected
void
InitPager()
42
{
43
PagerControl pager
=
GetPager();
44
if
(ShowPager
==
false
)
45
{
46
if
(pager
!=
null
)
47
pager.Visible
=
false
;
48
}
49
else
50
{
51
Int32 count
=
PageUtil.GetCurrentCount(
this
.Page);
52
if
(count
<
0
) count
=
GetResultCount();
53
54
if
(pager
!=
null
)
55
pager.UrlManager
=
new
Orc.Util.AspDotNet.DefaultUrlManager(count, PageSize,
"
page
"
,
"
count
"
);
56
}
57
}
58
59
protected
void
BindData()
60
{
61
Int32 page
=
PageUtil.GetCurrentPage(
this
.Page);
62
if
(page
<
1
) page
=
1
;
63
BindData(page);
64
}
65
66
protected
abstract
void
BindData(Int32 page);
67
}
然后是具体的控件:
Controls_NewsList
1
public
partial
class
Controls_NewsList : BaseListControl
2
{
3
private
Int32 m_categoryId
=
-
1
;
4
public
Int32 CategoryId
5
{
6
get
{
return
m_categoryId; }
7
set
{ m_categoryId
=
value; }
8
}
9
10
protected
override
Orc.Util.AspDotNet.PagerControl GetPager()
11
{
12
return
this
.pager;
13
}
14
15
protected
override
int
GetResultCount()
16
{
17
return
NewsService.SelectCount(
null
,
this
.Mark,
this
.Mode,
this
.CategoryId,
this
.InDays);
18
}
19
20
protected
override
void
BindData(
int
page)
21
{
22
this
.rpList.DataSource
=
NewsService.SelectData(
null
,
this
.Mark,
this
.Mode,
this
.CategoryId,
this
.InDays,
this
.OrderBy,
this
.PageSize, page);
23
this
.rpList.DataBind();
24
}
25
}
这样一封装就超级好用,可以显示pager也可以不显示,可以用在分页里,也可以用在栏目的首页,且代码量几乎降低到最低,而性能几乎提高到最高,还可以根据各种参数进行缓存。
四、抱怨
尽管使用了这些技巧,Web开发还是太累。一个月坐下来还没做其它项目三五天赚的钱多,并且技术支持比其它类型项目难得多。做一个Web项目后悔一次,今后,尽量不做了,除非价格很高或自己用。