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="http://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/>