Tuesday 1 March 2011

Dynamic Loading of ASP.NET User Controls

In this article we'll look at how to dynamically load and unload web user controls on an ASP.NET page and then page through the selected controls.

Why? I hear you ask; well, my need was for a referral system in which a user could select one or more agencies to refer a job to and then complete each of the agency-specific forms (which in turn are connected to their own data sources). The challenge was on to link-up the user selections with the various forms in a simple end-to-end manner and then have each of the forms handle their own bit of data at the end of the process.

To demonstrate this, we'll create a clutch of user controls to load on demand, another control to take the user selections and then finally wrap it all up in an ASP.NET page to handle the paging.

First create a set of user controls that you want to make available to your user:
<%@ Control Language="VB" AutoEventWireup="false" CodeFile="Car.ascx.vb" Inherits="Car" %>
<h1>Car</h1>

<asp:Label ID="CarLabel" runat="server" AssociatedControlID="CarList" Text="Car:"></asp:Label>
<asp:DropDownList ID="CarList" runat="server">
    <asp:ListItem Value="Rolls-Royce" Text="Rolls-Royce"></asp:ListItem>
    <asp:ListItem Value="Bentley" Text="Bentley"></asp:ListItem>
</asp:DropDownList>

Then create a selector control so that the user can choose which of the controls to be presented with:
<%@ Control Language="VB" AutoEventWireup="false" CodeFile="Selector.ascx.vb" Inherits="Selector" %>
<h1>Acme Party Services</h1>
<asp:Label ID="SelectorLabel" runat="server" AssociatedControlID="SelectorList" Text="Please select your party services:"></asp:Label>
<asp:CheckBoxList ID="SelectorList" runat="server">
    <asp:ListItem Value="Car" Text="Car"></asp:ListItem>
    <asp:ListItem Value="Dress" Text="Dress"></asp:ListItem>
    <asp:ListItem Value="Suit" Text="Suit"></asp:ListItem>
</asp:CheckBoxList>

In the code behind of the selector control expose the user selections through a property:
Partial Class Selector
    Inherits System.Web.UI.UserControl

    Public ReadOnly Property SelectedControlNames() As ArrayList
        Get
            Return GetSelectedControlNames()
        End Get
    End Property

    Private Function GetSelectedControlNames() As ArrayList

        Dim al As New ArrayList

        For Each li As ListItem In SelectorList.Items
            If li.Selected Then
                al.Add(li.Value)
            End If
        Next

        Return al

    End Function

End Class

Finally, create the page to host the user controls along with the paging mechanism:
<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb" Inherits="_Default" %>
<%@ Register TagPrefix="uc" TagName="Selector" Src="~/Selector.ascx" %>
<%@ Register TagPrefix="uc" TagName="Car" Src="~/Car.ascx" %>
<%@ Register TagPrefix="uc" TagName="Dress" Src="~/Dress.ascx" %>
<%@ Register TagPrefix="uc" TagName="Suit" Src="~/Suit.ascx" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Control Pager</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    
        <%-- PlaceHolder to wrap the dynamic user controls --%>        
        <asp:PlaceHolder ID="PlaceHolder1" runat="server">
        
            <%-- Declare the static 'Selector' user control --%>            
            <uc:Selector ID="Selector1" runat="server" />
        
        </asp:PlaceHolder>
        
        <br />
        
        <%-- Buttons for paging through the controls --%>        
        <asp:Button ID="BackButton" runat="server" Text="&lt; Back" Visible="false" />
        <asp:Button ID="NextButton" runat="server" Text="Next &gt;" />
    
    </div>
    </form>
</body>
</html>

The code behind shows that we start by loading all of the controls from the control history (using an array list held in view state), this is because dynamic controls do not survive page post backs on their own and must be reloaded in exactly the same order in which they were last provisioned. Next we load up any new controls that have been selected and then later in the page life cycle, if the selection has changed, unload any controls that are no longer required. Finally, using the 'Back' and 'Next' buttons we can display each control in the sequence:
Partial Class _Default
    Inherits System.Web.UI.Page

    ' this property maintains a history of loaded controls
    Public Property SelectedControls() As ArrayList
        Get
            Dim o As Object = ViewState("SelectedControls")
            If o Is Nothing Then
                Return New ArrayList
            End If
            Return CType(ViewState("SelectedControls"), ArrayList)
        End Get
        Set(ByVal value As ArrayList)
            ViewState("SelectedControls") = value
        End Set
    End Property

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        LoadControls()

    End Sub

    Private Sub LoadControls()

        ' Maintain the control indexes and viewstate by loading the
        ' control history plus any new selections and then drop any 
        ' de-selected controls later in the page life cycle.

        ' Read in control history
        Dim controlHistory As ArrayList = SelectedControls

        ' Load controls
        For Each controlItem As String In controlHistory
            Dim uc As UserControl = LoadControl("~/" & controlItem & ".ascx")
            uc.ID = controlItem
            PlaceHolder1.Controls.Add(uc)
        Next

        ' Read in the selected control names from the selector control
        Dim controlList As ArrayList = Selector1.SelectedControlNames

        ' Load new selections
        For Each controlItem As String In controlList

            If Not controlHistory.Contains(controlItem) Then

                Dim uc As UserControl = LoadControl("~/" & controlItem & ".ascx")
                uc.ID = controlItem

                PlaceHolder1.Controls.Add(uc)

                ' It matters to viewstate that properties are added after 
                ' the control has been added to the control hierarchy
                uc.Visible = False

                controlHistory.Add(controlItem)

            End If
        Next

        ' Update the control history
        SelectedControls = controlHistory

    End Sub

    Private Sub CleanUpControlHistory()

        Dim controlHistory As ArrayList = SelectedControls
        Dim controlHistoryCopy As ArrayList = controlHistory.Clone

        ' Read in the selected control names from the selector control...
        Dim selectedControlNames As ArrayList = Selector1.SelectedControlNames

        ' ...and compare them to the control history
        For Each controlItem As String In controlHistoryCopy

            If Not selectedControlNames.Contains(controlItem) Then

                ' Drop unused control
                Dim uc As UserControl = PlaceHolder1.FindControl(controlItem)
                PlaceHolder1.Controls.Remove(uc)
                controlHistory.Remove(controlItem)
            End If
        Next

        ' Update the control history
        SelectedControls = controlHistory

    End Sub

    Protected Sub NextButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles NextButton.Click

        ' Clean up control history
        If Selector1.Visible Then CleanUpControlHistory()

        Move(1)

    End Sub

    Protected Sub BackButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles BackButton.Click

        Move(-1)

    End Sub

    Private Sub Move(ByVal iDirection As Integer)

        Dim ctrlTop As Integer = 0
        Dim ctrlBottom As Integer = PlaceHolder1.Controls.Count - 1

        ' Iterate through the control collection
        For i As Integer = ctrlTop To ctrlBottom

            If ((iDirection = -1 And i > ctrlTop) Or (iDirection = 1 And i < ctrlBottom)) Then

                Dim uc As UserControl = PlaceHolder1.Controls(i)

                ' Position on visible control
                If uc.Visible Then

                    Dim nxt As Integer = (i + iDirection)

                    ' Change view
                    uc.Visible = False
                    CType(PlaceHolder1.Controls(nxt), UserControl).Visible = True
                    BackButton.Visible = (nxt > 0)

                    Exit For
                End If
            End If
        Next

    End Sub

End Class

There you have it, a web user control loader and pager. Enjoy.

Demo

View a live demo at http://code.zyky.com/controlpager

Code

Download the Source Code

Trails