wwwyfjp

The more you learn, the less you know
随笔 - 8, 文章 - 0, 评论 - 27, 引用 - 4
数据加载中……

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 已经有了一个全面的了解。下面一篇我将带领大家利用ViewState 来为我们的自定义控件创建
复杂的控件属性。
 

posted @ 2008-09-12 17:08 welkin 阅读(136) | 评论 (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 阅读(250) | 评论 (3)编辑

2008年7月15日

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

     摘要: 在我的第一篇文章讨论控件生命周期的时候。提到控件生命周期中的LoadViewState, LoadPostBackData, RaiseChangedEventsRaisePostbackEvent 等几个阶段是页面在postback 中所特有的。深刻得理解控件的postback 机制是开发专业的 自定义控件所必须的基本功。1. 如何引发一个Postback. Asp.nt 内置有2 种方法可以引... 阅读全文

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

2008年7月11日

将DataTable 导出到Excel

     项目中需要将DataTable导出到Excel文件。 到网上查了查相关的资料, 决定采用托管Excel 来实现,  很快就完成了。
    正在得意中, 突然发现Excel进程无法释放。 于是深入网络查找答案,一会儿让我找到了一个killExcelProcess() 的方法 它通过调用Win32 API
    在Excel 退出 之后强行杀掉Excel.exe进程。 在本机测试无误, 但是一放到服务器上 , 就出现权限问题。 只好想别的办法了。
    拜读网上达人的帖子,一个一个 的试验, 花了半天的时间总算搞定。
    下面是我的代码。 注意看我的注释。
public static bool ExportToExcel(DataTable table, string excelName, int[] columnIndexs, string[] columnHeads)
    {
        
#region 将方法中用到的所有Excel变量声明在方法最开始,以便最后统一回收。
         Excel.ApplicationClass oExcel 
= new Excel.ApplicationClass();
        Excel.Workbook obook 
= null;
        Excel.Worksheet oSheet 
= null;
        Excel.Range range 
= null;
        
#endregion
        
try
        {
            obook 
= oExcel.Workbooks.Add("");
            oSheet 
= (Excel.Worksheet)obook.Worksheets[1];
            
int rCount, cCount;
            rCount 
= table.Rows.Count;
            cCount 
= table.Columns.Count;
            
object obj = System.Reflection.Missing.Value;

            
if (cCount < columnIndexs.Length || cCount < columnHeads.Length)
            {
                
throw new ArgumentOutOfRangeException("columnIndexs 与 columnHeads 长度必须一致。");
            }
            
for (int i = 1; i <= columnIndexs.Length; i++)
            {
                
//Excel.Range = (Excel.Range)oSheet.Columns.get_Item(i, obj);  
                range = (Excel.Range)oSheet.Columns.get_Item(i, obj);
                range.NumberFormatLocal 
= "@";
            }
            
for (int c = 0; c < columnIndexs.Length; c++)
            {
                oSheet.Cells[
1, c + 1= columnHeads[c];
                
for (int r = 1; r <= rCount; r++)
                {
                    oSheet.Cells[r 
+ 1, c + 1= table.Rows[r - 1][columnIndexs[c]].ToString();
                }
            }
            obook.SaveCopyAs(excelName);
            
//必须调用 obook.Close(), 否则无法释放进程。
            obook.Close(false, System.Reflection.Missing.Value, System.Reflection.Missing.Value);
            
return true;
        }
        
catch (Exception ex)
        {
            
throw ex;
        }
        
finally
        {
           
// 调用System.Runtime.InteropServices.Marshal.ReleaseComObject(object) 方法释放方法中
            
//用到的所有的Excel 变量, 记住是所有的。 比如说此方法中的range 对象, 就容易被遗忘。
            
             ystem.Runtime.InteropServices.Marshal.ReleaseComObject(range);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(oSheet);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(obook);
            
            
// 很多文章上都说必须调用此方法, 但是我试过没有调用oExcel.Quit() 的情况, 进程也能安全退出,
             
//还是保留着吧。
            oExcel.Quit();
            System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcel);
            
// 垃圾回收是必须的。 测试如果不执行垃圾回收, 无法关闭Excel 进程。
            GC.Collect();
        }
    }

   


人格担保, 以上代码在VS 2005, VS2003, 中使用EXCEL 2003  测试通过。

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

2008年7月4日

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

    在上一篇的基础上, 我们来看看如何为我们的服务器控件添加事件支持。 丰富的事件支持能让控件的使用者最大限度的参与控件的生命周期,最大限度得提高控件的重用性。
     每一个服务器控件都从Control基类继承了 OnInit, OnLoad, OnPreRender, and OnUnLoad 等 4 个事件. 在页面开发中, 开发人员可以通过2种方式来注册ASP.NET事件.
1. 通过声明的方式
     <asp:Button id="button1" OnClick="button1_Click" Text="Submit" runat="server" />
2. 通过编程的方式
     button1.Click += new EventHandler(this.button1_Click);
    另外一点需要注意的是, 在Asp.net 2.0 里面. 页面模型自动将Page_Init, Page_Load, Page_PreRender, and Page_Unload 4 个方法绑定到页面对应的事件.
而不需要显式的声明. 这个特性是通过在页面声明中加入 AutoEventWireup="true" 来实现的. 如果发现页面不触发Page_Load 事件. 一个可能的原因就是
AutoEventWireup 的值被设为 False.
    在C# 中 定义一个典型的事件代码如下:
Code
    然而, 在ASP.NET 中, 微软并没有采用这样一种方式来定义事件.而是基于Web 开发的特点进行优化, 采用了另外一种方式. 书中提到了2 点之所以不采用
    经典事件实现模式的原因:
    1.  不管事件是否被注册到具体的方法, 每当我们定义一个事件成员, 比如说    public event ClickEventHanlder OnClickEvent = null;  编译器都会为
        之生成一个私有的代理,比如: delegate void ClickEventHanlder(object sender, EventArgs e). 一但事件数目比较多, 将会浪费很多宝贵的内存空间
    2.  编译器会为经典事件模式生成一大堆的线程同步的代码. 这样不但降低了执行效率而且是毫无必要的, 因为页面开发者很少在单个页面使用多线程.
 
现在让我们来看一下针对于Asp.net 的优化的 基于System.ComponentModel.EventHandlerList 类的事件实现模式, 
首先来看看Control 中 OnInit 事件的实现.
 1. 声明一个 EventHandlerList  类型的事件索引,用来保存代理列表。
Code
 Control 基类为每一个事件定义了一个静态只读的键, 用来从EventHandlerList 中获取事件代理, 或者将事件代理添加到事件列表上.
Code
      注意这个键是静态共享的, 这意味着所有的控件的所有实例共享这个键.

 2. Control 基类使用一个事件属性来定义事件. 而不是使用事件成员.
Code

       当用户向事件列表添加一个事件代理, Events 类首先通过事件的键 判断 事件是否已经添加。 如果未添加, 则添加,如果键已经存在, 则将代理方法添加到键所对应的代理的代理列表上。
       当用户再事件列表移除一个事件代理, 过程刚好相反。

  3. 定义一个事件触发器, 通过一个hook, 调用代理方法。

Code


 最后  在页面的特定阶段触发事件, OnInit 事件的触发可以参考我第一篇当中的 InitRecursive方法

通过以上方式就可以任意的为我们的自定义控件添加事件。 而不需要去考虑任何的性能问题。

下一篇  : 服务器控件的 PostBack 机制。


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

2008年6月26日

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

     摘要: 上一篇里我们介绍了Control 基类, 这一篇我们来看看WebControl 类。 Asp.Net 里面的大部分控件都是从WebControl 继承的, WebControl 与 Control 相比。 提供了一系列支持控件样式的属性。 如果你的控件需要相客户端呈现HTML标签。从WebControl 继承将省去你不少的工作。 这些属性以及说明列举如下:AccessKey String The ... 阅读全文

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

2008年6月16日

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

     摘要: 要想熟练的开发服务器控件 首先需要了解asp.net 中服务器控件的生命周期。其实服务器控件的生命周期与asp.net 页面的生命周期差不多, 因为asp.net 页面其实就是间接从Control继承。我们来看每个阶段Control控件里面都做了一些什么事情1. Instantiate : 控件被页面或者另一控件调用, 实例化。2. Initialize :3. Begin Tracking Vi... 阅读全文

posted @ 2008-06-16 18:08 welkin 阅读(477) | 评论 (3)编辑

2008年5月30日

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

     摘要: 这是本人第一篇博客,首先相各位博友问个好。博客注册了有好长一段时间, 一直没有时间写点什么东西。最近项目终于不像以前忙了, 想起来写点什么。 前段时间在项目遇到一个长任务显示进度条的问题。 在网上找了好久都没有找到好的控件。要么下下来bug 一堆, 要么就是用起来极端不方便。于是琢磨着自己写一个。站在各位前辈的肩上,花了2 天的时间终于让我弄出来了。总的思路就是: 客户端使用Javascript ... 阅读全文

posted @ 2008-05-30 09:50 welkin 阅读(236) | 评论 (3)编辑