65.9K
CodeProject 正在变化。 阅读更多。
Home

在 ASP.NET 服务器控件中公开事件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (11投票s)

2009年5月26日

CPOL

7分钟阅读

viewsIcon

39419

在公开 ASP.NET 服务器控件事件方面的想法、方法和主要陷阱。

引言

尽管 ASP.NET 是一项成熟且被广泛接受的技术,但一些基本概念对开发人员来说仍然很不清楚。其中一个概念就是事件处理。该主题的复杂性源于 ASP.NET 页面和控件的生命周期以及定义事件处理程序的平台特定方法。

我在这里不讨论 ASP.NET 事件处理的基础知识。相反,我建议您首先阅读以下主题的更多内容:

快速简便的方法

对于简单的解决方案,您只需要以下内容:

public class MyControl : Control
{
    public event EventHandler MyEvent;
}

这种策略适用于原型设计和开始技术实验。上面代码片段中发生的情况是,您的控件会收到一个额外的 EventHandler 类型 *字段*。尽管简单,但这种方法的一个缺点是内存使用效率稍低。

问问自己:您是否总是处理页面上所有控件的所有事件?大多数用户只为一小部分事件定义处理程序。即使您的控件公开了事件,也不能保证它总是会被处理。大多数时候,您的字段值将是 null,仅仅占用内存。

通常,您应该始终让您的设计方法接受分析。如果您确实认为将处理程序存储在字段中适合您的解决方案,那就去做吧!但如果您仍然愿意尝试,让我们继续下一部分。

以正确的方式公开事件

由于我们的控件总是继承自 System.Web.UI.Control 类,因此首先研究其基类为我们的控件提供了哪些功能是有意义的。

如果您使用的是 Visual Studio 2005 或更高版本,一种很好的方法是使用“转到定义”命令。Visual Studio 会自动显示一个看起来很像源文件的窗口。整个类接口都在那里显示(所有 publicprotected 成员,没有标记为 internal)。简短的文档也在那里。非常信息丰富,不是吗?

我们需要的是 Events 集合

protected EventHandlerList Events { get; }

此属性是 protected 的事实意味着它将用于派生类。实际上,如果您进行一些 *反射*,您会发现情况确实如此。

现在,让我们看一下 System.ComponentModel.EventHandlerList

namespace System.ComponentModel
{
  public sealed class EventHandlerList : IDisposable
  {
    public EventHandlerList();

    public Delegate this[object key] { get; set; }

    public void AddHandler(object key, Delegate value);
    public void Dispose();
    public void RemoveHandler(object key, Delegate value);
  }
}

尽管此类看起来很像一个用于存储委托的字典,但其构建和使用方式存在根本性差异。

首先,它不如专门制作的字典有效。文档说明:

此类使用线性搜索算法在委托列表中查找条目。当处理大量条目时,线性搜索算法效率低下。因此,当列表很大时,查找条目速度很慢。

稍后我们将讨论如何解决这个问题。

其次,此类最可能的用法场景并不意味着这样的事情:

EventHandlerList aList = new EventHandlerList();
object aKey = new object();

aList[aKey] = new EventHandler(MyHandler); //ery unlikely to be used

事实上,我甚至不明白为什么 set 访问器会存在。如果您对此有任何想法,我很乐意了解。AddHandlerRemoveHandler 是最常用的两个方法。因此,我们事件的新代码如下所示:

public event EventHandler MyEvent
{
  add    { Events.AddHandler(key, value); }
  remove { Events.RemoveHandler(key, value); }
}

上面的代码几乎很简单,除了一个问题:key 的意思是?而这正是字典和 EventHandlerList 之间第三个差异发挥作用的地方。

我感到惊讶的是,文档中没有提到这种差异。如您所知,.NET Framework 中的大多数关联容器(HashtableDictionary 等)都使用 *哈希* 和 *值* 相等性检查来确定两个对象是否相同。但是,对于 EventHandlerList 则不是这样,它使用 *引用* 相等性和 *无哈希*。有了这些知识,请回答一个简单的问题:**可以作为委托查询的键使用什么?**

答案是:**任何引用类型的对象,其生命周期等于或长于控件的生命周期。** 换句话说,*不能* 用作键的是:

  • 数值(整数、浮点数、布尔值)
  • 结构体(以及因此的枚举)

如果您仍然对为什么不能使用 int 感兴趣,请查看索引器的原型:

public Delegate this[object key] { get; set; }

现在,如果我们向它传递一个 int 会发生什么?没错,*装箱*。值类型实体首先被包装到引用类型的容器中。然后,这个新的引用类型容器被传递到引用相等运算符(==)中。只需编译并运行以下控制台程序:

static void Main(string[] args)
{
  int a = 1;
  int b = 1;

  object aA = (object)a;
  object aB = (object)b;

  Console.WriteLine("a==b returns:   {0}", a==b);
  Console.WriteLine("aA==aB returns: {0}", aA==aB);
}

就像第二个 Console.WriteLine 输出 *False* 一样,索引器将始终无法将您的整数与集合中已有的任何内容匹配。相同的概念也适用于结构体和枚举。

另一方面,*可以* 用作键的是:

  • this 引用
  • 任何引用类型的实例字段
  • 任何引用类型的静态字段

