玩转C科技.NET

从学会做人开始认识这个世界!http://volnet.github.io

导航

复合控件和事件(3)——事件基础

上一篇:复合控件和事件(2)——属性,页面要回发,属性要保存
本文的例子以CompositeControl来命名,但不代表本文是描述复合控件,只是这个系列都在描述这个而已,本文在描述的是控件的制作过程而非复合控件,因此命名只是为了保持解决方案的美观(真不理解自己为了美而放弃了事实,大家就将就一下哈!记住这不是复合控件只是控件)至于在复合控件的文章里面提到这个,理由在文章的和字之后,也就是事件。所以请您仔细阅读咯。后续的文章将会建立在此基础之上。】
每次当我们拿到Button的时候我们可以非常轻松地拖动它到页面,然后双击它就可以出现以下代码:
protected void Button1_Click(object sender,EventArgs e)
{

}
你是否正确理解它了呢?如果没有,请继续下一段,如果很了解请跳过。
   也许你会想这样一句话有什么好说明的,但其实它也是很重要的。它是一个函数,但是它更代表了一种固定于事件的函数,这种函数我们称之为事件处理程序。每当我们“按下按钮”事件被激发的时候,我们就会执行这里面的代码。事件处理程序的返回值是void的,用C#入门经典里面对返回值的描述是:
《C#入门经典》:可以为事件提供返回类型,但这会出问题。这是因为引发给定的事件,可能会调用好几个事件处理程序。如果这些处理程序都返回一个值,那么我们该用哪个返回值

系统处理这个问题的方式是,只允许访问由事件处理程序最后返回的那个值,也就是最后一个订阅该事件的处理程序返回的值
这个功能在某些情况下是有用的,但最好不要使用它们。推荐使用void类型的事件处理程序,且避免使用out类型的参数。
因此我们应该在这里使用void,Button1_Click是一个事件处理程序的名字,其实只是名字,你要换成AAABBBCCC也是可以的,当然你一定不会这么做的。object sender,EventArgs e在MSDN里面讲的很清楚了,就直接贴了:引发事件的源和该事件的数据。(事件和委托)说白了就是谁引发了事件,传递了什么数据。系统定义了一个EventArgs作为事件数据的基类,你可以定义自己的事件数据类,用于在事件处理的过程中传输数据。
那当你按下按钮的时候是如何得知事件被触发的呢?
先不要管“按钮按下的时候”这个关键字,先来看看事件一般都是如何触发的。
首先会有一个委托名为
public delegate void ××××EventHandler(object sender, ×××EventArgs e)
然后定义一个该委托类型的事件:
public event ××××EventHandler EventName
之后还有一个事件处理程序:
protected void OnEventName(×××EventArgs e)
{
……
}
事件是作为当前类的一个成员而存在的,我们可以在当前类中去引发它,应该理解为:在****条件下,我们会去处理我们的这个事件!而为了让这个事件的处理方式由外部提供,我们使用了委托来将具体的操作留给类的调用者,因此,我们在上面事件处理程序中的代码中不应该是具体解决问题的代码,而是一个委托,因此事件处理程序通常可以写为:
protected void OnEventName(×××EventArgs e)
{
if(EventName!=null)//如果事件存在的话
{
EventName(
this,e);
}

}
而在程序中的某个地方则会调用到OnEventName这个函数,并将其所需要的数据添加进e里,并传递给那个外部被委托的那个函数。也就是为什么我们能在外部使用e加上点运算符得到具体数据的原因了。
根据委托的定义,我们在使用它的时候还需要添加如下语句:
ClassName obj = new ClassName();

//订阅事件
obj.××××EventHandler += new ××××EventHandler(
Method_Handler);
或者C#
2.0内可以直接简化为:
obj.××××EventHandler 
+= Method_Handler;

//事件处理程序
protected void Method_Handler(object sender, ×××EventArgs e)
{
//使用者需要在事件被触发的时候需要实现的代码
}

而到我们控件的处理上,当我们拖动控件的时候,事实上ClassName obj = new ClassName();这句话的内容在****.aspx.designer.cs中,而订阅事件的部分也在<asp:…… OnEventName="Method_Handler" ……/>的句子中声明了,在页面预编译的时候将转换成订阅事件的标准IL代码。因此我们可以在我们使用控件的时候简单到只要有事件处理程序就可以了。
说了这么多其实只是把简单的事件运行的模型从语言层向整个以VS为开发平台的方向推了推,或者让开发者更了解自己做的是哪部分内容。

