ASP.NET 服务器控件 - 设计时支持






4.41/5 (11投票s)
一篇关于如何为 ASP.NET 自定义服务器控件添加设计时支持的教程。

更新 - 参见文章末尾
引言
如果您已经创建了一个自定义服务器控件,您可能会注意到 HTML 视图中的智能感知(intellisense)功能缺失。
本文将向您展示如何恢复它。
Softomatix 发表了一篇关于如何创建服务器控件的文章:ASP.NET 服务器控件 -UpDown 控件 1。那是基础。我将继续讨论其他主题。
我的代码示例是一个名为 ClickOnce 的控件,它在回发(postback)返回之前是禁用的。
IntelliSense 支持
将您的代码标识添加到控件的 TagPrefix。
 [assembly: System.Web.UI.TagPrefix("Gootvilig.Controls","Gootvilig")]
现在,当您放置您的控件时,TagPrefix 属性将显示一个友好的名称,而不是像 TagPrefix="cc1" 这样的内容。
<%@ Register TagPrefix="gootvilig" Namespace="Gootvilig.Controls" 
                       Assembly="Gootvilig.Controls" %> 
Web 控件的 IntelliSense 位于一个名为 "asp.xsd" 的文件中,该文件位于 "Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml" 路径下。这是任何以 "asp:" 为前缀的控件及其属性的模式。
此外,在属性窗口中显示数据的能力也依赖于此模式。

那么,我们如何添加我们自己的模式呢?
- 添加您自己的 XSD 文件。
- 将文件复制到 "Asp.xsd" 文件夹。
- 在页面的 BODY标签中添加命名空间
<body MS_POSITIONING="GridLayout" xmlns:Gootvilig=
                  "urn:http://www.Gootvilig.com/schemas">
现在我们将获得 IntelliSense 支持(以及属性窗口支持)。

下一个代码片段是我控件模式的基础。
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema targetNamespace="urn:http://www.Gootvilig.com/schemas" 
elementFormDefault="qualified"
xmlns="urn:http://www.Gootvilig.com/schemas" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:vs="http://schemas.microsoft.com/Visual-Studio-Intellisense" 
vs:friendlyname="Gootvilig Control Schema" 
vs:ishtmlschema="false" 
vs:iscasesensitive="false" 
vs:requireattributequotes="true">
 
<xsd:annotation> 
<xsd:documentation>Gootvilig Control schema.</xsd:documentation> 
</xsd:annotation> 
<xsd:element name="ClickOnce" type="ClickOnceDef"></xsd:element> 
<xsd:complexType name="ClickOnceDef" vs:noambientcontentmodel="true"> 
<xsd:attributeGroup ref="ButtonDef" /> 
</xsd:complexType> 
</xsd:schema> 
现在我们遇到了一个问题。对 "ButtonDef" 的引用位于 "asp.xsd" 文件中,但命名空间与我为我的控件选择的命名空间不同。所以我们必须从 "asp.xsd" 文件中复制一些行。完整的模式在代码下载中。
现在控件已经具备完整的 IntelliSense 功能了。

工具箱支持
将您的组件添加到工具栏很简单。只需右键单击所需的工具箱选项卡,选择 "添加/删除项…",然后浏览到您的组件的程序集 DLL。组件名称将出现在那里,并带有以下默认图标: 。
。
要为工具箱添加图标,请将此属性添加到您的类中
ToolboxBitmap(typeof(ClickOnce),"Gootvilig.Controls.ClickOnce.bmp")
// or
ToolboxBitmap(typeof(Bottun)) 
正如您所见,有两种选择。第一种选择使用嵌入的 BMP 文件并将其与类关联。该文件必须是 16 x 16 像素。"Build Action" 必须是 "Embedded Resource"。
第二种选择是将您的类与内置的类图标关联。

现在,当我们把 ClickOnce 控件拖放到我们的页面上时,我们的页面将添加一些行:(首先,我们必须添加对程序集的引用)
<%@ Register TagPrefix="gootvilig" Namespace="Gootvilig.Controls"
                                       Assembly="Gootvilig.Controls" %>
<Gootvilig:ClickOnce id="ClickOnce1" runat="server" 
                               Text="Button"></Gootvilig:ClickOnce>
ClickOnce 控件
该控件旨在支持长期的往返操作。在这种情况下,我们希望避免任何重复单击按钮的可能性。
为了实现此操作,我们需要添加一些 JavaScript 代码,供客户端在长时间服务器回调代码执行之前执行。
请确保不要替换 "onclick" 事件,而是将您的代码添加到事件代码链中。
以下是实现此功能的代码
protected override void OnPreRender(EventArgs e)
{
      base.OnPreRender(e);
      string currentOnClick = Attributes["onclick"];
 
      // Add javascript onclick handler
      Attributes["onclick"] 
        += string.Format("document.getElementById(
                     '{0}').disabled = true;",ClientID);
}
现在我们遇到了另一个问题:因为控件被设置为 "disabled = true",所以服务器回调方法不会触发。
因此,我添加了一个对 JavaScript 方法的调用,该方法通常添加到大多数 ASP.NET 页面中 - __doPostBack()
 