但即使在这里也可能有细微差别。如果您的控件公开了多个事件,您就只能使用一个,其键是 this。您将无法区分一个事件和另一个事件。实例字段绝对可以用作键,但请记住我们为什么开始尝试事件的替代方法:内存优化。一旦我们摆脱了字段,为什么还要引入另一个字段呢?因此,我建议您在公开事件时始终使用静态字段。

static readonly object ourKey = new object();

public event EventHandler MyEvent
{
  add    { Events.AddHandler(ourKey, value); }
  remove { Events.RemoveHandler(ourKey, value); }
}

protected void OnMyEvent(EventArgs e)
{
  EventHandler aH = Events[ourKey] as EventHandler;

  if (aH != null)
    aH(this, e);
}

您也可以使用其他类型。但是,object 是最紧凑的,这正是我们想要的。此外,想象一下使用字符串。尝试确定以下两种用法场景哪种会起作用:

  1. 场景 1
  2. protected void OnMyEvent(EventArgs e)
    {
      EventHandler aH = Events["MyEventKey"] as EventHandler;
    
      if (aH != null)
        aH(this, e);
    }
  3. 场景 2
  4. static readonly string ourKey = "MyEventKey";
    
    protected void OnMyEvent(EventArgs e)
    {
      EventHandler aH = Events[ourKey] as EventHandler;
    
      if (aH != null)
        aH(this, e);
    }

即使字符串是引用类型,它们也是 *不可变的*。也就是说,每次构造(或修改)字符串时,都会创建一个新实例。即使两个字符串包含完全相同的字符序列:如果它们是独立构造的(或一个被转换成具有另一个的*值*),它们的引用也会指向内存中的不同位置。只需编译并运行以下控制台应用程序:

static void Main(string[] args)
{
  string a = "foobar";
  string b = "foo" + "bar";
  string c = b;

  object aA = (object)a;
  object aB = (object)b;
  object aC = (object)c;

  Console.WriteLine("a==b returns:   {0}", a==b);
  Console.WriteLine("b==c returns:   {0}", b==c);
  Console.WriteLine("a==c returns:   {0}", a==c);

  Console.WriteLine("-----------------------------------------");

  Console.WriteLine("aA==aB returns: {0}", aA==aB);
  Console.WriteLine("aB==aC returns: {0}", aB==aC);
  Console.WriteLine("aA==aC returns: {0}", aA==aC);
}

令人惊讶的是,所有六行都打印“*True*”,让您认为我疯了。老实说,我一开始对结果感到惊讶。但是,在分析了代码之后,我得出结论,是 CLR 优化技术导致了这种行为!基本上,CLR“看到”两个完全相同的字符串字面量,只为两者分配了一个内存块。只需做一些更复杂的字符串处理,您就会失去优化效果。

string a = "foobar";

// string b = "foo" + "bar";
string b = ("0foo" + "bar").Substring(1);
...

如果您对为什么 aA==aB 在原始示例中返回 true 有其他想法,请在您的评论中分享。

最终,即使这两种用法场景*可能*有效,我想强调的是,使用字符串作为键仍然很尴尬,因为您可能会误解字符串的值相等性和引用相等性。理想的解决方案是使用 static readonly object 字段,因为:

  1. 它占用的内存量最小
  2. 您只能通过获取引用来处理它
  3. 因为它没有值,所以您无法误解值相等性和引用相等性

另外值得注意的是编写传统 OnMyEvent 方法的方式:

protected void OnMyEvent(EventArgs e)
{
  EventHandler aH = Events[ourKey] as EventHandler;

  if (aH != null)
    aH(this, e);
}

如果您就这样保留它,您将失去 .NET Framework 开发人员最初设计的优化优势。Events 属性在首次引用时按需创建。为了保持一致性,让我们添加一个条件:

protected void OnMyEvent(EventArgs e)
{
  if ( !HasEvents() )
    return;

  EventHandler aH = Events[ourKey] as EventHandler;

  if (aH != null)
    aH(this, e);
}

这几乎完成了,除了我承诺要对 EventHandlerList 类中效率低下的线性搜索算法提供一些想法。首先,如果您确实非常关注性能和速度,您可以回归最初基于字段的方法。第二,如果您的控件公开了多个事件,您可以将它们存储在单独的 EventHandlerList 中,您可以自己添加。您也可以添加一个更优化的类。但是,只有当您的控件确实公开了大量事件时,您才会注意到差异!这样,它们就不会与标准的控件事件混淆,从而减少搜索时间。第三,如果您的控件以循环方式触发事件,那么完全可以去掉 OnMyEvent 方法并将事件搜索移出循环。

private void FireMyEventForAll(EventArgs e)
{
  if ( !HasEvents() )
    return;

  EventHandler aH = Events[ourKey] as EventHandler;

  if (aH == null)
    return;

  for (int i = 0; i < NumIterations; i++)
    aH(this, e);
}

最后,.NET Framework 的开发人员可能会在他们未来的某个版本中改进他们的搜索算法。类似 HybridDictionary 的行为将非常有意义。这个更改不太可能在 .NET Framework 4.0 版本中发生,因为它的第一个 beta 版使用了旧的、无效的实现。

历史

  • 2009 年 5 月 26 日:初始帖子。
  • 2009 年 6 月 8 日:关于 4.0 版本的小更新。
© . All rights reserved.