2009年1月6日

(转)SQL Server 2005四个新的排序函数: ROW_NUMBER、RANK、DENSE_RANK 和 NTILE

 
//tzik Ben-Gan   
        ROW_NUMBER、RANK、DENSE_RANK 和 NTILE,这些新函数使您可以有效地分析数据以及向查询的结果行提供排序值。您可能发现这些新函数有用的典型方案包括:将连续整数分配给结果行,以便进行表示、分页、计分和绘制直方图。

Speaker Statistics 方案

下面的 Speaker Statistics 方案将用来讨论和演示不同的函数和它们的子句。大型计算会议包括三个议题:数据库、开发和系统管理。十一位演讲者在会议中发表演讲,并且为他们的讲话获得 范围为 1 到 9 的分数。结果被总结并存储在下面的 SpeakerStats 表中:

CREATE TABLE SpeakerStats(
    speaker        VARCHAR(10) NOT NULL PRIMARY KEY
    , track          VARCHAR(10) NOT NULL
    , score          INT         NOT NULL
    , pctfilledevals INT         NOT NULL
    , numsessions    INT         NOT NULL)

SET NOCOUNT ON

INSERT INTO SpeakerStats VALUES('Dan',     'Sys', 3, 22, 4)
INSERT INTO SpeakerStats VALUES('Ron',     'Dev', 9, 30, 3)
INSERT INTO SpeakerStats VALUES('Kathy',   'Sys', 8, 27, 2)
INSERT INTO SpeakerStats VALUES('Suzanne', 'DB', 9, 30, 3)
INSERT INTO SpeakerStats VALUES('Joe',     'Dev', 6, 20, 2)
INSERT INTO SpeakerStats VALUES('Robert', 'Dev', 6, 28, 2)
INSERT INTO SpeakerStats VALUES('Mike',    'DB', 8, 20, 3)
INSERT INTO SpeakerStats VALUES('Michele', 'Sys', 8, 31, 4)
INSERT INTO SpeakerStats VALUES('Jessica', 'Dev', 9, 19, 1)
INSERT INTO SpeakerStats VALUES('Brian',   'Sys', 7, 22, 3)
INSERT INTO SpeakerStats VALUES('Kevin',   'DB', 7, 25, 4)

每个演讲者都在该表中具有一个行,其中含有该演讲者的名字、议题、平均得分、填写评价的与会者相对于参加会议的与会者数量的百分比以及该演讲者发表演讲的次数。本节演示如何使用新的排序函数分析演讲者统计数据以生成有用的信息。

ROW_NUMBER

ROW_NUMBER 函数使您可以向查询的结果行提供连续的整数值。例如,假设您要返回所有演讲者的 speaker、track 和 score,同时按照 score 降序向结果行分配从 1 开始的连续值。以下查询通过使用 ROW_NUMBER 函数并指定 OVER (ORDER BY score DESC) 生成所需的结果:

SELECT ROW_NUMBER() OVER(ORDER BY score DESC) AS rownum,   speaker, track, scoreFROM SpeakerStatsORDER BY score DESC以下为结果集:

rownum speaker    track      score
------ ---------- ---------- -----------
1      Jessica    Dev        9
2      Ron        Dev        9
3      Suzanne    DB         9
4      Kathy      Sys        8
5      Michele    Sys        8
6      Mike       DB         8
7      Kevin      DB         7
8      Brian      Sys        7
9      Joe        Dev        6
10     Robert     Dev        6
11     Dan        Sys        3
得 分最高的演讲者获得行号 1,得分最低的演讲者获得行号 11。ROW_NUMBER 总是按照请求的排序为不同的行生成不同的行号。请注意,如果在 OVER() 选项中指定的 ORDER BY 列表不唯一,则结果是不确定的。这意味着该查询具有一个以上正确的结果;在该查询的不同调用中,可能获得不同的结果。例如,在我们的示例中,有三个不同的 演讲者获得相同的最高得分 (9):Jessica、Ron 和 Suzanne。由于 SQL Server 必须为不同的演讲者分配不同的行号,因此您应当假设分别分配给 Jessica、Ron 和 Suzanne 的值 1、2 和 3 是按任意顺序分配给这些演讲者的。如果值 1、2 和 3 被分别分配给 Ron、Suzanne 和 Jessica,则结果应该同样正确。

如 果您指定一个唯一的 ORDER BY 列表,则结果总是确定的。例如,假设在演讲者之间出现得分相同的情况时,您希望使用最高的 pctfilledevals 值来分出先后。如果值仍然相同,则使用最高的 numsessions 值来分出先后。最后,如果值仍然相同,则使用最低词典顺序 speaker 名字来分出先后。由于 ORDER BY 列表 — score、pctfilledevals、numsessions 和 speaker — 是唯一的,因此结果是确定的:

SELECT ROW_NUMBER() OVER(ORDER BY score DESC, pctfilledevals DESC,                           numsessions DESC, speaker) AS rownum,   speaker, track, score, pctfilledevals, numsessionsFROM SpeakerStatsORDER BY score DESC, pctfilledevals DESC, numsessions DESC, speaker以下为结果集:

rownum speaker    track      score       pctfilledevals numsessions
------ ---------- ---------- ----------- -------------- -----------
1      Ron        Dev        9           30             3
2      Suzanne    DB         9           30             3
3      Jessica    Dev        9           19             1
4      Michele    Sys        8           31             4
5      Kathy      Sys        8           27             2
6      Mike       DB         8           20             3
7      Kevin      DB         7           25             4
8      Brian      Sys        7           22             3
9      Robert     Dev        6           28             2
10     Joe        Dev        6           20             2
11     Dan        Sys        3           22             4
新的排序函数的重要好处之一是它们的效率。SQL Server 的优化程序只需要扫描数据一次,以便计算值。它完成该工作的方法是:使用在排序列上放置的索引的有序扫描,或者,如果未创建适当的索引,则扫描数据一次并对其进行排序。

另一个好处是语法的简单性。为了让您感受一下通过使用在 SQL Server 的较低版本中采用的基于集的方法来计算排序值是多么困难和低效,请考虑下面的 SQL Server 2000 查询,它返回与上一个查询相同的结果:

SELECT (SELECT COUNT(*)   FROM SpeakerStats AS S2  
            WHERE S2.score > S1.score    
            OR (S2.score = S1.score         AND S2.pctfilledevals > S1.pctfilledevals)    
            OR (S2.score = S1.score         AND S2.pctfilledevals = S1.pctfilledevals         AND S2.numsessions > S1.numsessions)    
            OR (S2.score = S1.score         AND S2.pctfilledevals = S1.pctfilledevals         AND S2.numsessions = S1.numsessions         AND S2.speaker < S1.speaker)
            ) + 1 AS rownum
            , speaker, track, score, pctfilledevals, numsessions
            FROM SpeakerStats AS S1
            ORDER BY score DESC, pctfilledevals DESC, numsessions DESC, speaker

该查询显然比 SQL Server 2005 查询复杂得多。此外,对于 SpeakerStats 表中的每个基础行,SQL Server 都必须扫描该表的另一个实例中的所有匹配行。对于基础表中的每个行,平均大约需要扫描该表的一半(最少)行。SQL Server 2005 查询的性能恶化是线性的,而 SQL Server 2000 查询的性能恶化是指数性的。即使是在相当小的表中,性能差异也是显著的。

 

行号的一个典型应用是通过查询结果分页。给定页大小(以行数为单位)和页号,需要返回属于给定页的行。例如,假设您希望按照“score DESC, speaker”顺序从 SpeakerStats 表中返回第二页的行,并且假定页大小为三行。下面的查询首先按照指定的排序计算派生表 D 中的行数,然后只筛选行号为 4 到 6 的行(它们属于第二页):

SELECT *
FROM (SELECT ROW_NUMBER() OVER(ORDER BY score DESC, speaker) AS rownum,
        speaker, track, score
      FROM SpeakerStats) AS D
WHERE rownum BETWEEN 4 AND 6
ORDER BY score DESC, speaker
以下为结果集:

rownum speaker    track      score
------ ---------- ---------- -----------
4      Kathy      Sys        8
5      Michele    Sys        8
6      Mike       DB         8
用更一般的术语表达就是,给定 @pagenum 变量中的页号和 @pagesize 变量中的页大小,以下查询返回属于预期页的行:
DECLARE @pagenum AS INT, @pagesize AS INT
SET @pagenum = 2
SET @pagesize = 3
SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY score DESC, speaker) AS rownum
                ,speaker
                , track
                , score
                FROM SpeakerStats)
                AS DWHERE rownum BETWEEN (@pagenum-1)*@pagesize+1 AND @pagenum*@pagesize
                ORDER BY score DESC, speaker

上述方法对于您只对行的一个特定页感兴趣的特定请求而言已经足够了。但是,当用户发出多个请求时,该方法就不能满足需要了,因为该查询的每个调用都 需要您对表进行完整扫描,以便计算行号。当用户可能反复请求不同的页时,为了更有效地进行分页,请首先用所有基础表行(包括计算得到的行号)填充一个临时 表,并且对包含这些行号的列进行索引:

SELECT ROW_NUMBER() OVER(ORDER BY score DESC, speaker) AS rownum, *
INTO #SpeakerStatsRN
FROM SpeakerStats
CREATE UNIQUE CLUSTERED INDEX idx_uc_rownum ON #SpeakerStatsRN(rownum)
然后,对于所请求的每个页,发出以下查询:

SELECT rownum, speaker, track, score
FROM #SpeakerStatsRN
WHERE rownum BETWEEN (@pagenum-1)*@pagesize+1 AND @pagenum*@pagesize
ORDER BY score DESC, speaker
只有属于预期页的行才会被扫描。

