用户控件事件处理方法






4.76/5 (17投票s)
本文将讨论用户控件中处理事件的各种方法。
引言
在本文中,我们将讨论在用户控件中处理事件的各种方法。有关用户控件的更多详细信息,请访问http://msdn.microsoft.com/en-us/library/y6wb1a0e.aspx。
您可以将用户控件视为一个自成一体的复合控件,例如用于显示登录用户详细信息且不与宿主页面交互的控件。但是,我们也可以拥有包含按钮、链接并应与宿主页面(容器)交互的用户控件。
ASP.NET 用户控件引发的事件与 Windows 应用程序中的事件不同。在 ASP.NET 或基于 Web 的应用程序中,事件在客户端引发,在服务器端处理。您可以查看以下用户控件事件模型图来理解这一点。
为了通常处理这种情况,可以使用以下方法之一。
方法 1:直接订阅
使用此方法,用户控件直接在其自身中订阅容器页面的事件。此方法被认为是一种不良做法。
让我们通过一个例子来看看。
考虑一个包含四个按钮的控件:添加、删除、更新和重置。我们希望这些控件在 ASP.NET 项目的多个屏幕中使用。为了避免所有页面重复和外观一致,让我们创建一个通用控件,如下所示。
ASCX 代码
<div style="width: 100%;">
<asp:Button ID="btnAdd" runat="server" Text="Add"
OnClick="btnAdd_Click"></asp:button>
<asp:button id="btnEdit" runat="server"
text="Edit" onclick="btnEdit_Click"> </asp:button>
<asp:button id="btnDelete" runat="server"
text="Delete" onclick="btnDelete_Click"> </asp:Button>
<asp:button id="btnReset" runat="server"
text="Reset" onclick="btnReset_Click"></asp:button>
</div>
控件的代码隐藏如下。
public partial class Direct : System.Web.UI.UserControl
{
protected void btnAdd_Click(object sender, EventArgs e)
{
}
protected void btnEdit_Click(object sender, EventArgs e)
{
}
protected void btnDelete_Click(object sender, EventArgs e)
{
}
protected void btnReset_Click(object sender, EventArgs e)
{
}
}
现在向项目中添加一个将使用此控件的页面。
ASPX 代码(设计器)
将我们之前创建的用户控件添加到页面。您的设计器代码将如下所示。
<%@ Register src="Controls/Direct.ascx"
tagname="Direct" tagprefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<uc1:Direct ID="Direct1" runat="server" />
</div>
</form>
</body>
</html>
现在,在您的代码隐藏页面中添加四个方法:Add
、Delete
、Update
和 Reset
。您的代码隐藏页面将如下所示。
public partial class Direct : System.Web.UI.Page
{
public void Add()
{
Response.Write("Added.");
}
public void Delete()
{
Response.Write("Deleted.");
}
public void Update()
{
Response.Write("Updated.");
}
public void Reset()
{
Response.Write("Reset done.");
}
}
要单击操作控件按钮来调用上述方法,您需要直接在控件按钮事件中编写逻辑。完成此操作后,您的用户控件代码隐藏将如下所示。
protected void btnAdd_Click(object sender, EventArgs e)
{
//In case of multiple pages you need to check each and every page.
Action.Direct direct = this.Page as Action.Direct;
if (direct != null)
{
direct.Add();
}
}
protected void btnEdit_Click(object sender, EventArgs e)
{
//In case of multiple pages you need to check each and every page.
Action.Direct direct = this.Page as Action.Direct;
if (direct != null)
{
direct.Update();
}
}
protected void btnDelete_Click(object sender, EventArgs e)
{
//In case of multiple pages you need to check each and every page.
Action.Direct direct = this.Page as Action.Direct;
if (direct != null)
{
direct.Delete();
}
}
protected void btnReset_Click(object sender, EventArgs e)
{
//In case of multiple pages you need to check each and every page.
Action.Direct direct = this.Page as Action.Direct;
if (direct != null)
{
direct.Reset();
}
}
缺点
在这种方法中,控件不是通用的;您需要检查每个页面以执行任何操作。如果项目中添加了新页面并希望使用通用控件,则可能需要修改控件以处理新页面。
方法 2:事件委托
在这种情况下,所有希望使用该控件并执行任何操作的页面都基于用户控件引发的事件来执行。为此,用户控件需要发布事件,并且所有消耗页面都需要处理事件。
通常,如果我们想要完全封装并且不想公开我们的方法,我们会采用这种方法。这是解决我们当前问题的最推荐的方法。
让我们考虑与方法 1 相同的示例,并尝试使用事件委托来实现。
ASCX 代码
<div style="width: 100%;">
<asp:Button ID="btnAdd" runat="server"
Text="Add" OnClick="btnAdd_Click"></asp:button>
<asp:button id="btnEdit" runat="server"
text="Edit" onclick="btnEdit_Click"> </asp:button>
<asp:button id="btnDelete" runat="server"
text="Delete" onclick="btnDelete_Click"> </asp:Button>
<asp:button id="btnReset" runat="server"
text="Reset" onclick="btnReset_Click"></asp:button>
</div>
控件的代码隐藏如下。
public delegate void ActionClick();
public partial class EventDelegation : System.Web.UI.UserControl
{
public event ActionClick OnAddClick;
public event ActionClick OnDeleteClick;
public event ActionClick OnEditClick;
public event ActionClick OnResetClick;
protected void btnAdd_Click(object sender, EventArgs e)
{
if(OnAddClick!= null)
{
OnAddClick();
}
}
protected void btnEdit_Click(object sender, EventArgs e)
{
if (OnEditClick != null)
{
OnEditClick();
}
}
protected void btnDelete_Click(object sender, EventArgs e)
{
if(OnDeleteClick!= null)
{
OnDeleteClick();
}
}
protected void btnReset_Click(object sender, EventArgs e)
{
if(OnResetClick!= null)
{
OnResetClick();
}
}
}
上面的代码与方法 1 的代码大相径庭。
让我解释一下要点。用户控件指定一些公共事件,如 OnAddClick
、OnEditClick
等,这些事件声明了一个委托。任何想使用这些事件的人都需要添加 EventHandler 以在相应的按钮单击事件发生时执行。
让我们看以下代码。
protected void btnAdd_Click(object sender, EventArgs e)
{
if(OnAddClick!= null)
{
OnAddClick();
}
}
在上面的代码片段中,我们首先检查是否有人订阅了该事件,如果找到,则引发该事件。
现在向项目中添加一个将使用此控件的页面。
ASPX 代码(设计器)
将我们之前创建的用户控件添加到页面。您的设计器代码将如下所示。
<%@ Register src="Controls/EventDelegation.ascx"
tagname="EventDelegation" tagprefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<uc1:Direct ID="Direct1" runat="server" />
</div>
</form>
</body>
</html>
页面的代码隐藏将如下所示。
public partial class EventDelegation : System.Web.UI.Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
ActionControl.OnAddClick += ActionControl_OnAddClick;
ActionControl.OnDeleteClick += ActionControl_OnDeleteClick;
ActionControl.OnEditClick += ActionControl_OnEditClick;
ActionControl.OnResetClick += ActionControl_OnResetClick;
}
private void ActionControl_OnResetClick()
{
Response.Write("Reset done.");
}
private void ActionControl_OnEditClick()
{
Response.Write("Updated.");
}
private void ActionControl_OnDeleteClick()
{
Response.Write("Deleted.");
}
private void ActionControl_OnAddClick()
{
Response.Write("Added.");
}
}
在上面的代码中,您可以发现容器页面需要在 OnInit
事件期间添加事件处理程序。
我没有发现此方法有任何缺点,但有几件事使其有些麻烦。
- 您需要为每个事件添加一个事件处理程序。如果您不在页面的
OnInit
事件中添加事件处理程序,您可能会遇到一些问题,即在页面回发时,您会丢失事件分配(因为 ASP.NET 是无状态的,而 Windows 控件不是)。在此方法中,您需要遵守页面生命周期事件。 - 有时,当您在设计器上工作时,可能会出现事件处理程序在您不知情的情况下丢失的情况。
- 即使您没有添加事件处理程序,也不会收到任何错误或警告。
- 如果您有多个页面执行相同的操作,则不能保证所有方法名称都相同;开发人员可以选择自己的方法名称,这会降低代码的可维护性。
方法 3:间接订阅
在这里,我们讨论一种直接订阅的扩展方法,该方法试图解决我们实现中的问题。直接订阅方法的主要问题在于维护;每次我们将一个想要使用通用控件的页面添加到项目中时,都需要修改通用控件以处理页面事件。这是因为我们在控件中直接引用容器;每当容器发生更改时,都需要以不同的方式处理。
让我们使用代码讨论间接订阅方法。这里我们考虑与方法 1 和方法 2 讨论的相同示例。
IAction 接口
public interface IAction
{
void Add();
void Update();
void Delete();
void Reset()
}
在上面的代码中,我们创建了一个接口 IAction
,我们在其中定义了我们的用户控件将支持的规则。换句话说,我们在这里定义合同而不是实际实现。这将有助于我们将用户控件与容器(Page
)解耦。
ASCX 代码
<div style="width: 100%;">
<asp:Button ID="btnAdd" runat="server"
Text="Add" OnClick="btnAdd_Click"></asp:button>
<asp:button id="btnEdit" runat="server"
text="Edit" onclick="btnEdit_Click"> </asp:button>
<asp:button id="btnDelete" runat="server"
text="Delete" onclick="btnDelete_Click"> </asp:Button>
<asp:button id="btnReset" runat="server"
text="Reset" onclick="btnReset_Click"></asp:button>
</div>
控件的代码隐藏如下。
public partial class ActionBar : System.Web.UI.UserControl
{
protected void btnAdd_Click(object sender, EventArgs e)
{
Action.Add();
}
protected void btnEdit_Click(object sender, EventArgs e)
{
Action.Update();
}
protected void btnDelete_Click(object sender, EventArgs e)
{
Action.Delete();
}
protected void btnReset_Click(object sender, EventArgs e)
{
Action.Reset();
}
private IAction Action
{
get
{
IAction action = Page as IAction;
// Check if Page implements IAction interface or not.
if (action == null)
{
throw new Exception(
"No A valid Container type. Page should " +
"implement the IAction interface."
);
}
return action;
}
}
}
在上面的代码中,属性 Action
返回一个 IAction
类型对象;如果容器不实现 IAction
接口,则会引发异常。
在不检查是否有人订阅事件处理程序的情况下,方法会被引发。
例如
Action.Add();
现在添加一个将使用用户控件的页面,并将用户控件添加到其中。页面设计器代码将如下所示。
<%@ Register src="Controls/ActionBar.ascx" tagname="ActionBar" tagprefix="uc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<uc1:ActionBar ID="ActionBar1" runat="server" />
</div>
</form>
</body>
</html>
服务器端代码将如下所示。
public partial class _Default : System.Web.UI.Page, IAction
{
#region IAction Members
public void Add()
{
Response.Write("Added.");
}
public void Update()
{
Response.Write("Updated.");
}
public void Delete()
{
Response.Write("Deleted.");
}
public void Reset()
{
Response.Write("Reset done.");
}
#endregion
}
在上面的代码中,您可以看到我们实现了 IAction
接口,这迫使我们在其中实现 Add
、Update
、Delete
和 Reset
方法。您可以实现所有方法并在这些块中编写特定于页面的代码。请注意,这里我们不关心注册事件处理程序。由用户控件本身负责在适当的事件(按钮单击)上调用方法。
优点
- 您无需在页面中注册事件处理程序。
- 所有将使用此控件的页面都应该具有相同的方法名称;这将有助于代码统一并提高可维护性。
- 对于任何新的订阅,您都无需修改用户控件。
但是从面向对象的编程角度来看,这种方法有一个主要缺陷,那就是它不允许封装。由于我们实现了 IAction
接口,因此所有方法默认都是公共的。如果您正在设计一个控件库供第三方使用,那么请选择第二种方法。
取消订阅事件
当我们处理事件时,取消订阅事件起着重要作用。在某些情况下,如果我们不再需要我们的事件,我们需要取消订阅已附加的事件。
您需要根据您正在处理的应用程序类型来处理这个问题。在 ASP.NET 应用程序的情况下,我们不需要取消订阅事件,因为页面实例及其所有相关组件将在特定请求完成后超出范围。请求得到处理后,它们就有资格进行垃圾回收。因此,在我们的情况下,页面中附加的事件将随着页面/用户控件一起超出范围。在正常情况下,我们不需要为 Web 页面取消订阅事件,但您需要取消订阅属于某种单例对象的事件,该对象在每次请求时都保持活动状态。如果您不取消订阅事件,事件的引用将保留在页面中,即使在请求完成后,它也不会被垃圾回收器收集。这可能导致潜在的内存泄漏,在最坏的情况下,您的网站可能会因内存不足而崩溃。
取消订阅事件非常简单明了;您可以按照此处所示的方式实现。
ActionControl.OnAddClick -= ActionControl_OnAddClick;
还有一种使用匿名方法订阅事件的方法。为此,我们需要赋值运算符(+=)将匿名方法附加到事件。以下示例展示了如何使用匿名方法订阅事件。
ActionControl.OnAddClick +=
delegate
{
//Do the add operation here.
};
对于匿名方法,我们无法轻易取消订阅事件。要取消订阅,我们需要将匿名方法存储在委托变量中,然后我们需要将委托添加到事件中。我在这里建议,如果需要取消订阅事件,那么就不要使用匿名方法。
如果您正在处理 Windows 应用程序,事件取消订阅非常重要。如果我们不正确地取消订阅事件,可能会面临资源泄漏。在处理订阅对象之前,取消订阅事件非常必要。如果您不取消订阅事件,对于那些被某些发布对象(如 Windows 中的 MDI)引用的事件,如果它们持有对订阅对象(如 Windows 窗体)的引用,它们将不会被垃圾收集器收集。
弱引用
到目前为止,我们讨论的是引用。所有这些引用都称为“强引用”。这意味着只要引用存在,GC 就不会收集对象,即使在处理之后。弱引用是一种特殊的引用类型,即使存在对对象的引用,GC 也可以收集该对象。
有关弱引用的更多详细信息,您可以查看以下文章。