Grouping Update Panels

Without a doubt, AJAX is one of the most useful web development tools out there. Through AJAX, we finally have a method for developing web pages with robust user interfaces.

The most common and simplest method of AJAX development on ASP.NET is to use the UpdatePanel. While the UpdatePanel creates a lot of overhead, it is very simple to implement. However, sometimes you will have multiple UpdatePanels on a page that relate to each other. In order to reduce your overhead, you will probably want to set thier UpdateMode to Conditional. With the Conditional UpdateMode enabled, they will only update under three conditions (note that nested UpdatePanels are a bit more complicated):

  1. A child control of the UpdatePanel performs a postback, and ChildrenAsTriggers is true
  2. The Update method is explicitly called.
  3. A manually defined trigger is fired.

However, this doesn’t help with firing the other UpdatePanels on the page that are related. While you can update the other panels manually using the Update method or triggers, that can potentially be a lot of work and difficult to maintain.

In order to address this, I’ve created a simple GroupedUpdatePanel. It has a GroupName property which is used to group multiple GroupedUpdatePanels together. If any one UpdatePanel in a group is updated as part of an asynchronous postback, they will all be updated.

There are two important restrictions to note. First, you must set UpdateMode to Conditional. If any one GroupedUpdatePanel in a group is set to Always, they will all update every time. This is a result of the limitations of inheriting from the UpdatePanel class. Second, all of the UpdatePanels must be created and on the page at the time the PreRender event occurs. I’ve seen some reports on the web of certain situations where this is not the case, though I haven’t tested for any of it.

You can download the source for the control here. Feel free to use it in your own applications.

UPDATE: Please see updated version here

Linking Stylesheets From Content Pages

Personally, I find stylesheets to be the best way to apply formating to web pages. Skins certainly have their uses, but they tend to pad out the HTML with lots of redundant information that can be avoided by referencing a stylesheet, thus reducing download times. Stylesheets can be easily added to themes, but sometimes you don’t want to reference all of your stylesheets on every page, which is what happens if you put stylesheets into a theme.

When using master and content pages, this can be a bit tricky. The common solution isn’t all that difficult, you just have to add a content section inside the header of your master page.

<% Master Language=”VB” AutoEventWireup=”false” CodeBehind=”SystemMaster.Master.vb” Inherits=”MyWebApp.SystemMaster” %>

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>

<html xmlns=”http://www.w3.org/1999/xhtml>
<head runat=”server”>
<title&gt;Untitled Page</title>
<asp:ContentPlaceHolder ID=”HeadEntries” runat=”server”></asp:ContentPlaceHolder>
</head>
<body>
<asp:ContentPlaceHolder ID=”Body” runat=”server”></asp:ContentPlaceHolder>
</body>
</html>

Then on your content page:

<%@ Page Language=”vb” AutoEventWireup=”false” MasterPageFile=”~/SystemMaster.Master” CodeBehind=”Page.aspx.vb” Inherits=”MyWebApp.Page”
title=”Untitled Page” %>

<asp:Content ID=”HeadEntries” ContentPlaceHolderID=”HeadEntries” runat=”server”>
<link rel=”stylesheet” type=”text/css” href=”styles/styles.css” />
</asp:Content>

<asp:Content ID=”Body” ContentPlaceHolderID=”Body” runat=”server”>
Insert Body Here…
</asp:Content>

Now this works great for most scenarios, but what if you’re using URL rewriting in order to dynamically generate your pages based on the URL that was requested? In that case, the relative path to the stylesheet won’t work. If you know that your web application will always be the root application on the server, you can use a path that starts with /, but that isn’t always the case. What you really need to do is use an application relative path, starting with a ~/.

Normally you can change your link tag to include runat=”server” and it will process application relative paths in the href attribute. However, this doesn’t work on content pages for some reason. Therefore, I’ve created the class below to help.

Imports System
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

<DefaultProperty(“href”), ToolboxData(“&lt;{0}:SmartLink runat=””server””&gt;&lt;/{0}:SmartLink&gt;”)> _
Public Class SmartLink
Inherits WebControl

<Bindable(True), Category(“Behavior”), DefaultValue(“”), Localizable(True)> _
Property href() As String
Get
Dim s As String = CStr(ViewState(“href”))
If s Is Nothing Then
Return String.Empty
Else
Return s
End If
End Get

Set(ByVal Value As String)
ViewState(“href”) = Value
End Set
End Property

<Bindable(True), Category(“Behavior”), DefaultValue(“”), Localizable(True)> _
Property rel() As String
Get
Dim s As String = CStr(ViewState(“rel”))
If s Is Nothing Then
Return String.Empty
Else
Return s
End If
End Get

Set(ByVal Value As String)
ViewState(“rel”) = Value
End Set
End Property

<Bindable(True), Category(“Behavior”), DefaultValue(“”), Localizable(True)> _
Property type() As String
Get
Dim s As String = CStr(ViewState(“type”))
If s Is Nothing Then
Return String.Empty
Else
Return s
End If
End Get

Set(ByVal Value As String)
ViewState(“type”) = Value
End Set
End Property

Public Overrides Sub RenderBeginTag(ByVal writer As System.Web.UI.HtmlTextWriter)
If rel &lt;&gt; String.Empty Then
writer.AddAttribute(HtmlTextWriterAttribute.Rel, rel)
End If
If type &lt;&gt; String.Empty Then
writer.AddAttribute(HtmlTextWriterAttribute.Type, type)
End If
If href &lt;&gt; String.Empty Then
writer.AddAttribute(HtmlTextWriterAttribute.Href, ResolveClientUrl(href))
End If
writer.RenderBeginTag(HtmlTextWriterTag.Link)
End Sub

