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.
="1.0" ="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.
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")
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"];
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:
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:
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:
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);
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
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">
-->
</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">
<%}%>
-->
<% 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!