春鱼·编程观点

技术在进步, 世界在变得美好...

导航

User Control (ASCX) 引发自定义事件的实现

[按]
本文论述了使User Control(文中很多处称为ASCX)可以向后端引发自定义事件的方法. 作者是一个偏向应用的开发人员. 因此可能某些方面涉论不深. 本着多思考, 和抛砖引玉的想法, 希望大家可以广泛批评. 作者的电子邮件xujian(a)nwpu.edu.cn

[知识需求]
1. ASCX机制
2. 事件编程
3. 回发数据处理
4. 数据绑定

[引言]
在与我认识的一些同行开发人员合作的过程中, 我发现他们很少使用ASCX. 并且将所有过程均在ASPX页面中实现, 他们仅仅使用了一点简单的对事件的编程. 而且没有分层. 他们在很大程度上依旧按照ASP3.0或者PHP的写法来写ASP.NET应用. 先不用说这样做在架构方面的弊病, 即使连页面构图的重用都很困难.

ASP.NET提供了User Control技术来简化一些应用. 总的来说, User Control与ASPX即(Web Form)没有什么不同. 但ASCX可以做为一个模块嵌入到ASPX页中.  所以我们可以将应用中的业务操作尽量封装到ASCX中.从而为某一项具体的功能设计一个密封的应用程序“块“. 这样再次设计相同操作的应用是可以直接在页面上引入该“块“.

什么是自定义事件?
事件是消息驱动的编程模型的“消息“. ASP.NET框架实现了类似WINDOWS平台的消息机制. 即将用户对浏览器中页面的操作简化为服务器端的消息. (这一过程是通过Post Back Data实现的), 对象通过向容器报告消息来维持程序运行. 消息中一般可包含数据. 即Event Arguments. 比如页面上有一个服务器端按钮(asp:Button), 它可以向其容器报告Click消息. 而该消息是通过一定的数据回发机制实现的.

事件与类设计
为控件开发事件目的是降低类型之间的依赖性. 明白地讲, 如果为对象实现了事件, 我们只要在一定时刻将事件引发就可以了. 而不用了解容器的逻辑. (一般而言, 容器内的对象无法了解容器的接口, 这个时候如果想完成某操作, 就得调用容器的方法). 容器捕获该事件时, 同时取得了事件的数据.

事件可以使程序结构更灵活
这个时候有的朋友可能会问. 为什么要给ASCX实现事件? 这就要从ASCX的设计谈起. 给ASCX设计事件的目的是提高程序的模块化. 前面我提到ASCX将作为某具体操作的模型, 例如“显示人员的名单(数据集列表)“就是很具体的操作. 但“显示人员名单“时一般我们会想看看某个具体人员的详细资料. 这个过程必然是鼠标点击人员的姓名, 或者每行都有的一个按钮, 此时看看我们的程序如何处理.  在ASP的时代. 我们自然会想到在每一行取得改行的主键标识, 然后在构造一个URL. 让URL带上参数, 让URL指向的页面处理这个过程. 这样的过程我们在ASP.NET框架下依旧可以实现. 但是我们现在是在ASP.NET时代. 上面这一个过程框架已经为我们隐藏地很好. 这个时候我们应该将这个过程归纳为事件. 而不用管容器如何处理.