刚才还遗留了一个问题“按钮按下的时候”。这是什么问题呢?是这样的,事实上我们向页面添加一个服务端的按钮,运行的时候,我们添加的代码将从:
<asp:Button ID="Button2" runat="server" OnClick="Button2_Click" Text="Button" />

转换为:
<input type="submit" name="Button2" value="Button" id="Button2" />

这个过程看上去像是翻译,而事实上在控件的内部是将asp的那些代码中的Attribute填充到Html的控件中,比如Text="Button",就被映射到了value,runat="server"则体现了type="submit"的HTML标签,这些HTML标签从服务器传回了客户端,客户端通过IE等浏览器,对HTML进行解析,就可以得到我们所见到的丰富多彩的网页了。

说到这里你似乎还是没法理解“按钮按下的时候”这个问题,其实当HTML在传回到客户端后就和服务器之间失去了联系,那么“按钮按下的时候”事实上只是一个完全客户端的事情。如果你对HTML有所了解的话你会发觉这里的type="submit"指示了这个按钮不是只代表了一个看上去很有弹性的方块,它还表示它按下的时候与type="button"类型按钮的区别在于它会导致整个页面回发(仔细看浏览器下方中间(如果是IE的话)的进度条会有一次刷新,如果是button则没有任何反应)。页面回发会向服务端再次请求当前页面,也就是这次请求让客户端与服务端再次地彼此联系。关于页面回发,下面是MSDN的一些描述:

