Introduction
I was very interested in reading the article, Incremental Page Display Pattern for User Controls [^] by Acoustic. He built a solution for real asynchronous loading of partial content. His solution was good but had one weakness: he was unable to react to postbacks. Therefore, controls were static. They could only display initial content, and users were unable to interact with the controls. I decided to write a completely new control that could support the following features:
- Developers would not even have to write one line of JavaScript.
- Loads user controls (*.ascx) really asynchronously.
- The user should be able to interact with the user controls.
- It should not be necessary to render the whole page on updates, like the AJAX
UpdatePanel
does. To save server resources, only the user control should be rendered. - There should only be a small communication overhead. That means that only a part of the
viewstate
should be transferred back to the server, that belongs to the user control. AJAX UpdatePanel
transfers the viewstate
of the whole page and all the controls back to the server, which could be a large one. - The control should support some minor features like auto updating every n milliseconds.
Usage Scenarios
Example scenarios where you could use the PartialUpdatePanel
instead of the AJAX UpdatePanel
might be:
- Autonomous sections of your page that require PostBack-support but not the environment information of the whole page (e.g. data lists with paging support where the user can browse through news, feeds, mails, etc.)
- User feedback when your control has to complete long operations. In this case, use a
PartialUpdatePanel
with render method "Clientside
". The surrounding page will be displayed with a waiting message. The user gets feedback that something is going on he has to wait for.
Basic Concepts
The complete control consists of a server-side control, a client-side script part, and an HttpHandler
. The rendering scenario of an ASCX control is described in the following image:
The ASPX Page
contains a PartialUpdatePanel
which has its UserControlPath
property set to the ASCX control.
<iucon:PartialUpdatePanel runat="server" ID="Panel1"
UserControlPath="~/PostBackSample.ascx">
<ErrorTemplate>
Unable to refresh content
</ErrorTemplate>
<LoadingTemplate>
<div style="margin-left: 84px; margin-top: 10px;">
<asp:Image ID="Image1" runat="server"
ImageUrl="~/images/loading.gif" />
</div>
<div style="text-align: center">
Updating...
</div>
</LoadingTemplate>
</iucon:PartialUpdatePanel>
When the page loads, some JavaScript calls the PartialUpdatePanelHandler
. It sends the path to the user control and also other data like the viewstate
of the control, via HTTP-POST.
var request = new Sys.Net.WebRequest();
request.set_url('PartialUpdatePanelLoader.ashx');
request.set_httpVerb('POST');
request.set_body(this._createRequestBody(eventTarget, eventArgument));
request.set_userContext(this);
request.add_completed(this._loadingComplete);
The user control needs a Page
object to be rendered in. This job is done by PanelHostPage
.
if (context.Request.Form["__USERCONTROLPATH"] != null)
{
PanelHostPage page = new PanelHostPage(
context.Request.Form["__USERCONTROLPATH"],
context.Request.Form["__CONTROLCLIENTID"]);
((IHttpHandler)page).ProcessRequest(context);
context.Response.Clear();
context.Response.Write(page.GetHtmlContent());
}
The user control gets normally rendered by simply adding it to the Controls
collection of the Page
.
protected override void CreateChildControls()
{
if (_controlPath != null)
_mainForm.Controls.Add(LoadControl(ResolveUrl(_controlPath)));
base.CreateChildControls();
}
The page output is sent back to the HttpHandler
.
The contents are transferred to the client, and inserted into the active HTML-document via DOM-operations.
contentPanel.innerHTML = sender.get_responseData();
That's all.
ViewState
The contents of a PartialUpdatePanel
are handled like this were the whole page. So, it has its own viewstate
which is stored in a hidden field. The hidden field is transferred to the HttpHandler
and loaded by the PanelHostPage
. The logic of loading, serializing, and deserializing the viewstate
is done in two overridden methods in PanelHostPage
.
private string _pageViewState;
protected override object LoadPageStateFromPersistenceMedium()
{
PartialPageStatePersister persister =
PageStatePersister as PartialPageStatePersister;
persister.PageState = _pageViewState;
persister.Load();
return new Pair(persister.ControlState, persister.ViewState);
}
protected override void SavePageStateToPersistenceMedium(object state)
{
PartialPageStatePersister pageStatePersister =
this.PageStatePersister as PartialPageStatePersister;
if (state is Pair)
{
Pair pair = (Pair)state;
pageStatePersister.ControlState = pair.First;
pageStatePersister.ViewState = pair.Second;
}
else
{
pageStatePersister.ViewState = state;
}
pageStatePersister.Save();
_pageViewState = HttpUtility.UrlEncode(pageStatePersister.PageState);
}
The custom PageStatePersister
PartialPageStatePersister
uses a LosFormatter
to load and store the ViewState
and ControlState
correctly.
The contents of _pageViewState
are transferred back to the client again as a hidden input field.
Event Handling
In ASP.NET, event handling is quite simple. When a user clicks on a button, for example, the ClientID
of this button is passed back to the page in the __EVENTTARGET
field. This behaviour could be easily reproduced in our scenario. All we have to do is add OnClientClick
calls for every button, call from there a method that loads the partial content, and add the event source to the __EVENTTARGET
field that is passed to our HttpHandler
. We have nothing more to do, the rest is up to the ASP.NET event pipeline.
Using the Control
Before using the control, you have to register the HttpHandler
in your web.config:
<httpHandlers>
<add verb="*" path="*.ashx" validate="false"
type="iucon.web.Controls.PartialUpdatePanelHandler"/>
</httpHandlers>
Also add a value to the appSettings
. Change the value to your individual one!
<appSettings>
<add key="PartialUpdatePanel.EncryptionKey" value="k39#9sn1"/>
</appSettings>
Then, simply add a PartialUpdatePanel
to your page and set the UserControlPath
property. Add some controls to the LoadingTemplate
and ErrorTemplate
. The contents of the LoadingTemplate
become visible when update operations are in progress. The contents of the ErrorTemplate
are shown if an error occurs during the update (e.g. server timeout). If you set AutoRefreshInterval
to a value greater then 0
, the panel refreshes every n milliseconds automatically.
There is also a mechanism to communicate with the control via JavaScript or server-side code. Parameters
is a collection that takes key-value-pairs. They are passed to the control via HTTP-POST when it is rendered. The following sample shows how to provide some parameters in the ASPX-page, set values via JavaScript, and read them in your ASCX control with the help of the class ParameterCollection
.
<%----%>
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"
UserControlPath="~/ParameterSample.ascx">
<Parameters>
<iucon:Parameter Name="MyParameter" Value="Hello world" />
<iucon:Parameter Name="Counter" Value="0" />
</Parameters>
<ErrorTemplate>
Unable to refresh content
</ErrorTemplate>
</iucon:PartialUpdatePanel>
<%----%>
<script type="text/javascript">
var counter = 0;
function updateParameterSample()
{
$find("PartialUpdatePanel4").get_Parameters()["Counter"] = ++counter;
$find("PartialUpdatePanel4").refresh();
}
</script>
<input type="button" onclick="updateParameterSample(); return false;"
value="Click to update panel with counter" />
The code-behind of the ASCX reads the parameter values from the ParameterCollection
:
public partial class ParameterSample : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
iucon.web.Controls.ParameterCollection parameters =
new iucon.web.Controls.ParameterCollection.Instance;
Label1.Text = "Called " + parameters["Counter"] + " times";
}
}
You can also change values of your parameters. They will be transferred back and become accessible via JavaScript.
The property InitialRenderBehaviour
controls if the control should be rendered by the server during the normal page rendering (InitialRenderBehaviour.Serverside
). If this property is set to InitialRenderBehaviour.Clientside
, the client sends a request to the server to render the control when the page is already transferred to the browser. This is useful to the user if a part of the page needs longer time to render, but the user should be able to see other parts already. He does not need to wait for the whole page, but can already play with some parts while others are still loading. InitialRenderBehaviour.None
causes the control not to render until it is requested via the JScript call $find("PartialUpdatePanel4").refresh();
as shown in one of the above samples. In this case, the <InitialTemplate>
contents are shown until a refresh-call occurs.
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel1"
UserControlPath="~/ExternalRefreshSample.ascx"
InitialRenderBehaviour="None">
<ErrorTemplate>
Unable to refresh content
</ErrorTemplate>
<LoadingTemplate>
Updating...
</LoadingTemplate>
<InitialTemplate>
Nothing useful here until refresh() gets called
</InitialTemplate>
</iucon:PartialUpdatePanel>
If you want to prevent flickering with LoadingTemplate
and ContentTemplate
, you can use the property DisplayLoadingAfter
to set a timespan in milliseconds. The loading template won't be shown before this time expires. This is useful if your PartialUpdatePanel
refreshes very quickly.
By using the property RenderAfterPanel
you can load contents in a sequential order. If your site uses e.g. 10 PartialUpdatePanel
s with the InitialRenderBehaviour
set to Clientside
you will fire 10 requests to the Web server at the same time. This can cause a lot of load for the server. RenderAfterPanel
means that the current PartialUpdatePanel
will not be refreshed after another one was loaded. So the contents are loaded step by step and the load for the server gets reduced.
You can dynamically add and run JavaScript code from your UserControl
s. The methods ScriptManager.RegisterStartupScript
and ScriptManager.RegisterClientScriptBlock
are supported as long as you set addScriptTags
to true
. The following code shows how to show an alert box when the user clicks on a button in a UserControl
hosted in the PartialUpdatePanel
.
protected void Button1_Click(object sender, EventArgs e)
{
string script = string.Format("alert('{0}');", TextBox1.Text);
ScriptManager.RegisterStartupScript(this, GetType(), "alert", script, true);
}
The internal transport and execution of the JavaScript code is a bit tricky. You cannot simply add a <script>
tag in your HTML code. Then, when the HTML code updates the DOM-tree of your document, <script>
nodes won't be executed. Internally, the PartialUpdatePanel
transports all JavaScript snippets in a hidden div
. The div
is created by the ScriptRenderer
class. The PartialUpdatePanel
JavaScript code that updates the document with the newly rendered contents reads the div
, uses Sys._ScriptLoader.getInstance()
to execute the transported JavaScript, and at last, removes the div
from the document via DOM operations.
Properties
EncryptUserControlPath | Encrypts the path to your UserControl using DES algorithm and the value defined in your web.config. This property is set to true by default. Attention: If you set EncryptUserControlPath , you cannot change the UserControlPath via JavaScript. |
UserControlPath | Virtual path to your UserControl |
AutoRefreshInterval | Forces the Panel to refresh every n milliseconds |
DisplayLoadingAfter | Render the current UserControl after another successfully loaded |
Parameters | Described above in this article ;-) |
InitialRenderBehaviour | Sets the initial rendermode to Serverside, Clientside or None |
AJAX Control Toolkit Support
It was a really hard job to get some controls of the AJAX Control Toolkit working with the PartialUpdatePanel
. The main problem was that the controls are instantiating themselves in the client during runtime by calling $create()
in Sys.Application.add_init
.
Sys.Application.add_init(function() {
$create(AjaxControlToolkit.AutoCompleteBehavior, {...}, null, null, $get(
"PartialUpdatePanel7_myTextBox"));
);
Now when a partial post back is finished, the same code runs a second time and the function Sys.Application.addComponent
which gets called by $create$
fails because the component was already instantiated.
To solve this problem, I had to make quite a dirty hack. Before the client scripts in the partial update response are executed, I change the function reference of Sys.Application.addComponent
to a local one which checks if the component was already instantiated.
_addComponent : function(component) {
var id = component.get_id();
if (typeof(Sys.Application._components[id]) === 'undefined')
Sys.Application._components[id] = component;
}
This prevents addComponent
from failing and throwing an exception. When all scripts ran successfully, the original function pointer is restored.
One Last Word
If you use this control, I would be happy if you send me the URL of your project so I can see the control in action. And... please don't forget to vote for this article.
History & Updates
13th Sep, 2010 | Version 1.8
|
14th Nov, 2008 | Version 1.6
Special thanks to grown from CodePlex for his excellent work on the PartialUpdatePanel !
- Added encryption support for
UserControl path - Added support for custom
ScriptManager types (includes ToolkitScriptManager ) - Added support for
ToolkitScriptManager.CombineScriptsHandlerUrl - Change
UserControlPath using JavaScript during runtime - Manipulate Parameters serverside during roundtrip
- Fixed issue using validators and rendering in Clientside mode
- Fixed a bug with recreating components
|
9th July, 2008 | Version 1.5.2
- Fixed a bug with supporting the state of
RadioButtons - Added support for globalization
- Showing up extended exception information
|
10th June, 2008 | Version 1.5
- Added support for some Controls from the ASP.NET AJAX Control Toolkit
- Added support for
ScriptManager.RegisterDataItem - Added support for
ScriptManager.RegisterClientScriptInclude - New example called
ToolkitSample shows some Toolkit controls inside the PartialUpdatePanel
|
20th May, 2008 | Version 1.4
ControlState is now transferred correctly between postbacks using a custom PageStatePersister - New property
DisplayLoadingAfter - New property
RenderAfterPanel - Minor bug fixes
|
29th Mar, 2008 | Version 1.3
- Added support for
ScriptManager.RegisterStartupScript to run JScript code after partial postbacks - Parameters are no more transported via HTTP-GET but via HTTP-POST
- Output bug fix: the
<form> tag was rendered more than once when using controls with the InitialRenderMode server-side
|
28th Feb, 2008 | Version 1.2.1
- Minor bug fix:
Viewstate of initially server-side rendered controls were not handled correctly on normal postbacks
|
27th Feb, 2008 | Version 1.2
- Added the property
InitialRenderBehaviour to PartialUpdatePanel - Added
IRequiresSessionState to PartialUpdatePanelHandler (thanks to aliascodeproject) UrlEncode -d the viewstate in PanelHostPage (thanks to Acoustic)
|
20th Feb, 2008 | Initial release |
The project is hosted on CodePlex [^]. The updates there are uploaded in smaller intervals. So if you are interested in this project, you should visit this site regularly.
Known Problems
The ModalPopupExtender
runs fine only in FireFox. Internet Explorer has some problems I have not solved yet. So if you want to show a modal popup from within a PartialUpdatePanel
, create the ModalPopupExtender in the parent page. To show the dialog, call ScriptManager.RegisterStartupScript
in the code behind of your PartialUpdatePanel UserControl
.
Example:
<script type="text/javascript">
function showModalPopupViaClient() {
var modalPopupBehavior = $find('programmaticModalPopupBehavior');
modalPopupBehavior.show();
}
function hideModalPopupViaClient() {
var modalPopupBehavior = $find('programmaticModalPopupBehavior');
modalPopupBehavior.hide();
}
</script>
<asp:Button runat="server" ID="hiddenTargetControlForModalPopup" style="display:none"/>
<ajaxToolkit:ModalPopupExtender runat="server" ID="programmaticModalPopup"
BehaviorID="programmaticModalPopupBehavior"
TargetControlID="hiddenTargetControlForModalPopup"
PopupControlID="programmaticPopup"
BackgroundCssClass="modalBackground"
DropShadow="True"
PopupDragHandleControlID="programmaticPopupDragHandle"
RepositionMode="RepositionOnWindowScroll" >
</ajaxToolkit:ModalPopupExtender>
<asp:Panel runat="server" CssClass="modalPopup" ID="programmaticPopup"
style="display:none;width:350px;padding:10px">
<asp:Panel runat="Server" ID="programmaticPopupDragHandle"
Style="cursor: move;background-color:#DDDDDD;border:solid 1px Gray;
color:Black;text-align:center;">
ModalPopup shown and hidden in code
</asp:Panel>
<iucon:PartialUpdatePanel runat="server" ID="PartialUpdatePanel4"
UserControlPath="~/ToolkitSample.ascx" />
You can now use this sample to see how to use ModalPopup with an invisible TargetControl.
The ModalPopupExtender
this popup is attached to has a hidden target control. The popup is hidden
<asp:LinkButton runat="server" ID="hideModalPopupViaServer" Text="on the server side
in code behind" OnClick="hideModalPopupViaServer_Click" /> and
<a id="hideModalPopupViaClientButton" href="#">on the client in script</a>.
<br />
</asp:Panel>
The code behind in your PartialUpdatePanel
UserControl
will look like:
public partial class ToolkitSample : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
ScriptManager.RegisterStartupScript(this, GetType(), "showPopup",
"showModalPopupViaClient()", true);
}
}
This will execute showPopup
after a partial PostBack and the modal popup gets shown.