任何类都可以实现事件
在谈如何实现之前. 先简单描述一下ASP.NET的消息机制. 下面这段话见于MSDN中文版: [在事件通信中,事件发送方类不知道哪个对象或方法将接收到(处理)它引发的事件。所需要的是在源和接收方之间存在一个媒介(或类似指针的机制)。.NET Framework 定义了一个特殊的类型(Delegate),该类型提供函数指针的功能。]  任何事件都是一个Delegetelel类型. 需要如下声明: (C#)
public event PageChangeEventHandler PageChange;
其中PageChangeEventHandler是一个Delegate. 需要在类中如下声明:
public delegate void PageChangeEventHandler(object sender, PageChangeEventArgs e);
其中PageChangeEventArgs是一个继承自System.EventArgs的类. 其结构是该事件应该包含的数据.
 public class PageChangeEventArgs: System.EventArgs
 {
  private int pageIndex;
  public int PageIndex
  {
   get
   {
    return this.pageIndex;
   }
   set
   {
    this.pageIndex = value;
   }
  }
 }

以上定义之后, 我们就可以在类中引发事件.
ClassName.PageChange(this, e)

难点在于
对于一般的应用. 我们都可以设计类来引发不同类型的事件. 但是现在问题是引发ASCX中的事件. 现在问题是:
1. 事件是用户在前端的操作引起的. 我们如何了解用户何时做了何种操作?
2. 如何封装事件的数据?

后端必须了解前端的操作. 也就是说, 用户在前端单击了某个链接或者按钮, 后端必须马上得到该消息. 这就需要及时向后端发送数据. 而发送数据必须通过post方式发送到后端. 这一过程框架本来是对程序员隐藏的. 而我们必须揭开其中的机制.

我们知道ASP.NET页中仅有一个ACTION为自身的FORM. 所有的数据都是通过该FORM发送到后端的. 页面的每次发回都会向后端发送数据, 而不论引起页面回发的对象是什么. 那么就要想办法让后端了解到应该处理发回的数据. 也就是说应该让服务器端控件具有这样一种能力, 可以处理自己在前台发送的数据. 在ASP.NET框架中, POST数据的处理是通过这样一种方式实现的: 每次页面发回, 框架都会检查前台发送到后端的数据(POST数据), 与当前页面上的服务器控件检查, 如果发现与某控件的UniqueID相同, 而且该控件实现了IPostBackDataHandler接口, 那么就调用由该控件实现的IPostBackDataHandler定义的方法LoadPostData. 如果设计控件可以处理发送到后端的事件, 就必须实现IPostBackHandler接口. 关于IPostBackDataHandler接口和回发数据处理, 见MSDN文档:
ms-help://MS.MSDNQTR.2003FEB.2052/cpref/html/frlrfsystemwebuiipostbackdatahandlerclasstopic.htm

实现技巧
现在我们明白了我们必须是控件的前端HTML可以发送特定的数据到后端, 而后端必须知道如何处理这一事件而且在处理过程中将数据封装到EventArgs中. 但是问题是, 我们要处理的是一个ASCX, 其前端构图是通过编写HTML实现的, 向后端发送数据应该不难办到.  通过一个隐藏的文本框input type=text就可以办到. 但是如何使数据的发送方的Name是当前ASCX的UniqueID呢. 每一个控件都有一个UniqueID的服务器端属性, 而且该UniqueID的命名方式决定了该ID在当前的页面是唯一的. 并且发送到客户端之前框架才可以确定该值. 所以ASCX的设计时期开发人员是无法得到该值的. 怎么办?

在设计时无法创建满足要求的控件, 就只有在运行时期动态生成. 查找System.Web.UI.UserControl的API文档, 我们发现该类提供了Controls集合. 而该集合有Add方法可以在运行时期向其内部增加WebControl类型的对象. 办法就有了. 在运行时期创建一个Input对象, 并且设置各个Attribute. 就圆满完成了需求. 需要注意的是, 控件构建时会自动增加name特性, 我们需要显式地清除该特性并且新增加多个特性, 具体代码可以如下:
 System.Web.UI.WebControls.WebControl box = new WebControl(System.Web.UI.HtmlTextWriterTag.Input);
   box.Attributes.Remove("name");
   box.Attributes.Add("type", "text");
   box.Attributes.Add("name", this.UniqueID);
   box.Attributes.Add("id", "icq116987221_hidden");
   box.Attributes.Add("style", "display:none");
   this.Controls.Add(box);

在前端生成的目标HTML代码如下:
<input type="text" name="ResultList" id="icq116987221_hidden" style="display:none" />
(其中id特性应设置便于识别和记忆的名称以便前台脚本调用, 另外应注意重复)
这样在该控件内就存在一个name特性为自己的UniqueID的控件, 可以向后端发送POST数据了. 只要这个不可见的element有值, 前端需要编写浏览器脚本处理由用户操作传送数据到后端的过程. 后端就可以捕获到, 并且调用该类实现的LoadPostBackData方法. 我们只要在这个方法中引发自定义的事件就可以了.
以下是类代码中有关事件和POST数据处理部分(非完整代码):
处理POST数据:
public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
  {
   string source = postCollection[postDataKey].Trim();
    ......
    XxxCommandEventArgs e = new XxxCommandEventArgs();
    e.CommandName = ...;
    e.Id = ...;
    this.RaiseCommandEvent(e);
   }
   return false;
  }

事件定义:
  /// <summary>
  /// 自定义事件委托
  /// </summary>
  public delegate void ExpertListCommandEventHandler(object sender, Professor.UserControls.ExpertListCommandEventArgs e);

  /// <summary>
  /// 定义事件
  /// </summary>
  public event ExpertListCommandEventHandler Command;

  /// <summary>
  /// 引发事件的方法
  /// </summary>
  public void RaiseCommandEvent(Professor.UserControls.ExpertListCommandEventArgs e)
  {
   this.Command(this, e);
  }
事件特性(参数)类:必须继承自EventArgs. 可包含多种类型数据.
 public class CommandEventArgs: System.EventArgs
 {
  private string commandName;
  /// <summary>
  /// 命令名称.
  /// </summary>
  public string CommandName
  {
   get
   {
    return this.commandName;
   }
   set
   {
    this.commandName = value;
   }
  }

  private int id;
  /// <summary>
  /// 操作对象: 被操作的专家ID数列
  /// </summary>
  public int Id
  {
   get
   {
    return this.id;
   }
   set
   {
    this.id = value;
   }
  }
 }

[结束]
MSDN是很好的文档库, 以下是有关文档
事件和委托:
ms-help://MS.MSDNQTR.2003FEB.2052/cpguide/html/cpconeventsdelegates.htm
回发数据处理:
ms-help://MS.MSDNQTR.2003FEB.2052/cpguide/html/cpconreceivingpostbackdatachangednotifications.htm
其实本文到这里并没有交待前端的脚本如何写. 因为ASCX应用不同, 无法一一尽言. 总之只要把右端数据写入到隐藏的域中并且设法POST到后端, 就可以解决问题. 需要各位朋友自由发挥.

posted on 2004-03-26 12:32  春鱼  阅读(3744)  评论(3编辑  收藏  举报