ASP.Net Webforms - Dynamic UpdatePanels and UserControls issue
I have come across an interesting issues in ASP.Net WebForms when migrating a project from 3.5 up to 4.5.
The site in question is extremely dynamic, the page is built up based on configuration in a CMS fashion.
However in 4.5 we have a problem - when more content is added into the page via a button click, not all the markup for the content appears.
Setup
In order to demonstrate, the following path will setup a simple site that has the flaw. The steps work pretty much the same in either .net 3.5 or 4.5, and will show the difference between the versions. In my case I’m using the project templates in VS2013 for this, and I’m using the VB versions.
1: Start a new ASP.Net WebForms project (using the default template)
2: In .Net 3.5 add the following markup:
<asp:ScriptManager runat="server"></asp:ScriptManager>
3: In Default.aspx add the following markup:
<asp:UpdatePanel ID="udpTrigger" runat="server" UpdateMode="Always">
<ContentTemplate>
<asp:button id="btnGo" runat="server" Text ="Go" />
</ContentTemplate>
</asp:UpdatePanel>
<asp:Panel ID="pnlContainer" runat="server">
</asp:Panel>
4: In Default.aspx.vb add the following code:
Dim _udp As UpdatePanel
Private Sub Page_Init(sender As Object, e As EventArgs) Handles Me.Init
_udp = New UpdatePanel()
_udp.ID = "udpTarget"
_udp.UpdateMode = UpdatePanelUpdateMode.Conditional
pnlContainer.Controls.Add(_udp)
End Sub
Private Sub btnGo_Click(sender As Object, e As EventArgs) Handles btnGo.Click
Dim ctrl = LoadControl("Control.ascx")
Dim pnlWrapper = New Panel With {.ID = "pnlWrapper"}
pnlWrapper.Controls.Add(ctrl)
_udp.ContentTemplateContainer.Controls.Add(pnlWrapper)
_udp.Update()
End Sub
Note:
There is a wrapping panel here between the user control and the update panel. This serves to demonstrate what which markup is missing on output.
5: Add a new user control “Control.ascx” into the site root folder and add the following in the markup:
<asp:Panel ID="pnlControl" runat="server"></asp:Panel>
Results
So the portion of markup that we’re insterested is the content of pnlContainer
once we’ve hit btnGo
.
The markup for the two versions is shown below:
3.5
<div id="pnlContainer">
<div id="udpTarget">
<div id="pnlWrapper">
<div id="ctl05_pnlControl">
</div>
</div>
</div>
</div>
4.5
<div id="MainContent_pnlContainer">
<div id="MainContent_udpTarget">
<div id="MainContent_ctl02_pnlControl">
</div>
</div>
</div>
As you can see, in the 4.5 version, pnlWrapper
is omitted completely - the UpdatePanel update is only returning the content of the user control, not anything else that was added with it.
Checking the response body in fiddler2 shows that the wrapper panel is omitted at server side (ie we’re not losing it in the client side ajax processing)
On investigation the 4.5 behaviour is also there in 4.0 (which isn’t particularly surprising)
Attempting to solve
1: Use LoadControl(type,params) instead of LoadControl(virtualPath).
This method does reproduce the wrapping panel, but none of the content of the user control. See here for a reference to this.
2: Put the update panel in the markup instead of creating dynamically
This works in theory - everything displays, however I’m not going to be able to use this in the project where I have the problem.
Microsoft Connect
I’ve now logged this a Microsoft Connect here If you’re able to reproduce too, then head over there and add a vote to the issue. Hopefully they’ll feedback with something soon.
Workaround
I’ve implemented a workaround which wraps the user control in a server control, and overrides the RenderContent
method on the server control. This allows the user control to be rendered to a separate HtmlTextWriter
and then the content transferred to the main writer instance.
Its a mucky work around, but is working so far in our existing large project (as well as in the above sample).
Here the content of the workaround servercontrol:
UserControlLoader
''' <summary>
''' UserControl Loader - loads a user control
''' Works around a problem with ASP.Net Webforms in 4.0/4.5
''' </summary>
<ToolboxData("<{0}:UserControlLoader runat=server></{0}:UserControlLoader>")> _
Public Class UserControlLoader
Inherits WebControl
Public ReadOnly Property Control As Control
Get
Return _control
End Get
End Property
Private _control As Control
Public Sub LoadControl(page As Page, virtualPath As String)
_control = page.LoadControl(virtualPath)
Me.Controls.Add(_control)
End Sub
Public Overrides Sub RenderBeginTag(writer As HtmlTextWriter)
'Don't render anything
End Sub
Public Overrides Sub RenderEndTag(writer As HtmlTextWriter)
'Don't render anything
End Sub
''' <summary>
''' Overrides RenderContent to write the content to a separate writer,
''' then adds the rendered markup into the main HtmlTextWriter instance.
''' </summary>
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
If _control Is Nothing Then Return
Using sw = New StringWriter()
Using hw = New HtmlTextWriter(sw)
MyBase.RenderContents(hw)
writer.Write(sw.ToString)
End Using
End Using
End Sub
End Class
Usage:
To implement this into the above example the btnGo
event handler becomes:
Private Sub btnGo_Click(sender As Object, e As EventArgs) Handles btnGo.Click
Dim loader = new UserControlLoader()
loader.LoadControl(Page,"Control.ascx")
Dim pnlWrapper = New Panel With {.ID = "pnlWrapper"}
pnlWrapper.Controls.Add(loader)
_udp.ContentTemplateContainer.Controls.Add(pnlWrapper)
_udp.Update()
End Sub