Dynamically Adding Web Controls and adding event delegates

For the complete thread that led to this post, see the entire thread in the XML forum on ASP.NET.

I have played around with the Page.ParseControls method to add dynamically created controls to the page processing model before, but only really playing around. I never tried to fire a server-side event for a dynamically created control. I really haven't found a great use for this technique, but I guess others have as I see a lot of posts on it in newsgroups.  

I thought that adding server events should be simple enough: just add the OnServerClick attribute to the HTML, and you should be golden:

System.Web.UI.Control button = Page.ParseControl("<input id=\"IDAHFUX\" name=\"IDAHFUX\" value=\"Click me!\" style=\"Z-INDEX:102;POSITION: absolute;TOP: 40px;LEFT: 10px\" type=\"button\" runat=\"server\"> OnServerClick=\"Button_Click\"");
Page.FindControl("WebForm1").Controls.Add(button);

What I found was that the OnServerClick method is not used to generate the postback handling, instead it is rendered to the client. The client has no idea what "onserverclick" is, so nothing happens and the server-side Button_Click event never fires. So, you have to set the event delegate yourself. That should be simple enough, so I decided to use XSLT to generate the controls.

The XML document I used is very simple:

<root>
<foo/>
<foo/>
<foo/>
</root>

The XSLT to generate the controls is also pretty simple. The only 2 weird things might be the use of attribute value templates (AVT's), noted by the "{ }" and the generate-id function. I could have used a bunch of xsl:attribute tags to create the attributes, but AVT's are quite a bit shorter. The generate-id function is used to create a unique ID for each control (a requirement of ASP.NET), and 2 successive calls to generate-id for the same node context will return the same value. That means the value in the ID and NAME attributes will be identical for each distinct node in the result tree. I also used the node's position to manipulate both the TOP and Z-INDEX attributes of the control.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="https://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" />

 <xsl:template match="/root/foo">
<input id="{generate-id()}" name="{generate-id()}" value="Click me!" style="Z-INDEX:{101 + position()};POSITION: absolute;TOP: {position() * 40}px;LEFT: 10px" type="button" OnServerClick="Button_Click" runat="server"/>
</xsl:template>
</xsl:stylesheet>

There are a couple gotchas with the code. The first is using a foreach to enumerate over the parsedControl.Controls collection. If you replace the for loop with a foreach loop, you will receive an error that the collection has changed. This is because we are setting an eventhandler for items contained in the collection, causing the error. That explains the for loop instead of a foreach enumerator. The second non-obvious thing in the code is that you might try to optimize the page processing by checking IsPostBack. However, the controls will not be automatically added into the Page's Controls collection so the loop has to be performed each time the page is loaded.

<%@ Page language="c#"%>
<%@Import namespace="System.Xml"%>
<%@Import namespace="System.Xml.XPath"%>
<%@Import namespace="System.Xml.Xsl"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>WebForm1</title>
</HEAD>

<script language=C# runat=server>
private void Page_Load(object sender, System.EventArgs e)
{
XPathDocument doc = new XPathDocument(Server.MapPath("xmlfile1.xml"));
XslTransform trans = new XslTransform();
trans.Load(Server.MapPath("xsltfile1.xslt"));

    System.IO.StringWriter writer = new System.IO.StringWriter();
trans.Transform(doc,null,writer);
writer.Flush();
System.Text.StringBuilder sb = writer.GetStringBuilder();

    System.Web.UI.Control parsedControl = Page.ParseControl(sb.ToString());
HtmlForm form = (HtmlForm)Page.FindControl("WebForm1");
for (int i=0;i< parsedControl.Controls.Count;i++)
{
HtmlInputButton button = parsedControl.Controls[i] as HtmlInputButton;
if(null != button)
{
button.ServerClick += new System.EventHandler(this.Button_Click);
form.Controls.Add(button);
}
}
}

public void Button_Click(object sender, System.EventArgs e)
{
this.Label1.Text = "Button clicked at " + System.DateTime.Now.ToLongTimeString();
}
</script>

<body MS_POSITIONING="GridLayout">
<form id="WebForm1" method="post" runat="server">
<asp:Label id="Label1" style="Z-INDEX: 101; LEFT: 444px; POSITION: absolute; TOP: 77px" runat="server" Width="497px">Label</asp:Label>
</form>
</body>
</HTML>

By the way - I could have used code-behind for the Page_Load and Button_Click events, there is nothing special about this syntax. I chose this format for brevity.

<Kirk/>