Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Server Control - Design Time Support

0.00/5 (No votes)
9 Feb 2005 1  
A tutorial on adding design time support to ASP.NET custom server control.

Sample Image - DesignTimeSupport.jpg

Updates - see at the article's end

Introduction

If you already have created a custom server control, you would have probably noticed that the intelisense in the HTML view is absent.

This article will show you how to get it back.

Softomatix published an article about how to create server control: ASP.NET Server Control - Updown Control 1. That's the basics. I'll go on to other topics.

My code example is ClickOnce a control that becomes disabled, until postback returns.

IteliSense support

Add your code Identity to the control TagPrefix.

 [assembly: System.Web.UI.TagPrefix("Gootvilig.Controls","Gootvilig")]

Now when you drop your control, we will have a friendly name in the TagPrefix attribute and not something like TagPrefix="cc1".

<%@ Register TagPrefix="gootvilig" Namespace="Gootvilig.Controls" 
                       Assembly="Gootvilig.Controls" %> 

The Intelisense for web controls is in a file named "asp.xsd", that can be found in the path "Program Files\Microsoft Visual Studio .NET 2003\Common7\Packages\schemas\xml". This is the schema for any controls prefixed with "asp:", and their attributes.

Furthermore, the ability to display data in the Properties Window is also dependent on this schema.

So how can do we add our schema?

  • Add your own XSD file.
  • Copy the file to the "Asp.xsd" folder.
  • Add namespace to the page BODY tag:
<body MS_POSITIONING="GridLayout" xmlns:Gootvilig=
                  "urn:http://www.Gootvilig.com/schemas">

Now we will get intlisense support (And also Properties Window support).

The next code snippet is the base of the schema for my control.

<?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" target=_blank>http://www.w3.org/2001/XMLSchema" 
xmlns:vs="http://schemas.microsoft.com/Visual-Studio-Intellisense" target=_blank>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> 

Now we have a problem. The reference to "ButtonDef" is located in "asp.xsd" file with a different namespace from the one I chose for my control. So we have to copy some lines from "asp.xsd" file. The whole schema is in the code download.

Now there is complete intelisense for the control.

Full Intelisense

Support for the Toolbox

Adding your component to the toolbar is simple. Just right click on the desired toolbox tab, choose "Add/Remove Items�", and browse to your component's assembly DLL. The component name will appear there with the following default icon: .

To add your Icon to the toolbox, add this attribute to your class:

ToolboxBitmap(typeof(ClickOnce),"Gootvilig.Controls.ClickOnce.bmp")
// or

ToolboxBitmap(typeof(Bottun)) 

As you can see, there are two choices. The first choice uses an embedded BMP file and associates it to the class. The file must be 16 X 16 pixels. "Build Action" of the file must be "Embedded Resource".

The second choice associates your class with a built in class icon.

Now when you drag and drop ClickOnce to our page, lines will be added to our page: (First we have to add a reference to the assembly)

<%@ Register TagPrefix="gootvilig" Namespace="Gootvilig.Controls"
                                       Assembly="Gootvilig.Controls" %>

<Gootvilig:ClickOnce id="ClickOnce1" runat="server" 
                               Text="Button"></Gootvilig:ClickOnce>

The ClickOnce control

The control is designed to support long term round trip actions. In that case we want to avoid any possibility of re-clicking on a button.

To achieve this action we need to add some JavaScript code, for the client to execute it before the long time server callback code will take place.

Be sure not to replace the "onclick" event, but add your code to the event code chain.

Here is the code for implementing this:

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);
}

Now we have an additional problem: Because the control was set to "disabled = true", the server callback method doesn�t fire.

So I added a call to JavaScript method that is generally added to most ASP.NET pages - __doPostBack()

 
Attributes["onclick"] 
   += string.Format("document.getElementById('{0}').disabled 
          = true;__doPostBack('{0}','');",ClientID);

Here is the code that the Page added to support postback operations:

<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>

But the page doesn't always add these snippet code. Only if one of the following controls is embedded in the page, it will call the page internal RegisterPostBackScript() method to accomplish that. The controls that do that are: HtmlAnchor, HtmlButton, HtmlInputButton, HtmlImage, Calendar, CheckBox, LinkButton, ListButton, ListControl and TextBox. Those controls call RegisterPostBackScript() depend on some conditions.

Here is the code of Page.RegisterPostBackScript(). The flag _fRequirePostBackScript is for inserting the script later on.

internal void RegisterPostBackScript() 
{ 
    if (this._fPostBackScriptRendered);
    { 
        return; 
    } 
     if (!this._fRequirePostBackScript) 
    { 
         this.RegisterHiddenField("__EVENTTARGET", ""); 

         this.RegisterHiddenField("__EVENTARGUMENT", "");    
    }
   this._fRequirePostBackScript = true; 
} 

Here is the code to enforce the page to call RegisterPostBackScript(), using reflection:

//Call the internal method Page.RegisterPostBackScript()

MethodInfo  methodInfo = 
typeof(Page).GetMethod("RegisterPostBackScript",
          BindingFlags.Instance|BindingFlags.NonPublic);

if(methodInfo != null)
{
      methodInfo.Invoke(Page,new object[]{});
}

The ClientID, as the name indicates, is good only for client script code. When calling the __doPostBack(), in case our button is located on User Control, we need UniqueIDWithDollars name, which is also an internal method. Our good old friend, reflection, will help here as well:

//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;
     }
 }
 

Here is the code to call UniqueIDWithDollars by reflection:

//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[]{});
}

Wait! How will the ClickOnce control behave now? It should be enabled after the long server run:

So we set Enabled to true in the OnInit method.

protected override void OnInit(EventArgs e)
{
      base.OnInit(e);      
      
      //Enable upon PostBack

      Enabled = true;
}

After OnInit, ViewState will take place, so users of our ClickOnce Button, can change it's state to disabled. The demo code illustrates that.

See also Eric Plowe's ClickOnce Button Server Control.

Conclusions

What did we have here :

  • Adding design time ability to our custom web control.
  • Adding Toolbox Icon support.
  • How to enforce a button to have a ClickOnce behavior.

Updates

  • 09-02-2005

InteliSence in User Control

We saw that, we need to copy the XSD that support our control schema to the "Asp.xsd" folder, then add our namespace to the page BODY tag.

But what if we are working with a User Control, where we have no BODY tag?

We can do that by enclosing all the HTML code in a tag and add the namespace to that tag.For example:

<DIV xmlns:Gootvilig="urn:http://www.Gootvilig.com/schemas">
<!-- Here is all the User Control code -->
</DIV>

Or better in a way that will not rendered to the actual HTML of the client side:

<% if(false) { %>
<DIV xmlns:Gootvilig="urn:http://www.Gootvilig.com/schemas">
<%}%>
 
<!-- Here is all the User Control code -->
<% if(false) { %>
</DIV>
<%}%>

No need to copy the XSD file

If we have the XSD in the solution which we are working on, we don't have to copy that file to the "Asp.xsd" folder!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here