Attributes["onclick"] 
   += string.Format("document.getElementById('{0}').disabled 
          = true;__doPostBack('{0}','');",ClientID);
以下是 Page 添加的代码,用于支持回发操作
<input type="hidden" name="__EVENTTARGET" value="" />
<input type="hidden" name="__EVENTARGUMENT" value="" /> <script language="javascript" type="text/javascript">
<!--
function __doPostBack(eventTarget, eventArgument) {
var theform;            
if (window.navigator.appName.toLowerCase().indexOf("microsoft") > -1) {
theform = document.Form1;           }           
else {                  
    theform = document.forms["Form1"];        }    
    theform.__EVENTTARGET.value = eventTarget.split("$").join(":"); 
    theform.__EVENTARGUMENT.value = eventArgument;        
    theform.submit(); }// -->
</script>
但页面并不总是添加这些代码片段。只有当页面中嵌入了以下控件之一时,它才会调用页面内部的 RegisterPostBackScript() 方法来完成此操作。这些控件包括:HtmlAnchor, HtmlButton, HtmlInputButton, HtmlImage, Calendar, CheckBox, LinkButton, ListButton, ListControl 和 TextBox。这些控件调用 RegisterPostBackScript() 取决于某些条件。
以下是 Page.RegisterPostBackScript() 的代码。标志 _fRequirePostBackScript 用于稍后插入脚本。
internal void RegisterPostBackScript() 
{ 
    if (this._fPostBackScriptRendered);
    { 
        return; 
    } 
     if (!this._fRequirePostBackScript) 
    { 
         this.RegisterHiddenField("__EVENTTARGET", ""); 
         this.RegisterHiddenField("__EVENTARGUMENT", "");    
    }
   this._fRequirePostBackScript = true; 
} 
这是使用反射强制页面调用 RegisterPostBackScript() 的代码
//Call the internal method Page.RegisterPostBackScript()
MethodInfo  methodInfo = 
typeof(Page).GetMethod("RegisterPostBackScript",
          BindingFlags.Instance|BindingFlags.NonPublic);
if(methodInfo != null)
{
      methodInfo.Invoke(Page,new object[]{});
}
ClientID,顾名思义,仅适用于客户端脚本代码。调用 __doPostBack() 时,如果我们的按钮位于用户控件中,我们需要 UniqueIDWithDollars 名称,这也是一个内部方法。我们老朋友反射也会在这里有所帮助
//Code from Control 
internal string UniqueIDWithDollars{
      get
      {
         string text1 = this.UniqueID;
         if (text1 == null)
         {
             return null;
         }
         if (text1.IndexOf(':') >= 0)
         {
            return text1.Replace(':', '$');          
         }
         return text1;
     }
 }
 
以下是通过反射调用 UniqueIDWithDollars 的代码
//Get the Control UniqueIDWithDollars for the call to __doPostBack()
PropertyInfo  propertyInfo = typeof(Control).GetProperty(
         "UniqueIDWithDollars",BindingFlags.Instance|BindingFlags.NonPublic);
string uniqueIDWithDollars = ClientID;
if(propertyInfo != null)
{
    uniqueIDWithDollars = (string)propertyInfo.GetValue(this,new object[]{});
}
等等!ClickOnce 控件现在将如何表现?它应该在服务器长时间运行后启用。
因此,我们在 OnInit 方法中将 Enabled 设置为 true。
protected override void OnInit(EventArgs e)
{
      base.OnInit(e);      
      
      //Enable upon PostBack
      Enabled = true;
}
在 OnInit 之后,ViewState 将发挥作用,因此我们的 ClickOnce 按钮的用户可以将其状态更改为禁用。演示代码说明了这一点。
另请参阅 Eric Plowe 的 ClickOnce 按钮服务器控件。
结论
我们在这里学到了什么
- 为我们的自定义 Web 控件添加设计时功能。
- 添加工具箱图标支持。
- 如何强制一个按钮具有 ClickOnce 行为。
更新
- 09-02-2005
用户控件中的 IntelliSense
我们看到,我们需要将支持我们控件模式的 XSD 复制到 "Asp.xsd" 文件夹,然后将我们的命名空间添加到页面的 BODY 标签中。
但是如果我们处理的是一个用户控件,其中没有 BODY 标签呢?
我们可以通过将所有 HTML 代码包含在一个标签中来做到这一点,并向该标签添加命名空间。例如
<DIV xmlns:Gootvilig="urn:http://www.Gootvilig.com/schemas">
<!-- Here is all the User Control code -->
</DIV>
或者更好地,以一种不会呈现给客户端实际 HTML 的方式
<% if(false) { %>
<DIV xmlns:Gootvilig="urn:http://www.Gootvilig.com/schemas">
<%}%>
 
<!-- Here is all the User Control code -->
<% if(false) { %>
</DIV>
<%}%>
无需复制 XSD 文件
如果 XSD 存在于我们正在处理的解决方案中,我们就不必将该文件复制到 "Asp.xsd" 文件夹中!