数据终于千里迢迢从客户端回到了服务端。服务端又如何知道该怎么做呢?由于是回发事件,了解回发的控件是必须要实现IPostBackEventHandler接口的(如果是类似TextBox中数据改变等的则需要实现IPostBackDataHandler接口。控件可引起回发,使用 RaisePostBackEvent 方法捕获回发。RaisePostBackEvent 是IPostBackDataHandler接口的一个方法,也是唯一的一个公共方法。RaisePostBackEvent当由类实现时,使服务器控件能够处理将窗体发送到服务器时引发的事件。也就是当有回发发生的时候会调用这个程序。我写了一个代码来验证它:

        IPostBackEventHandler 成员

既然回发的时候这个片段会被调用到,那么我们是不是应该乘机将我们定义的一堆事件处理程序一并处理掉呢?答案是当然的。所以我们可以将我们在程序中定义的事件处理程序在这里分享给外部,而之前我们讨论过,我们的事件处理程序其实只是将我们要做的事情委托给控件类的外部,那么实际上要处理的代码也就是我们通常在××××.aspx.cs文件中所做的处理程序比如:Button1_Click(object sender,EventArgs e)这段再也熟悉不过的代码了。于是以上程序应该被写为:
        IPostBackEventHandler 成员


这下应该明白,为什么我们可以轻松地去实现按钮按下的事件了,理由是MS为我们提供了IPostBackEventHandler。(其实IPostBackDataHandler也是基于同样的使用方式,只是里面的方法不同而已。)

IPostBackDataHandler专门用来处理有数据回发的程序的,我们让我们的类去实现这个接口,Render方法内添加以下语句:

writer.Write("<input id=\"Text1\" name=\"" + this.UniqueID + "\" type=\"text\" value=\"" + this.Text + "\" /><br>");

另外添加一个Text属性和实现IPostBackDataHandler的方法:

        public String Text
        
{
            
get
            
{
                
if (ViewState["Text"!= null)
                    
return (String)ViewState["Text"];
                
else
                    
return string.Empty;
            }


            
set
            
{
                ViewState[
"Text"= value;
            }

        }


        
IPostBackDataHandler 成员

这些代码您可以通过MSDN和asp.net控件开发基础(3) 文章来清晰地了解,这里将不再重复这个过程。下面是控件现在的截图:

点击submit后,则是:

填写数据后再点submit的结果:

为什么会无端多出submit呢?
我们可以断点调试这个问题(过程不展示了,比较罗嗦),经过分析发现问题所在:

public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)

就在这个postDataKey中,记得我们是如何命名我们的控件么?不记得了那就让我们看看最后生成的控件HTML:

<INPUT TYPE=button name=CompositeControl3_1 Value='确定' />
<input id="Submit1" name="CompositeControl3_1" type="submit" value="submit" /><br>
<input id="Text1" name="CompositeControl3_1" type="text" value="" />

其中它们都是name为CompositeControl3_1,其中Type=button的由于只是处于客户端的一个按钮,它的name其实无什么作用(name通常只被用于服务端控件,id则用于客户端)。而后两个name我们都是通过this.UniqueID来将其赋值的,这些让我们有些遭殃。因为postDataKey在断点时表现为CompositeControl3_1,而LoadPostData则通过postCollection来处理。如果只有一个Text就无所谓,但这里又多了一个Button也是同名的,这时候postedValue的值会将所有这个名字的服务端控件的value值用“,”(逗号)连接起来,类似:submit,填写结果

【MSDN】postCollection :所有传入名称值的集合。

改造1(无意义):可以将其name做一些手脚,让与Text的name区别于别人,这样可以达到效果,但是在之后的实现中,事件处理程序将不可用。理由是“它已经不再是它本身,本身的事件处理程序自然不属于它管”。
改造2:只取需要的数据:(将Text框内的数据单独取出)
        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            String presentValue = Text;
            String postedValue = postCollection[postDataKey];

            postedValue = GetText(postedValue, 1);

            if (presentValue == null || !presentValue.Equals(postedValue))
            {
                Text = postedValue;
                return true;
            }

            return false;
        }

        private string GetText(string controlText,int textIndex)
        {
            string[] strArray = controlText.Split(new char[] { ',' });
         if (strArray.Length > textIndex)
                return strArray[textIndex];
            else
                return controlText;
        }

这个问题似乎解决了,但是新的问题又来了。不知道你是否正在同步演练这个示例,如果是的话你一定注意到submit自己的事件处理程序不再被调用了。为什么呢?
在IPostBackEventHandler与IPostBackDataHandler同时实现的时候IPostBackEventHandler 会得不到响应。IPostBackDataHandler提前捕获了回发,让IPostBackEventHandler无法感应到了。这个时候我们需要手动补充注册一下:
Page.RegisterRequiresRaiseEvent(this);

这句话加在哪里呢?既然是IPostBackDataHandler捕获了,而它捕获后的第一件事就是LoadPostData,因此我就将它放置于此了。由于是Page的一个方法,则必然Page要存在,当然Page一定是在的,因为是回发嘛,回发一定有页面存在咯。所以有:
        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        
{
            //同时实现IPostBackEventHandler和IPostBackDataHandler需要手动注册
            //将 ASP.NET 服务器控件注册为需要在 Page 对象上处理控件时引发事件的控件。
            //由于该方法一定是在回发的时候才会有所作为,因此Page对象一定!=null.

            Page.RegisterRequiresRaiseEvent(this);

            String presentValue = Text;
            String postedValue = postCollection[postDataKey];

            postedValue = GetText(postedValue, 1);

            if (presentValue == null || !presentValue.Equals(postedValue))
            {
                Text = postedValue;
                return true;
            }

            return false;
        }
当然了,这个添加的位置可以在其他地方,这个你可以自己尝试,我只是提出了一种可以解决问题的答案。(您可以参考撰写与呈现一文)

问题真的解决了吗?(以下过程需要添加两个相同的该控件)同步演练时你一定会发现CompositeControl3_1的ControlClick事件已经沦陷咯(不起作用了)!理由呢?
注释掉下面的句子你再试试:

writer.Write("<input id=t" + this.UniqueID + " name=" + this.UniqueID + " type=\"text\" value=\"" + this.Text + "\" /><br>");

一切就恢复正常了。
原因是我们的页面中只有一个name=CompositeControl3_1,也只有一个name=CompositeControl3_2。但是如果我们补充了上面这句,则上面两个name的标签将出现两次,类似:

<INPUT TYPE=button name=CompositeControl3_1 Value='确定' /><input id=CompositeControl3_1 name=CompositeControl3_1 type="submit" value="submit" /><br><input id=CompositeControl3_1 name=CompositeControl3_1 type="text" value="" /><br>

我们平时是如何使用asp控件的时候我们是不允许它们的ID相同的,也就是最后生成的页面中name必须是唯一值:

//aspx页面中添加以下代码,将会出现设计时错误以及编译不通过的尴尬境地
        <asp:Button ID="Button1" runat="server" Text="Button" />
        
<asp:Button ID="Button1" runat="server" Text="Button" />


刚才我们用了Page.RegisterRequiresRaiseEvent(this);注册事件,让Event可以外部所激活,this先后顺序分别是CompositeControl3_1和CompositeControl3_2,也就是最后一次是CompositeControl3_2的事件被Raise了。注意MSDN中对于RegisterRequiresRaiseEvent的描述:

每个页请求只能注册一个服务器控件。当窗体发送数据中不包括控件的控件 ID 时,必须使用 RegisterRequiresRaiseEvent。而且,注册的控件必须实现 IPostBackEventHandler 接口。

也就是说这里只会执行CompositeControl3_2的事件。因此其CompositeControl3_1的ControlClick就会doesn't work了。

将Render中TextBox的语句修改为:

writer.Write("<input id=" + this.UniqueID + "_t name=" + this.UniqueID + "_t type=\"text\" value=\"" + this.Text + "\" /><br>");

运行的结果配合:Page.RegisterRequiresRaiseEvent(this);的结果则是OK的。理由是因此之前控件name重名了,LoadPostData就会被执行N次,N=控件的数量。也就是每次都会有不同的postDataKey进来,分别是CompositeControl3_1、CompositeControl3_2……它会将所有Post的数据进行一次检查,Page.RegisterRequiresRaiseEvent(this);的最终结果也就是最后一个事件了。但是如果名字正确会怎样呢?答案就是按哪哪应。也就是LoadPostData只执行一次,因此Page.RegisterRequiresRaiseEvent(this);的结果也就是当前的那个按钮了。至此我们可以配合以下代码正确运行程序了。

            Page.RegisterRequiresRaiseEvent(this);

            String presentValue 
= Text;
            String postedValue 
= postCollection[postDataKey + "_t"];
而且因为Page.RegisterRequiresRaiseEvent(this);这个句子是在页面回发的时候才执行的,而执行的过程中我们保证了它只被用于当前控件,因此所谓的每个页请求只能注册一个服务器控件在这里就可以得到满足了。
也正是因为名字的唯一化,GetText的函数从此可以直接注释掉咯!

方案二:既然我们可以按哪哪应,而且不管是IPostBackEventHandler还是IPostBackDataHandler 都会感应到回发事件,一旦回发,分别会有RaisePostBackEvent和LoadPostData为它们响应。看看IPostBackEventHandler的作用也无非就是将相关事件进行一次处理而已,Page.RegisterRequiresRaiseEvent(this);的唯一目的也就是将RaisePostBackEvent执行一遍,因此可以将Page.RegisterRequiresRaiseEvent(this);的位置直接替换成OnControlClick(new ControlEventArgs());——>测试,通过!
        public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        
{
            
//回发的时候对其进行事件处理
            OnControlClick(new ControlEventArgs());

            String presentValue 
= Text;
            String postedValue 
= postCollection[postDataKey + "_t"];

            
//postedValue = GetText(postedValue, 1);

            
if (presentValue == null || !presentValue.Equals(postedValue))
            
{
                Text 
= postedValue;
                
return true;
            }

            
return false;
        }
 

另外还有事件数据方面的问题需要小小地说明:事件数据是和事件相关地数据(废话),在事件处理程序中传递着,它继承于EventArgs(前面已经提及)。如果你有这方面的需要的话则可以自己写一个自己的事件数据类(感觉就像一个实体类,当然你可以有自己的方法,这方面很灵活):

 

    public class ControlEventArgs : EventArgs
    
{
        
private string message = string.Empty;
        
public string Message
        
{
            
get
            
{
                
return message ;
            }

            
set
            
{
                message 
= value;
            }

        }

    }


代码:
CompositeControl3.cs

CompositeControl3.aspx

CompositeControl3.aspx.cs

CompositeControl3.aspx.designer.cs

ControlEvent.cs



至此事件与控件的结合就显得相对明晰了。本文描述了详细的演化过程,大家会不会觉得有点混乱呢?如果会,请一定回头跟着演练,因为我的表达不保证到位,希望大家至少能够意会,别让我的言传让您误会噢~!^.^

有一篇文章不错,推荐给大家Understanding ASP.NET View State其中对页面生命周期的描述相当地详尽,其中一些涉及的知识点都是很有用的。

posted on 2007-07-09 22:57  volnet(可以叫我大V)  阅读(2506)  评论(1编辑  收藏  举报

使用Live Messenger联系我
关闭