Action 标签
一种将功能封装在服务器控件中,与用户界面分离的方法。
引言
本文介绍了一种开发服务器控件库的方法,这里称为“动作标签”,它将动作的功能与其用户界面分离开来。标签本身是继承自System.Web.UI.Control
类的对象,并带有AttachTo
属性,指示关联的用户界面控件。例如,执行查询的代码可以被封装为一个<Query>
动作标签,然后附加到一个DataGrid
或DropDownList
控件上。或者,可以将会从Click
事件触发的代码封装在一个标签中,然后附加到一个Button
、LinkButton
或ImageButton
上。这是一种与通过子类化用户界面控件来添加功能不同的方法,并且可以提供一种将封装的代码与触发事件链接的一致方式。
将描述一个基类ActionTagControl
,它定义了这些动作标签将如何工作。System.Reflection
命名空间中的对象用于检查用户界面控件的事件和属性信息。本文还将展示三个工作示例,演示继承类,用于创建<ShowAndHideAction>
标签、<Selector>
标签和<Query>
标签。
背景
在某种程度上,这种方法是为HTML脚本编写者构思的。几年前,我有幸将我刚加入的部门从ColdFusion迁移到ASP,然后迁移到ASP.NET来开发Web应用程序。在我短暂接触ColdFusion的经历中,我体会到了它对脚本编写者的吸引力,它使用了一种与HTML一致的标签方法来提供功能。非程序员可以使用该平台完成很多事情。作为一名程序员,我更喜欢ASP.NET提供的强大功能和灵活性,但我有一个问题:ASP.NET能否像ColdFusion那样,也对非程序员有用?
以将查询结果绑定到DataGrid
或DropDownList
控件的动作为例。虽然这是一个简单的操作,代码量不大,但仍然需要一些编程代码。除了几行代码,处理连接和数据集类以及页面事件仍然需要比HTML脚本编写者更适合程序员的概念理解。我能理解ColdFusion平台的吸引力,因为它提供了一个<Query>
标签来处理这些细节,让脚本编写者无需关心。如果程序员开发类似的标签库,ASP.NET应该是一个适合非程序员的平台。本文提供了一种这样的方法。
基类 - ActionTagControl
本库中的所有动作标签都将基于抽象类ActionTagControl
。基类提供了一种将动作分配给控件的一致方法。由于动作标签本身不提供用户界面,因此基类继承自System.Web.UI.Control
而不是WebControl
。触发控件的ID——也就是说,触发标签所代表的动作的控件——被实现为一个字符串属性AttachTo
。
protected string _attachTo = "";
// AttachTo property - the id of the
// control whose event will trigger this action
public string AttachTo {
get {return _attachTo;}
set {_attachTo = value;}
}
ActionTagControl
继承类的构造函数通常会设置触发事件;如果没有指定,基类将在其自身的构造函数中将Click
事件设置为默认值。
// constructor
public ActionTagControl() : base() {
// default to the Click event; inheritors can change this
// through their own constructors
_triggerEvent = "Click";
}
基类还定义了一个TriggerEvent
公共属性,以便标签用户可以在适当的情况下覆盖该事件。
protected string _triggerEvent;
// TriggerEvent property - the event which will trigger this action;
// typically setting this would not be necessary as inheriting classes
// will establish the default _triggerEvent in their constructors
public string TriggerEvent {
get {return _triggerEvent;}
set {_triggerEvent = value;}
}
基类中定义了一个抽象方法:EventAction
。EventAction
方法是当在附加控件上触发事件时将执行的代码。该方法与System.EventHandler
委托具有相同的签名。
// inheriting ActionTags must supply an EventAction method
protected abstract void EventAction(Object o, EventArgs e) ;
基类中实际执行的工作及其有用性来自于它对Control.OnInit
事件的重写。在这里,会检查并定位AttachTo
控件ID。如果找不到这样的控件,将抛出异常。如果控件找到但缺少适当的触发事件,也将抛出异常。OnInit
函数的第一部分处理这些情况。
protected override void OnInit(EventArgs e) {
// is an AttachTo attribute specified?
if (_attachTo.Length == 0) {
string msg = "The 'AttachTo' attribute "
"is required. Specify a control"
" in the AttachTo attribute.";
throw new Exception(msg);
}
// attempt to locate the specified control
Control c = LocateControl(_attachTo);
if (c == null) {
string msg = string.Format("A control with" +
" id='{0}' could not be found. Specify " +
"a control in the AttachTo attribute.", _attachTo);
throw new Exception(msg);
}
OnInit
事件中使用的LocateControl
方法如下所示:
// look for the control identified by sID
protected Control LocateControl(String sID) {
Control c;
// look in this control's naming container first
c = this.FindControl(sID);
// if not found, look in the naming
// container of the parent of this control
if (c == null && this.Parent != null) {
c = this.Parent.FindControl(sID);
}
// if still not found then look in
// the naming container for the Page itself
if (c == null && this.Page != null) {
c = this.Page.FindControl(sID);
}
// at this point we either found the
// control or c is null; return it in either case
return c;
}
我决定编写这个LocateControl
函数,而不是直接使用Control
类的FindControl
方法,是为了允许在动作标签在网页上的放置方式上具有更大的灵活性。FindControl
会在调用它的控件的同一命名容器中按ID定位控件。如果页面上存在所需的控件,但它位于动作标签的命名容器之外,FindControl
将无法识别它。LocateControl
也定义为protected
而不是private
,以便继承类可以使用它。
回到OnInit
方法——其接下来的三行代码演示了如何使用System.Reflection
类来获取一个表示触发事件本身的对象的实例。
// attempt to get the attached control's
// triggering event through reflection
System.Type t = c.GetType();
BindingFlags bf = BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic;
EventInfo ei = t.GetEvent(_triggerEvent, bf);
这三行代码值得进一步解释。BindingFlags
对象允许设置影响反射搜索成员方式的选项。一旦控件的类型作为System.Type
对象获得,其GetEvent
方法就会用于获取一个System.Reflection.EventInfo
对象。EventInfo
对象提供了对事件元数据的访问,并公开了AddEventHandler
方法。通过使用AddEventHandler
,ActionTagControl
的EventAction
代码就与触发事件关联起来了。
// attempt to attach our EventAction to the appropriate control's event
if(ei != null) {
ei.AddEventHandler(c, new System.EventHandler(this.EventAction));
}
else {
string msg = string.Format("The control '{0}' " +
"does not have the {1} event. " +
"Either specifiy a control with the " +
"{1} event in the 'AttachTo' attribute, " +
"or specify a different event in " +
"the 'TriggerEvent' attribute.",
_attachTo, _triggerEvent);
throw new Exception(msg);
}
此时,我们有了一个基类,它实现了动作标签与用户界面控件触发事件的关联。继承类现在只需要重写EventAction
方法,就可以利用这种关联。
这是一个简单的但完整的动作标签示例,它继承自ActionTagControl
,设计用于通过Click
事件触发。由于Click
已经在基类中设置为默认触发事件,因此子类不需要在其自己的构造函数中重写它。所有需要做的就是实现EventAction
方法。
public class SimpleAction : ActionTagControl {
static int i;
protected override void EventAction(Object o, EventArgs e) {
i++;
this.Page.Response.Write(i);
}
}
与任何自定义服务器控件一样,该标签可以通过<%@ Register %>
指令在网页上使用。然后将AttachTo
属性赋值给Button
、LinkButton
、ImageButton
或任何其他支持Click
事件的用户界面控件的ID。
<%@ Register TagPrefix="actions"
Namespace="ActionTags" Assembly="ActionTags" %>
<html>
<head><title>SimpleAction</title></head>
<body>
<form runat="server">
<asp:Button id="btnTest" runat="server"
text="Click Me!" />
<actions:SimpleAction id="click1"
runat="server" AttachTo="btnTest" />
</form>
</body>
</html>
继承类示例 1: ShowAndHideAction
一个更实用的基于Click
事件的动作标签示例是ShowAndHideAction
类。它定义了Show
和Hide
属性,这些属性是逗号分隔的字符串,用于列出在触发Click
事件时应该显示或隐藏的控件。这里的EventAction
方法解析这些字符串,并相应地将每个单独控件的Visible
属性设置为true
或false
。
protected override void EventAction(Object o, EventArgs e) {
Control c;
// parse the comma-delimited list of controls to hide
if (_hide != "") {
string[] hides = _hide.Split(',');
for (int i=0; i<hides.Length; i++) {
c = LocateControl(hides[i]);
if (c != null) c.Visible = false;
}
}
// parse the comma-delimited list of controls to show
if (_show != "") {
string[] shows = _show.Split(',');
for (int i=0; i<shows.Length; i++) {
c = LocateControl(shows[i]);
if (c != null) c.Visible = true;
}
}
}
请注意,这个继承的ActionTagControl
使用了在基类中定义的受保护方法LocateControl
。
以下是一个.aspx页面的示例,演示了ShowAndHideAction
附加到LinkButton
控件上。
<%@ Register TagPrefix="actions" Namespace="ActionTags"
Assembly="ActionTags" %>
<html>
<head><title>ShowAndHideAction - Sample 1</title></head>
<body>
<form runat="server">
<asp:LinkButton id="Hide" runat="server"
text="Hide Text Boxes" />
|
<asp:LinkButton id="Show" runat="server"
text="Show Text Boxes" />
<br /><br />
<asp:Textbox id="text1" runat="server" text="text1" />
<asp:Textbox id="text2" runat="server" text="text2" />
<asp:Textbox id="text3" runat="server" text="text3" />
<actions:ShowAndHideAction
id="hide1" runat="server" attachTo="Hide"
hide="text1,text2,text3" />
<actions:ShowAndHideAction
id="show1" runat="server" attachTo="Show"
show="text1,text2,text3" />
</form>
</body>
</html>
使用ShowAndHideAction
标签的一个更复杂的示例在源文件showHide2.aspx中提供。
继承类示例 2: Selector
另一个ActionTagControl
继承类的示例是Selector
类。此操作设计用于由ListControl
的选择触发,而不是按钮点击。列表中的每个项目都与一个Panel
(或其他Control
)相关联;Panel
的可见性取决于它是否在列表中被选中。
SelectedIndexChanged
事件在Selector
类的构造函数中指定,覆盖了基类指定的Click
事件。
// create a constructor to override the _triggerEvent value
public Selector() : base() {
_triggerEvent = "SelectedIndexChanged";
}
为确保所有相关的面板(或控件)都可以在页面上使用,OnInit
方法被重写,其中包含查找每个控件的代码。如果找不到一个,将抛出异常。仍然会调用基类的OnInit
方法,以确保动作标签与所需的ListControl
相关联。
private Control[] _panels;
// we'll override the OnInit method to locate each
// panel (control) and throw an exception
// if we can't find one
// panels (controls) are identified
// through the ListItem.Value property
protected override void OnInit(EventArgs e) {
// call the base method first
base.OnInit(e);
Control c = this.AttachedControl;
if (c != null) {
ListControl lc = (ListControl) c;
_panels = new Control[lc.Items.Count];
for (int i=0; i<lc.Items.Count; i++) {
_panels[i] = LocateControl(lc.Items[i].Value);
if (_panels[i] == null) {
string msg =
string.Format("Selector is attached to {0}," +
" which has a ListItem value of {1}. " +
"A control with id='{1}' is not found.",
c.ID, lc.Items[i].Value);
throw new Exception(msg);
}
}
}
}
然后,此标签的EventAction
简单地遍历附加控件的列表项,相应地设置相关面板的可见性。
protected override void EventAction(Object o, EventArgs e) {
// loop through each of the items in the AttachedControl's item list
// setting the selected panels' visibility to true or false
Control c = this.AttachedControl;
if (c != null) {
ListControl lc = (ListControl) c;
for (int i=0; i<lc.Items.Count; i++) {
// set the panel's visibility
_panels[i].Visible = (lc.Items[i].Selected);
}
}
}
在源文件panelSelector.aspx中提供了一个Selector
动作标签用法的示例。请注意,附加的ListControl
的AutoPostBack
属性应设置为true
才能触发该事件。
继承类示例 3: Query
Query
动作标签作为最后一个示例提供。它的目的是针对OLEDB数据源执行查询,并将结果绑定到任何支持DataSource
属性的用户界面控件。Query
标签继承自ActionTagControl
,并定义了两个属性来支持每种功能:OLEDB连接字符串的标识和SQL语句。其构造函数定义PreRender
作为其触发事件——因此,DataGrid
、Repeater
或其他支持DataSource
的控件的PreRender
事件就是执行SQL语句并绑定结果的地方。
public class Query : ActionTagControl {
private string _connectionString = "";
private string _sql = "";
public string ConnectionString {
get {return _connectionString;}
set {_connectionString = value;}
}
public string Sql {
get {return _sql;}
set {_sql = value;}
}
// create a constructor to override the _triggerAction value
public Query() : base() {
_triggerEvent = "PreRender";
}
// the event action here is to execute
// the query and bind the results
protected override void EventAction(Object o, EventArgs e) {
// get the connection and execute the query
OleDbConnection con = new OleDbConnection(_connectionString);
OleDbDataAdapter da = new OleDbDataAdapter();
da.SelectCommand = new OleDbCommand(_sql, con);
DataSet ds = new DataSet();
da.Fill(ds);
// bind the query results to the AttachedControl
// we'll use reflection here to set the DataSource property;
// an exception occurs if the target control does not have
// a DataSource property.
Type t = this.AttachedControl.GetType();
PropertyInfo pi = t.GetProperty("DataSource");
pi.SetValue(this.AttachedControl, ds, null);
this.AttachedControl.DataBind();
con.Close();
}
}
与基类一样,这里使用System.Reflection
类来探查附加控件是否具有DataSource
属性,然后可以将其值设置为执行查询的结果。在这种情况下,将获取一个PropertyInfo
对象并调用其SetValue
方法。
此Query
动作标签有许多增强的可能性。例如,连接信息可以通过专门的连接文件获取,或者通过web.config文件作为应用程序参数获取。这个例子保持简单,以便专注于反射的元素,同时仍然具有功能性。
此示例.aspx页面演示了使用Query
动作标签将数据从Microsoft Access表填充到DropDownList
控件。
<%@ Register TagPrefix="actions"
Namespace="ActionTags" Assembly="ActionTags" %>
<html>
<head><title>Query Examples</title></head>
<body style="font-family: Tahoma;">
<form>
<asp:DropDownList id="dd1" runat="server"
DataTextField="Field1" DataValueField="Field2" />
</form>
</body>
</html>
<actions:Query id="myQuery" runat="server" attachTo="dd1"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data
Source=c:\inetpub\wwwroot\ActionTags\test.mdb"
sql="SELECT Field1, Field2 FROM Table1"
/>
源文件query.aspx显示了使用DataGrid
的额外示例。
摘要
本文介绍了一种替代子类化用户界面控件以封装可重用功能的方法:开发一个动作标签库。实现的标签基于具有AttachTo
属性的抽象ActionTagControl
类,并使用System.Reflection
命名空间的EventInfo
对象来定义一种将功能与触发事件相关联的一致方法。对于程序员来说,这种方法可以带来不需为相同功能对多个控件(例如Button
、LinkButton
和ImageButton
)进行子类化的便利。开发和分发此类库也可能有助于使ASP.NET成为非程序员更实用的平台选择。