第5章 事件和数据回发机制
5.1.1 事件及其意义
1.注册事件
<asp:Button ID="Button1" runat="server"OnClick="Button1_Click"Text="Button"/>
或在Page_Load中注册:
protected void Page_Load(object sender, EventArgs e)
{
this.Button1.Click +=new EventHandler(Button1_Click); //+=运算,解释见下文“更深层了解一下Button的事件...”
}
首先从事件要实现的功能角度理解事件。我们把按钮(Button)看做一个对象,把页面(Page)也看做一个对象。
正向理解:假如我们在Page对象中要修改Button的行为,可以直接通过this.Button1的形式直接访问Button对象的属性或方法对Button进行修改,可以理解为Page类能访问到Button代码功能。原因是Button1是Page类内部的一个对象,类当然可以直接访问其内部的对象。
逆向理解:假如我们需要在Button中要访问Page中的代码呢?直接像上面那样通过this.Page的形式是不行的,因为Button是Page类的内部对象,但Page不是Button类的内部对象,从面向对象角度讲,类(Button)不能访问其外部的对象(Page),也就是说在Button中不可能通过this.Page的形式访问到Page对象。而使用事件机制就可以解决此问题,即事件机制解决了面向对象编程中不允许类访问类外部代码问题。
更深层了解一下Button的事件:假如我们没有对Button注册Click事件,则Button会执行一遍它内部的Click相关逻辑,并没有对Page对象产生任何影响;如果我们为Button定义了Click事件(如上面代码片段),则Button还是执行一遍它内部的Click相关逻辑,不同的是在执行自己内部逻辑的过程中它还执行了Page对象中的一些代码功能(即Button的事件体Button1_Click方法),就达到了我们要实现的功能。通过触动一个对象Button影响到另一个对象Page的行为,并且在Button的事件体中即可以修改Button本身(通过sender或this.Button1),也可以修改Page页面对象的其他控件或执行任意想要的代码功能。
5.1.2 数据回发机制
很多时候我们要在按钮提交的服务端事件中处理提交之前的数据和提交按钮时用户输入的最新数据,即想同时得到文本框的旧值和新值,而服务端不会保存前一个请求的任何信息,那怎样才能做到这一点呢?两次页面请求之间的数据关联性问题,ASP.NET是通过视图机制实现的。有了视图机制,在其基础之上的数据回发机制就是完成处理视图信息的功能。具体过程是,服务端控件只要实现IPostBackDataHandler接口,则当客户端提交请求后,就会有机会利用IPostBackDataHandler接口的LoadPostData方法,在该方法内部处理子控件的新旧值逻辑,而视图信息数据这时以一个集合对象形式作为LoadPostData参数,并可以决定是否引发控件值变化后的事件。这就是要引入数据回发机制功能的原因。
5.2 事件和数据回发机制的实现
5.2.1 客户端回传事件接口IPostBackEventHandler
要使控件捕获回发事件,控件必须实现System.Web.UI.IPostBackEventHandler 接口。此接口约定允许控件在服务器上引发事件来响应来自客户端的回发。
[ToolboxData("<{0}:PostBackEventControl runat=server></{0}:PostBackEventControl>")]
public class PostBackEventControl : Control, IPostBackEventHandler
{
public event EventHandler Click;
protected virtual void OnClick(EventArgs e)
{
if (Click != null)
{
Click(this, e);
}
}
public void RaisePostBackEvent(string eventArgument)
{
OnClick(EventArgs.Empty);
}
protected override void Render(HtmlTextWriter output)
{
output.Write("<INPUT TYPE=submit name="+this.UniqueID+"Value='单击我'
/>");
}
}
由于这个按钮类型为submit,所以当单击按钮时,其本身已经可以提交事件到服务器,但仅仅这样主控件还不能够捕捉到该按钮事件。
控件能够捕捉处理该事件需要具备两个条件:第一,主控件继承了IPostBackEventHandler接口以及实现了RaisePostBackEvent方法;第二,必须有name值为UniqueID的客户端标签,页框架只认识控件的name属性。只有这两个条件同时具备才能够使控件具备捕捉并处理事件的机会。
5.2.2 客户端回发(PostBack)/回调(CallBack)揭密
客户端回发与客户端回调的区别在于网页处理客户端回发事件要用完一个正常的生命周期,而GetCallbackEventReference是异步请求,在客户端回调中,客户端脚本函数向ASP.NET网页发送异步请求,网页修改其正常生命周期来处理回调。两者调用是有些区别的。
5.2.2.1 设置HTML Button标记的类型为submit从客户端提交回发
output.Write("<INPUT TYPE=submit name=" + this.UniqueID + " Value='单击我' />");
5.2.2.2 使用方法GetPostBackEventReference 得到回发脚本
1.为HTML客户端控件增加回发功能
output.Write("<INPUT TYPE=button name=" + this.UniqueID + " Value='[使用提交按钮]' />");
为了使一般按钮也具有回发的功能,ASP.NET提供了Page.ClientScript.GetPostBackEventReference方法。ClientScript类型为ClientScriptManager,该类主要功能是在Web应用程序中定义用于管理客户端脚本的方法。
在期望不执行回发而从客户端运行服务器代码的情况下,可以使用ClientScriptManager类来调用客户端回调。这称为对服务器执行带外回调。在客户端回调中,客户端脚本函数向ASP.NET网页发送异步请求。网页修改其正常生命周期来处理回调。使用GetCallbackEvent Reference方法获取一个对客户端函数的引用,当调用该函数时,它将启动一个对服务器端事件的客户端回调。
使用GetCallbackEventReference方法对上面代码增加回调客户端功能,修正后的代码如下:
output.Write("<INPUT type=button name=\"{0}\" value='[使用Page.ClientScript对象方法]' onclick=\"{1}\">", this.UniqueID, Page.ClientScript.GetPostBackEvent Reference(this, ""));
2.为服务端控件生成回发脚本
比如在使用Button控件时把UseSubmitBehavior属性设置为false,则禁用按钮的自动提交功能,就可以使用GetPostBackEventReference方法返回Button控件的客户端回发事件脚本,代码如下所示:
string strPostBackCode = this.Page.ClientScript.GetPostBackEventReference(button1,”edit”);
然后把Button的客户事件与生成的回发事件脚本进行关联:this.button1.Attributes[“onclick”] = strPostBackCode;
5.2.2.3 使用方法GetPostBackClientHyperlink得到回发脚本
output.AddAttribute(HtmlTextWriterAttribute.Href, href);
output.RenderBeginTag(HtmlTextWriterTag.A);
output.Write("[使用Page.ClientScript对象的GetPostBackClientHyperlink方法]");
output.RenderEndTag();
HTML输出:<a href="javascript:__doPostBack('PostBackFromClientControl1','')">[使用Page.ClientScript对象的GetPostBackClientHyperlink方法]</a>
5.2.3 回传数据处理接口IPostBackDataHandler
5.2.3.1 数据回发和回传事件
IPostBackDataHandler接口用于检查提交给页面的数据,并确定数据是否在客户端修改过。当控件实现该接口,控件则自动具有了参与回传数据的处理能力。开发人员可以通过实现接口相关成员,完成针对回传数据的处理逻辑。
IPostBackDataHandler接口将ASP.NET服务器控件定义为自动加载回发数据而必须实现的方法。LoadPostData方法用来检查提交给服务器的数据,根据控件状态数据和回发数据是否发生更改而判断是否调用RaisePostDataChangedEvent方法,如果返回true,则.NET Framework会自动调用RaisePostDataChangedEvent方法,在此方法中可以引发自己定义的事件。
数据回发处理和数据回发事件就讲这些,最后再总结一下其要点步骤:
1、设置主控件的name值为UniqueID。
2、实现LoadPostData方法,处理自己的数据比较逻辑,返回布尔值(返回true或false是由我们自己决定的)。
3、实现RaisePostDataChangedEvent,在该方法加入自定义事件。
4、如果LoadPostData方法返回true,页框架会自动调用RaisePostDataChangedEvent方法。
5.2.3.2 把控件注册为要进行回发处理的控件
TextBox tb = new TextBox();
protected void Page_PreRender(object sender, EventArgs e)
{
this.Page.RegisterRequiresPostBack(tb); //引发异常
}
要注册的控件必须实现IPostBackDataHandler接口,否则将引发HttpException。
当控件实现IPostBackDataHandler接口时,该接口将可以进行回发数据处理,并可以引发任何回发数据已更改的事件,而且必须要在页生命周期的Page_PreRender事件中或该事件之前向页注册当前控件为要进行数据回发处理的控件。
5.2.3.3 数据回发及引发回发事件示例
实践验证一下IPostBackDataHandler是否真的能够完成它的功能。我们自己做一个TextBox控件,不但要显示文本,而且能够执行数据回发事件。
该控件应该继承IPostBackDataHandler接口,并实现LoadPostData和RaisePostDataChangedEvent方法,完整代码如下:
[DefaultEvent("TextChanged")]
[ToolboxData("<{0}:KingTextBox runat=server></{0}:KingTextBox>")]
public class KingTextBox : Control, IPostBackDataHandler
{
public KingTextBox()
{
}
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["Text"] = value;
}
}
protected override void Render(HtmlTextWriter writer)
{
StringBuilder sb = new StringBuilder();
sb.Append("<input type=\"text\" name=");
sb.Append("\"" + UniqueID + "\"");
sb.Append("value=");
//HttpUtility.HtmlEncode 将用户输入字串转换成HTML格式,主要将用户输入的HTML关键字转义为非HTML关键字字符
sb.Append("\"" + HttpUtility.HtmlEncode(Text) + "\"");
sb.Append(" />");
writer.Write(sb.ToString());
}
public virtual bool LoadPostData(string postDataKey, NameValueCollection
postCollection)
{
string strOldValue = Text;
string strNewValue = postCollection[this.UniqueID];
if( strOldValue == null || ( strOldValue != null && !strOldValue.Equals
(strNewValue)))
{
this.Text = strNewValue;
return true;
}
return false;
}
public virtual void RaisePostDataChangedEvent()
{
OnTextChanged(EventArgs.Empty);
}
//此方法体中判断开发人员在页面使用控件时是否注册了TextChanged 事件,如果注册了就调用开发人员的事件逻辑。
public event EventHandler TextChanged;
protected virtual void OnTextChanged(EventArgs e)
{
if (TextChanged != null)
{
TextChanged(this, e);
}
}
}
测试页:
<tr>
<td style="width: 75px">
King TextBox</td>
<td style="width: 3px">
Asp.NET TextBox
</td>
</tr>
<tr>
<td style="width: 75px; height: 21px">
<cc1:KingTextBox ID="KingTextBox1" runat="server" OnTextChanged=
"KingTextBox_TextChanged">
</cc1:KingTextBox> </td>
<td style="width: 3px; height: 21px;">
<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></td>
</tr>
<tr>
<td colspan= 1>
<asp:Button ID="btnCopy" runat="server" Text="Copy>>>" OnClick=
"btnCopy_Click" /></td>
<td colspan= 1>
<asp:Button ID="btnCopy2" runat="server" Text="<<<Copy" OnClick=
"btnCopy2_Click" /></td>
</tr>
</table>
<br />
<asp:Label ID="Label1" runat="server" Width="325px"></asp:Label>
测试页对应代码:
{
this.TextBox1.Text = this.KingTextBox1.Text;
this.KingTextBox1.Text = "";
}
protected void btnCopy2_Click(object sender, EventArgs e)
{
this.KingTextBox1.Text = this.TextBox1.Text;
this.TextBox1.Text = "";
}
protected void KingTextBox_TextChanged(object sender, EventArgs e)
{
this.Label1.Text = "The KingTextBox's TextChanged event runed.";
}
事件顺序:LoadPostData - Page_Load- RaisePostDataChangedEvent - OnTextChanged - KingTextBox_TextChanged - btnCopy_Click - Render
5.2.4 正确处理继承基类中控件的事件
上面的两个函数:
public virtual void RaisePostDataChangedEvent()
{
OnTextChanged(EventArgs.Empty);
}
protected virtual void OnTextChanged(EventArgs e)
{
if (TextChanged != null)
{
TextChanged(this, e);
}
}
看上去有些啰唆,如果把OnTextChanged方法去掉,修改后的代码如下所示:
public virtual void RaisePostDataChangedEvent()
{
if (TextChanged != null)
{
TextChanged(this, e);
}
}
看上去比较简练,但这种写法对于继承此控件的控件,处理基类中事件有些限制,解释如下:
如果定义了一个KingTextBoxExtend控件,并且此控件是从KingTextBox扩展而来,则我们在KingTextBoxExtend中重写OnTextChanged方法,重写后的方法如下所示:
protected virtual void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e); //保留默认的逻辑
//在这里可以增加自己的逻辑
}
至此,KingTextBoxExtend控件中的OnTextChanged能够保证基类中所有的功能,并且可以扩展,也可以干脆把base.OnTextChanged(e)这句注释掉。
如果换成第二种写法,那么,KingTextBoxExtend控件根本没有机会重写基类中的OnTextChanged方法。
即使在KingTextBoxExtend控件中重写RaisePostDataChangedEvent事件,如果事件比较多的话,会使代码比较乱,难于控制每个事件的可执行性,最重要的是LoadPostData如果返回false,RaisePostDataEvent根本不会执行,那么这些事件也不会执行。
5.3 复合控件的事件处理机制