分段
可以在行组内部独立地计算排序值,而不是为作为一个组的所有表行计算排序值。为此,请使用 PARTITION BY 子句,并且指定一个表达式列表,以标识应该为其独立计算排序值的行组。例如,以下查询按照“score DESC, speaker”顺序单独分配每个 track 内部的行号:

SELECT track,
ROW_NUMBER() OVER(
    PARTITION BY track
    ORDER BY score DESC, speaker) AS pos,
speaker, score
FROM SpeakerStats
ORDER BY track, score DESC, speaker
以下为结果集:

track      pos speaker    score
---------- --- ---------- -----------
DB         1   Suzanne    9
DB         2   Mike       8
DB         3   Kevin      7
Dev        1   Jessica    9
Dev        2   Ron        9
Dev        3   Joe        6
Dev        4   Robert     6
Sys        1   Kathy      8
Sys        2   Michele    8
Sys        3   Brian      7
Sys        4   Dan        3
在 PARTITION BY 子句中指定 track 列会使得为具有相同 track 的每个行组单独计算行号。

RANK, DENSE_RANK

RANK 和 DENSE_RANK 函数非常类似于 ROW_NUMBER 函数,因为它们也按照指定的排序提供排序值,而且可以根据需要在行组(分段)内部提供。但是,与 ROW_NUMBER 不同的是,RANK 和 DENSE_RANK 向在排序列中具有相同值的行分配相同的排序。当 ORDER BY 列表不唯一,并且您不希望为在 ORDER BY 列表中具有相同值的行分配不同的排序时,RANK 和 DENSE_RANK 很有用。RANK 和 DENSE_RANK 的用途以及两者之间的差异可以用示例进行最好的解释。以下查询按照 score DESC 顺序计算不同演讲者的行号、排序和紧密排序值:

SELECT speaker, track, score,
ROW_NUMBER() OVER(ORDER BY score DESC) AS rownum,
RANK() OVER(ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER(ORDER BY score DESC) AS drnk
FROM SpeakerStats
ORDER BY score DESC
以下为结果集:

speaker    track      score       rownum rnk drnk
---------- ---------- ----------- ------ --- ----
Jessica    Dev        9           1      1   1
Ron        Dev        9           2      1   1
Suzanne    DB         9           3      1   1
Kathy      Sys        8           4      4   2
Michele    Sys        8           5      4   2
Mike       DB         8           6      4   2
Kevin      DB         7           7      7   3
Brian      Sys        7           8      7   3
Joe        Dev        6           9      9   4
Robert     Dev        6           10     9   4
Dan        Sys        3           11     11 5
正 如前面讨论的那样,score 列不唯一,因此不同的演讲者可能具有相同的得分。行号确实代表下降的 score 顺序,但是具有相同得分的演讲者仍然获得不同的行号。但是请注意,在结果中,所有具有相同得分的演讲者都获得相同的排序和紧密排序值。换句话说,当 ORDER BY 列表不唯一时,ROW_NUMBER 是不确定的,而 RANK 和 DENSE_RANK 总是确定的。排序值和紧密排序值之间的差异在于,排序代表:具有较高得分的行号加 1,而紧密排序代表:具有明显较高得分的行号加 1。从您迄今为止已经了解的内容中,您可以推导出当 ORDER BY 列表唯一时,ROW_NUMBER、RANK 和 DENSE_RANK 产生完全相同的值。

NTILE

NTILE 使您可以按照指定的顺序,将查询的结果行分散到指定数量的组 (tile) 中。每个行组都获得不同的号码:第一组为 1,第二组为 2,等等。您可以在函数名称后面的括号中指定所请求的组号,在 OVER 选项的 ORDER BY 子句中指定所请求的排序。组中的行数被计算为 total_num_rows / num_groups。如果有余数 n,则前面 n 个组获得一个附加行。因此,可能不会所有组都获得相等数量的行,但是组大小最大只可能相差一行。例如,以下查询按照 score 降序将三个组号分配给不同的 speaker 行:

SELECT speaker, track, score,
ROW_NUMBER() OVER(ORDER BY score DESC) AS rownum,
NTILE(3) OVER(ORDER BY score DESC) AS tile
FROM SpeakerStats
ORDER BY score DESC
以下为结果集:

speaker    track      score       rownum tile
---------- ---------- ----------- ------ ----
Jessica    Dev        9           1      1
Ron        Dev        9           2      1
Suzanne    DB         9           3      1
Kathy      Sys        8           4      1
Michele    Sys        8           5      2
Mike       DB         8           6      2
Kevin      DB         7           7      2
Brian      Sys        7           8      2
Joe        Dev        6           9      3
Robert     Dev        6           10     3
Dan        Sys        3           11     3
在 SpeakerStats 表中有 11 位演讲者。将 11 除以 3 得到组大小 3 和余数 2,这意味着前面 2 个组将获得一个附加行(每个组中有 4 行),而第三个组则不会得到附加行(该组中有 3 行)。组号(tile 号)1 被分配给行 1 到 4,组号 2 被分配给行 5 到 8,组号 3 被分配给行 9 到 11。通过该信息可以生成直方图,并且将项目均匀分布到每个梯级。在我们的示例中,第一个梯级表示具有最高得分的演讲者,第二个梯级表示具有中等得分的演 讲者,第三个梯级表示具有最低得分的演讲者。可以使用 CASE 表达式为组号提供说明性的有意义的备选含义:

SELECT speaker, track, score,
CASE NTILE(3) OVER(ORDER BY score DESC)
    WHEN 1 THEN 'High'
    WHEN 2 THEN 'Medium'
    WHEN 3 THEN 'Low'
END AS scorecategory
FROM SpeakerStats
ORDER BY track, speaker
以下为结果集:

speaker    track      score       scorecategory
---------- ---------- ----------- -------------
Kevin      DB         7           Medium
Mike       DB         8           Medium
Suzanne    DB         9           High
Jessica    Dev        9           High
Joe        Dev        6           Low
Robert     Dev        6           Low
Ron        Dev        9           High
Brian      Sys        7           Medium
Dan        Sys        3           Low
Kathy      Sys        8           High
Michele    Sys        8           Medium

posted @ 2009-01-06 10:05 welkin 阅读(1096) 评论(0) 编辑

2008年12月5日

理解索引

     索引是什么东西呢?索引是如何提高查询的性能的。 对于新手来说, 要弄懂数据库的页, 二叉树等等概念实在是太难了。
难道非得弄清楚这些概念之后才能理解索引吗? 当然不是的, 以下是我对索引的理解,和真实情况并不完全一致, 但是起码
能够帮助我们更直观的理解索引。

简单的说表的索引就相当于 表的一个副表, 不过他只包含该索引所包含的字段, 外加一个指向真实表的地址字段。


下面我们以一个例子来说说索引是如何提高性能的。

1. 非聚集索引

假设有一个学生表: TStu   他包含20 个字段。现在有一个查询

select * from TStu where fName like '%平'

如果没有索引, 数据库怎么查呢?
首先数据库引擎会逐行的读取 TStu 表的每一行记录, 来检查fName 字段是否以‘平’为结尾, 如果是, 则将记录放入结果集。
这个操作叫做 表扫描。

现在我们在fName 字段上建了一个非聚集索引, 按照我先前的说法, 相当于在数据里面建了一个只有2 个字段的附表
一个字段 与 TSTu.fName 一样, 另一个字段是fName对应的行的指针。我们把这个表叫做索引副表好了。

数据库是如何来执行上述查询的呢?

第一步  数据库将扫描索引副表, 找出姓名以 ‘平’ 结尾的学生。
第二部  再根据副表所指示的真实记录的地址去查找改学生得其他字段信息。

由于索引副表只有真实表的 约 10 份之 1大小,第一步查找效率比没有索引时提高了10 倍。
第二步的效率可能出现2 总情况:

如果所有学生的 姓名都是以 ‘平’结尾, 那么数据库引擎最终还是要对真实表的所有记录进行扫描,
这种情况下, 使用索引其实比不使用索引效率更加差。
如果只有很少的学生的姓名是以‘平’结尾,那么数据库引擎只需要对真实表很少的记录进行扫描。
这种情况下, 使用索引比不使用索引效率将高出很多。

我们再来考虑另外一个查询

select fName from TStu where fname like '%平'

这个查询与上一个查询 不同的地方在于 只需要选取fName 字段就好了。

如果不使用索引的话,
数据库引擎还是执行全表扫描,  找出对应的记录,然后取出其中的fName 字段。

如果使用索引的话,
数据库引擎 只需要在 索引副表中找到 fName 字段就好了, 而不需要去检查真实表。 即便是所有学生姓名都是已 ‘平’ 结尾。
查询性能都比原查询快了将近 10 倍。

从上面的分析中可以得出结论。 在记录总数越多, 表的字段越多, 所选出来的记录数越少的情况下, 使用索引比不使用索引性能更加好,

同时索引字段是否包含所需要查询的字段,对查询性能的影响也是明显的。


2. 集索引的优势:
聚集索引其本质就是排了序的索引,并且真实表的物理顺序与索引副表的排序保持一致, 每个表只能有一个物理顺序, 所以每个表就只能有一个聚集索引。
我们考虑下面这个查询:

select * from TStu where fSID  between 12 and 20

这个查询查询学生ID 在12 与 20 之间的考生信息。 使用非聚集索引的情况我们上面已经分析过,
现在我们来分析一下 使用聚集索引的情况:我们假设已经在fSID 字段上建立了聚集索引, 并且按照fSID 的升序排列。

第一步 :数据库引擎在查询的时候首先根据索引统计信息 在索引表中找到fSID 为12 的记录以及fSID 为20 的记录。
第二步 :根据获取的记录以及地址。 再到真实表中找到相应的记录。因为聚集索引是排序顺序就是真实表记录的物理存储顺序。
  所以fSID为12 与 20 之间的记录在物理上是保存在数据库连续的页中。所以只需要做最少的页扫描就能获取所需要的记录。
  非聚集索引由于记录分散在数据库中不同的页中获取每一条记录都必须进行一次页扫描(如果记录总大小超过1页则进行多次页扫描)
  其效率已经大大的提高了。

我们再回过头来看看第一步 的实现:
数据库引擎是如何直接获取 fSID 为12 记录, 而不需要进行扫描操作的呢?数据库将会根据索引统计信息来决定采用那种算法为最优。

在这里我简单的举出可能的 2 种简单算法:

 从上往下扫描法:从索引副表的第一条记录从上往下扫描,直到找到所需要的记录, 就不需要继续往下扫描。 这种情况下查询效率与目标记录的位置有关系
  如果目标记录位于表的最前面, 那么查找将最快。
 折中取值法:   从索引表的中间位置取一条记录进行比较, 如果大于12 则到下半表再取中间值再比较, 如果小于 12 则到上半表取中间值再比较, 以此

类推,直到找到目标记录。

事实上数据库引擎根据索引统计信息以及数据特性往往能以最快的方式找到记录, 而不需要进行耗时的扫描。

由于每个表只能有一个聚集索引, 建立聚集索引的时候一定要慎重, 从以上的分析中可以得出这样结论 :
 1. 在数据中逻辑上存在连续性的字段上建立聚集索引将能够大大的提高性能。 比如日期字段, 按一定规则编码的学号字段。 相反的, 对于逻辑上
 没有必然的连续性的字段则意义相对不大。 比如考生照片字段, 自动增长字段, GUID 字段, 以及其他的一些无规律的编码字段。

 2. 在一个具有适量重复键的字段上建立聚集索引能够大大提高性能。 相反对于一个唯一字段 或者 全部相同字段 建立聚集索引 其效果并不明显。


3. 组合索引:

 理解了单字段索引, 组合索引就很好理解了, 考虑下面一个查询:
 
select * from TSTU where fSID between 100 and 10000 and fName like '%平'

如果单单以fSID 字段建立索引, 则数据库在查找索引副表之后 需要到真实表中扫描9900 条记录。
如果单单以fName 字段建立索引,并且以‘平’ 结尾的记录数特别多, 我们还是需要到真实表中扫描相当多的记录。

如果我们以fSID 与 fName 2 个字段建立组合索引。 经过这2 个字段在索引副表的联合筛选, 我们只需要在真实表中扫描很少的记录就能找到

我们所需要的目标记录, 而真实表的扫描恰恰是最花时间的。

再看下面的查询:

select fSID , fName from TSTU where fSID between 100 and 10000 and fName like '%平'.

这种情况下无论是以fSID 还是以fName  建立索引。 都需要去扫描真实表, 而如果以2 个字段建立联合索引。 则不需要再去扫描真实表。

当然:  组合索引的索引副表随着字段数的增加而增大 必然导致索引副表的扫描时间增加。具体使用要根据实际情况权衡利弊。

从上面的分析我们可以得出结论, 建立组合索引需要考虑以下几个因数:
 
1. 字段数尽可能得少, 这样能减少索引扫描的时间。

2. 对于组合条件的查询, 尽量挑选出能够最大限度限制结果集的大小的字段 作为组合字段索引。

3. 对于只需要查询很少字段的查询, 可以考虑使用组合索引覆盖查询字段。

 

写到这里不知道大家对索引的优化理解了多少,对于文中如果存在不正确的理解请高手指正。 

posted @ 2008-12-05 13:19 welkin 阅读(176) 评论(0) 编辑

2008年9月12日

服务器控件开发——ViewState(6)

  在控件开发中ViewState 是一个很重要的概念, 同时也是一个很难搞清楚的东西。 我曾经到网上查看过很多关于View state
的文章,很多都是对于ViewState 的原理进行解释, 很少做深入的探讨0 在我的这篇文章当中,我不想对ViewState 的基本原理再进行冗余的重复,
而是努力写出一点与众不同的地方, 让大家对ViewState 更深一点了解。

 要理解ViewState 必须的搞清楚下面几个问题 :
 1. 什么是ViewState.
 2. Asp.net 如何保存ViewState 以及读取 ViewState
 3. ViewState 保存些什么东西。

下面我们对这是那个问题一一探讨。

1. 什么是ViewState ,他有什么作用 ?
严格来讲, ViewState 是一种机制, 是Asp.Net 保存状态的一种机制,具体来讲ViewState 就是Asp.net 服务端写入到客户端一个 隐藏 字段。

该字段的名字叫做:“ __VIEWSTATE”随便打开一个Asp.net 的page都能看到有一个像这样

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwUKLTM3ODU5NDMxNQ9kFgICAw9kFgICAw8PZGRkZIa1AcwFgqgCeB/r2LaDqCLk/Ihz" />
的隐藏字段。 这个Value 就是Viewstate的值

 

2. Asp.net 是如何保存Viewstate 的?

下面的方法是Page 基类在PreRend 生命周期之后调用的用来保存ViewState 的方法。

 

Code

 

注意没有, 页面通过调用Control 基类的 SaveViewStateRecursive 来获取页面的Viewstate信息,然后通过LosFormatter 类将其序列化为
一个字符串, 然后写入到客户段。

那么Asp.net 是如何加载客户端的ViewState 的呢?

加载客户端的ViewState 与 写入Viewstate 刚好相反。

Code

 

LoadPageStateFromPersistenceMedium  将视图状态反序列化为对象, 然后交给Page 对象的 LoadViewStateRecursive
处理。 

LoadViewStateRecursive 与 SaveViewStateRecursive 的处理逻辑请参考我第一篇的关于Control 基类的博文。简单说来

SaveViewStateRecursive 分别将子控件ID 的集合, 子控件的视图状态数据的集合, 控件本身的视图状态数据(通过调用控件的IStateManger.SaveViewstate 得到)
 保存到一个Tripet 结构当中。然后返回。

LoadViewStateRecursive 首先调用IstateManage.LoadViewState 方法 加载自身的ViewState  然后调用子类的LoadViewStateRecursive
方法加载子类的视图状态。

以前一直有一个疑问: 脱离Asp.net, 我们是否可以通过一个通用的程序来解释ViewState, 并且明白其中的意义, 理论上这是不可行的。
因为控件保存 与读取 自身视图状态的逻辑被抽取成了一个接口IStatemanager接口。 不同的控件保存自身视图状态的方式不一样。
读取的方式也就不一样。 不过假设我们能够猜到页面本身以及其所有子控件的保存控件的视图状态的方式。 那么针对于
特定的页面我们是可以读取其ViewState 的内容的。 事实上, 对于SaveViewState 的实现,大部分控件都采用了基类的实现,
即使自定义实现, 我们也是可以通过查看ViewState 的内容猜测到, 定制这样的一个针对与大部分页面的ViewStateParser 是
可能的。 http://www.codeproject.com/KB/viewstate/viewstate_viewer.aspx 这是我在CodeProject 找到的一篇关于
如何通过自定义程序来Parser 的文章, 有兴趣的朋友可以去看看。


3. ViewState 保存些什么东西

这个问题看似简单, 实则不然, 大部分人的误解也正在这儿。比如说TextBox 控件的Text 属性。 一般都认为 如果启用ViewState 的话 Text 属性
的值会保存到ViewState 中。而事实上并不是这样。在这儿不得不提一下IStatemanager 接口了。该接口实现3个方法和一个属性。
      
 // 检查视图状态跟踪状态。
 bool IStateManager.IsTrackingViewState
        {
            get;
        }
 
 // 加载视图状态。
        void IStateManager.LoadViewState(object state)
        {
            
        }

 // 保存视图状态, 只有在视图状态跟踪开启的时候才会保存, 否则不保存。
        object IStateManager.SaveViewState()
        {
         
        }
 
        // 开启视图状态跟踪, 该方法被调用之后, IsTrackingViewState 属性返回True.
   void IStateManager.TrackViewState()
        {
         
        }

 

在控件的基类中。控件对于IStateManager 的实现被委托给一个StateBag 类型的变量来实现,剩下的就是StateBag魔法了
这个类的实现是很复杂的, 有兴趣的朋友可以通过Google Code Search 搜索出来看一下。在这里我重点介绍一下他的功能。
StateBag 内置了一个Dictionary ,通过这个Dictionary 你可以向StateBag 添加键值对。 同时该对象还为
每一个加入其中的Item 跟踪一个Dirty  状态值, 记住只有状态为Dirty 的键值对才会被序列化到ViewState中。

为了搞清楚Dirty 究竟是怎么回事, 我写了一下小小的程序来测试一下。

Code

该程序的输出为:
  false
  True
  false
  false
  True
聪明的你可能已经得出结论: 在 StateBag 开启跟踪视图状态之前, 对于 StateBag 中的键值对来说, 无论是添加, 还是修改其状态始终都是UnDirty状态。
         而在StateBag 开启跟踪视图状态之后,对于StaeBag 中的键值对来说, 添加或者修改,其状态将被修改为Dirty 状态。
Microsoft  为什么要如此设计呢?

 我们已Label 控件为例来说明一下。在生命周期的初始化阶段: Label  控件根据Aspx 页面中的申明初始化控件。 比如一个申明如下的控件 
  <asp:Label ID="Label1" runat="server" Text="myLabel" Width="86px"></asp:Label>
初始化后其Text属性值就被保存在 StateBag 中, 但是这个时候其状态为 Undirty

接着控件开启视图状态跟踪, 在此后的控件生命周期中, 只要用户设置Text 属性的值, 这个状态都会被设置成
Dirty 状态。当然如果用户不设置这个值, 其状态将一直保存UnDirty 状态。通过这样一种方式, Asp.net 保障了
在aspx 页面中申明的属性,初始化必须的属性, 以及在用户在Onint 代码中改变的属性 都不会保存到ViewState 中
从而保证了Viewstate的最小化。
这儿还有一点需要注意的地方(我自己也很迷惑)就是 TextBox  的Text 值无论在什么地方被赋值, 都不会保存到ViewState 中 ,但是如果你使用了TextBox 控件的
TextChange 事件 那么则另当别论, 我想可能是因为TextBox 的Text 已经随PostBack 发送到服务器端,而不需要那么
麻烦再到ViewState 中去取, 这样既减少的ViewState 的大小, 又提高了效率。


回答了上面3 个问题之后, 我想大家对于ViewState 已经有了一个全面的了解。 

posted @ 2008-09-12 17:08 welkin 阅读(360) 评论(0) 编辑

2008年7月28日

服务器控件开发——组合控件(5)

       组合控件, 顾名思义就是指由2 个或2 个以上的已存在的控件组合在一起, 协同工作从而完成新功能的新的服务器控件组合控件由于

够重用已经存在控件的功能, 能够最大限度的提升我们的开发效率。组合控件就像一台精密的机器, 而起所引用的子控件就像是机械的零件,

通力合作,共同完成新的功能。下面就让我们看看这台机器是如何将各个零件完美的组装在一起的。
     提到组合控件不得不重新来审视我们第一篇所提到的Control基类,Control基类为我们开发组合控件提供了很好的支持。我们要做的只需

要根据需求填充Control 基类为我们提供的模板。 下面我们来看看这个模板的内容。

 

1.  实现INamingContainer  接口。 这个接口没有任何方法, 纳闷呢, 没有任何方法的接口有什么用啊! 当然有用了在Control类的实现中,

 Control类通过 if(this is INamingContainer) 来判断控件是否继承INamingContainer 接口。 并且根据是否为True 来决定采用何种方式

给子控件的ID赋值。 事实上, INamingContainer 的主要作用就是为了保障子控件UniqueID在整个页面的唯一性。让我们来做一个简单的试验:

我们来做一个类似与上一篇的SimpleTextBoxControl 的SimpleCompositeControl 控件。

 

Code

我想这应该够简单的, 只是将一个TextBox 与 Button 控件在页面上呈现出来。 在测试页面拖入一个的SimpleCompositeControl控件,

 并且查看HTML代码,我们将看到类似于这样的源代码。
    <span id="SampleCompositeControl1" style="display:inline-block;width:197px;">
        <input name="tbInput" type="text" id="tbInput" />
        <input type="submit" name="btnSubmit" value="提交" id="btnSubmit" />
    </span>
    
我们注意到2 个Input 标签的ID 属性 正是我们给控件所分配的ID。 这看起来没有什么问题。 但是如果我们在页面
当中存在2 个SimpleCompositeControl 呢,那岂不是在同一个页面里面存在2 个ID 为 tbInput的Input呢。 假设控件PostBack 数据,

我们又如何知道到底是那个Input 里面的数据呢?       显然这样是行不通的,我们需要INamingContainer 接口来保障我们控件的 ID 的唯

一性。将上面的控件改为实现INamingContainer 接口。
    public class SimpleCompositeControl : System.Web.UI.WebControls.WebControl,INamingContainer
同样查看源代码。 控件HTML 变为
     <span id="SampleCompositeControl1" style="display:inline-block;width:197px;">
      <input name="SampleCompositeControl1$tbInput" type="text" id="SampleCompositeControl1_tbInput" />
     <input type="submit" name="SampleCompositeControl1$btnSubmit" value="提交"   id="SampleCompositeControl1_btnSubmit" />
    </span>
控件的ID 变为 父控件ID +子控件ID的形式, INameContainer 改变了组合控件子控件Render 自身ID 的行为, 从而保障了子控件ID 在页
面的

全局唯一性。事实上, 开发组合控件必须继承INamingContainer 接口, 否则将会导致各种奇怪的错误,比如说无法保持子控件视图状态,

 无法触发Button 事件等。

2.  重载CreateChildControls 方法
    这个比较简单,如前例所示, 只要将我们的子控件的属性一个一个的定义好, 然后加入到组合控件的子控件集合中。 为了保障子控件不被多次创建
(可能由控件的继承者不当调用CreateChildControls导致)我们将我们的CreateChildControls 稍微改良一下

   protected override void CreateChildControls()
        
{
            Controls.Clear();           
// 清除子控件
            if (HasChildViewState)       //HasChildViewState 标识是否存在子控件视图信息。
                ClearChildViewState();   //清除子控件视图信息,看过我第一篇的朋友都知道, 在移除子控件的时候
                                            
//视图信息并不会被一次性移除。                                         
                                             
// 而会 是保存在一个 一个HashTable 中, ClearChildViewState方
                                             
//法将清空这个HashTable。
            tbInput = new TextBox();
            tbInput.EnableViewState 
= false;
            tbInput.ID 
= "tbInput";
            
this.Controls.Add(tbInput);

            btSubmit 
= new Button();
            btSubmit.ID 
= "btnSubmit";
            btSubmit.Text 
= "提交";
            btSubmit.CommandName 
= "submit";
            btSubmit.CommandArgument 
= "arg";
            
this.Controls.Add(btSubmit);
        }

3. EnsureChildControls 的调用,
  这个方法实在是太重要了, 尽管我在第一篇的时候已经把代码贴出来过, 我想我有必要再贴出来让大家看看:

Code

该方法的本质就是调用CreateChildControls() 创建子控件集合, 只不过他做了必要的限制, 保障子控件不会被重复创建。 在Control 基类中,

有2 处地方调用了这个方法。第一个地方在Control 生命周期的Load 事件之后, PreRender 之前。 另一个就是在用户调用FindControl 的时候,

显然, 为了保障用户在Load 事件以及之前的事件当中的调用能够获取正确的结果,首先必须要调用  EnsureChildControls() 创建子控件。
根据以上的分析,我们可以得出这样的结论, 组合控件中任何与子控件发生交互的属性或方法, 只要我们不能确定用户将在什么时候调用。

都需要首先调用EnsureChildControls方法。比如我们为控件增加一个Text 属性。 这个属性的值与子控件中的textbox 值保持一致, 我们可以这样写:

 

Code

4. 重载Render 方法。 控制控件的显示样式和布局。
   子控件是有了, 但是所有的子控件简单的叠加在一起显然不是我们所希望的, 我们可以通过重载Render 方法来控制子控件的布局,比如我们

可以将子控件放到一个table 里面来控制子控件的位置。

 

Code

5 事件冒泡机制,
所谓事件冒泡机制就是将子控件的事件Mapping 为组合控件事件的一种机制。 首先我们利用冒泡机制来为我们的控件添加一个submit 事件,

然后再深入的探讨一下其中的机制。首先还是老方法为控件声明submit 事件

 

Code

声明之后我们该在什么地方引发事件呢?Control 基类为我们提供了一个OnBubbleEvent 的事件冒泡事件。

 

Code

设置一下断点, 启动调试,可以发现Submit 事件完美的被激发了. 看起来相当神奇,.NET 是如何做到这一点的呢?我尝试在Control 基类能找到了点
蛛丝马迹, 果然让我找到了RaiseBubbleEvent 方法。

 

Code

对照Button 控件的相关实现:

 

Code
不用我说了吧,神话就此终止。

控件开发系列, 写到这里, 感觉有点写不下去了, 一方面工作又紧张起来了,时间越来越不够了, 另一方面是由于有些东西自己也没有深入理解,

要花很多的时间去Research。
但是无论如何我都会尽自己最大的努力坚持下去, 在这里给自己打一下气。下一篇 服务器控件开发之复杂属性与视图状态管理 可能要等上一阵子才能
出来, 透透气先。
 


 

posted @ 2008-07-28 16:05 welkin 阅读(618) 评论(3) 编辑

2008年7月15日

服务器控件开发—— PostBack机制(4)

posted @ 2008-07-15 12:31 welkin 阅读(2177) 评论(4) 编辑

2008年7月11日

将DataTable 导出到Excel

posted @ 2008-07-11 14:08 welkin 阅读(2811) 评论(2) 编辑

2008年7月4日

服务器控件开发 - 事件机制(3)

posted @ 2008-07-04 16:27 welkin 阅读(1760) 评论(8) 编辑

2008年6月26日

服务器控件开发— WebControl(2)

posted @ 2008-06-26 09:26 welkin 阅读(2225) 评论(4) 编辑

2008年6月16日

服务器控件开发 —— Control 基类(1)

posted @ 2008-06-16 18:08 welkin 阅读(820) 评论(5) 编辑

2008年5月30日

Asp.Net 基于客户端回调的进度条控件

posted @ 2008-05-30 09:50 welkin 阅读(583) 评论(4) 编辑

导航

<2012年2月>
2930311234
567891011
12131415161718
19202122232425
26272829123
45678910

公告

昵称:welkin
园龄:3年8个月
粉丝:0
关注:0

搜索

 
 

常用链接

我的标签

随笔档案

最新评论

阅读排行榜

评论排行榜

推荐排行榜