End Class


Now you replace the link tag on the content page with this:

<cc1:SmartLink runat=”server” rel=”stylesheet” type=”text/css” href=”~/styles/style.css” />

That’s it, now you have an application relative stylesheet link on your content page that works with URL rewriting.

Using UpdatePanels Inside A Repeater On A Content Page

In my previous post, I described how to use an UpdatePanel that is located inside a Repeater or other data-bound control while maintaining your databinding. In addition to the data-binding challenge, dynamically creating UpdatePanels your page can also pose other complications.

Another issue that I’ve encountered is identifying controls that the AsyncPostBackTrigger on the UpdatePanel refer to if you are using master pages. So long as the control you are trying to trigger on is located in the same Content control of the content page, everything works just fine. However, if the UpdatePanel is located in one Content control while the control that triggers the asynchronous postback is in another UpdatePanel, you’ll get an exception when the page is loaded saying that it couldn’t find a control with the ID you specified.

I’ve found various solutions for correcting this problem by dynamically adding the trigger in code, and using the control’s UniqueID to identify it. However, this becomes significantly more complicated when you are dealing with a dynamically added control. The best solution I’ve found so far is to use ScriptManager.RegisterAsyncPostBackControl to register the post back, and manually call Update on each UpdatePanel to perform the update. Below is the code that I’ve found fixes the problem.

Partial Public Class _Default
Inherits System.Web.UI.Page

Dim DateUpdatePanels AS New List(Of UpdatePanel)

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
ScriptManager.GetCurrent(Me).RegisterAsyncPostBackControl(edtDate)
DataBind()
End Sub

Private Sub Repeater_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.RepeaterItemEventArgs) Handles Repeater.ItemDataBound
Dim pnl As UpdatePanel = e.Item.FindControl(“upnlShowTimes”)
If Not IsNothing(pnl) Then
DateUpdatePanels.Add(pnl)
End If
End Sub

Private Sub edtDate_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles edtDate.TextChanged
For Each pnl As UpdatePanel In DateUpdatePanels
pnl.Update()
Next
End Sub

End Class

Note that if you define your ScriptManager on your content page instead of the master page, you can refer to it directly instead of using ScriptManager.GetCurrent(Me). Also, you must be sure to set UpdateMode to Conditional on each UpdatePanel.

Using UpdatePanels inside a Repeater

Sometimes you might find it necessary to place an UpdatePanel control inside the ItemTemplate of a Repeater or other databound control. Normally you would probably just put the UpdatePanel outside of the repeater, but this can cause other problems. First of all, it makes the size of the update much larger. Secondly, I ran into a situation where I was using the curvyCorners system for rounding the corners of DIVs that were inside my ItemTemplate.

First of all, it was problematic to run the javascript to round the corners again after the AJAX was done. You can do it using the ScriptManager.RegisterStartupScript method, so long as you place the recommended script code directly in the script block, and don’t use the window.onload event. However, I found that it is VERY slow since it is running on several DIVs.

Therefore, I decided to use individual UpdatePanel controls inside each DIV that update just the data that needs updating. The problem I encountered is that I couldn’t reference Container.DataItem for my data-binding inside the UpdatePanel like I could when it wasn’t there.

The workaround I found was to use type-casting, and instead reference CType(Container, IDataItemContainer).DataItem. This fixed the problem without any side effects, at least so far. Each update panel is independently updated, and the rounded corners stay intact on my DIVs.

Note that I didn’t have any problems using the Eval method, just DataBinder.GetPropertyValue and other DataBinder methods that require that you pass the DataItem as a parameter.

Using Transactions With DataSets

One of the major shortcomings of the ADO.NET dataset system, in my opinion, is the lack of support for transactions. In .NET 2.0 this situation was somewhat addressed by the addition of the System.Transactions namespace. However, this namespace has two flaws when it comes to using it with a DataSet. First, if you open more than one database connection within the same TransactionScope, it will use the Distributed Transaction Coordinator (MSDTC) instead of more efficient and configuration free database transactions. Second, if you require backwards compatibility with SQL 2000, it will ALWAYS use the MSDTC even if you open just a single database connection. The first issue can be addressed by opening the connection in advance and passing it to each TableAdapter that you use. Note that you’ll need to change the Connection property on the TableAdapter from Friend (internal) to Public in the DataSet designer if the DataSet is in a DLL.

The second issue is a bit trickier to solve. In this case, you also need to open a single database connection as before, but then use an old fashioned SqlTransaction instead of the System.Transactions namespace. Then both the SqlConnection and SqlTransaction need to be set on each TableAdapter. This can be done by adding a special helper function to the partial class for the TableAdapter, as shown below.

Namespace DataSetTableAdapters

Partial Class TableAdapter

Public Sub SetConnection(ByVal cn As SqlConnection, trans As SqlTransaction)
Connection = cn
For Each cmd As SqlCommand In Me.CommandCollection
cmd.Transaction = trans
Next
Adapter.UpdateCommand.Transaction = trans
Adapter.InsertCommand.Transaction = trans
Adapter.DeleteCommand.Transaction = trans
End Sub

End Class

End Namespace