Managing Nested GridView Controls

Because propagating events up through two, three, or more layers of nested grids and controls can be challenging, I receive a lot of email about nested grids. In response, I have devised an easy way to manage complex ASP.NET presentations using a consistent, reliable approach and the observer pattern. This article demonstrates my approach.

You’ll see the plumbing of a nested presentation layer, including getting data in downstream, publishing changes to the page upstream using the publish-subscribe form of the observer pattern, and using nested controls. (Read a step-by-step walkthrough of creating a nested GridView control.) Whereas delegates/events in .NET are an implementation of the observer pattern that you can daisy chain through multiple layers of nested controls, the approach in this article bypasses that approach and implements a single publisher. All events are sent to the publisher, which forwards the events to the subscriber. This approach works in theory and in practice.

Get Up to Speed

My previous article showed you how to add a page with a GridView. In the grid view, it showed how to create a template column, drop a user control in it, and place a nested GridView on that user control. As far as the mechanics of nesting controls go, you can repeat this process as often as you’d like. The only caveat is that after three or four levels of nesting, the amount of data coming back from the server may make the page respond very slowly.

Adding the Plumbing to Nested Controls

The first half of this article presents a step-by-step walkthrough of designing the nested presentation layer. To manage nested controls, you need to push data to the downstream, nested controls and get events and data back to the main page. The reason for this is the page itself usually manages session state and has the business objects.

Tip: If you are using the InProc session state, you can modify data in session from anywhere in the control hierarchy because the InProc server uses pointers. However, once you switch to an out-of-process server, your business objects are serialized and desterilized, and an object in session in a user control will not point to the same address as a cached object in another control. That is, out-of-process session does not maintain addresses—it can’t.

It also is harder to reuse user controls if a specific object is grabbed from session at the user control level rather than passed down from a single object source.

This technique assumes that the business object is created and managed (in session) at the Page level, and that subordinate—or detail objects—are passed down to the nested controls using a public property. To summarize how the plumbing is assembled, you need to:

  1. Add a public property on every user control. This property represents the detail data you will be passing to the nested user control.
  2. You will need a binding statement to get the business object assigned to each nested user control instance created by each row of the GridView.
  3. You will need to implement the publisher and subscriber interfaces. The publisher is a stand-alone object and the subscriber is the Page itself.
  4. All nested controls will send all their events to the singleton instance of the publisher, which in turn will forward all events to one location, the Page.

Adding a Public Property on the User Control

Getting data to nested controls is easy. You need to declare a public property on the User Control. I use an object data type for the property. This approach permits me to reuse controls for different literal data types with similar kinds of data. It also eliminates the need for importing my business assembly references into the ASP.NET page.

Continuing where my previous article left off (with the custom business objects Customer and Order, each Customer contained a generic list of order objects; the main page contained a GridView with the Customer; and the nested GridView showed each of the Order objects for the Customer), you need a property that will ultimately represent the Customer Orders. Listing 1 demonstrates the user control with the property representing the orders.

Listing 1: The Elided OrdersControl with the Public Property Used to Bind the List (of Orders)

Partial Class OrdersControl
   Inherits System.Web.UI.UserControl

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

   End Sub

   Public WriteOnly Property Data() As Object
      Set(ByVal value As Object)
         If (TypeOf value Is Customer) Then
            GridView1.DataSource = CType(value, Customer).MyOrders
            GridView1.DataBind()
         End If
      End Set
   End Property

Binding the Detail Data to the User Control

The next step is to tell the main page—or the parent control, if you are nested more than two levels deep—that data needs to be pushed to children (see Listing 2). The very simple binding statement is shown in bold as an assignment to the OrdersControl’s public Data property using block script.

Listing 2: Binding the Child/Detail Data to the User Control

<%@ Page Language="VB" AutoEventWireup="false"
         CodeFile="Default.aspx.vb" Inherits="_Default" %>

<%@ Register Src="Orders.ascx" TagName="Orders" TagPrefix="uc2" %>

<%@ Register Src="OrdersControl.ascx"
             TagName="OrdersControl" TagPrefix="uc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html  >
<head runat="server">
   <title>Untitled Page</title>
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:GridView ID="GridView1" runat="server" Height="191px"
                    Width="325px" AutoGenerateColumns="False">
         <Columns>
            <asp:BoundField DataField="CustomerID"
                            HeaderText="Customer ID">
               <ItemStyle VerticalAlign="Top" />
               <HeaderStyle Wrap="False" />
            </asp:BoundField>
            <asp:BoundField DataField="CompanyName"
                            HeaderText="Company Name">
               <ItemStyle VerticalAlign="Top" />
               <HeaderStyle Wrap="False" />
            </asp:BoundField>
            <asp:BoundField DataField="ContactName"
                            HeaderText="Contact Name">
               <ItemStyle VerticalAlign="Top" />
               <HeaderStyle Wrap="False" />
            </asp:BoundField>
            <asp:TemplateField HeaderText="Order Shipping Details">
               <EditItemTemplate>
                   
               </EditItemTemplate>
               <ItemTemplate>
                  <!-- Here is the ASP with the binding statement -->
                  <uc1:OrdersControl ID="OrdersControl1"
                     runat="server" Data='<%# Container.DataItem %>'/>
               </ItemTemplate>
               <ItemStyle VerticalAlign="Top" />
               <HeaderStyle Wrap="False" />
            </asp:TemplateField>
         </Columns>
      </asp:GridView>

   </div>
   </form>
</body>
</html>

When the page in Listing 2 is rendered, each nested user control/GridView created for each row of the main GridView will be sent an instance of its Customer object. From the Customer object (refer to Listing 1), you have access to the List(Of Orders) named MyOrders.

Implementing IPublish and ISubscribe

You must define the publisher interface to indicate the events to which want to subscribe. These can be any variety. The key is to publish events for data that will change. Consequently, you need a delegate type and event arguments to contain the data. The publisher also needs to manage broadcasting the events to any subscriber listening. The subscriber is simply defined to get an instance of the publisher. When the subscriber is handed an instance of the publisher, it must bind to the published events.

To keep things manageable, the sample program for this article permits changing postal codes only. As such, you need to publish postal code changes and the main page needs to subscribe to those events. Listing 3 contains the definition of a suitable event argument class.

Listing 3: The definition of the Event Args Class

Public Class ChangeEventArgs

   Private _fieldName As String
   Private _parentID  As Object    REM Identify the object's parent
   Private _childID   As Object    REM Identify the object
   Private _value     As Object

   Public Sub New(ByVal FieldName As String, _
                  ByVal Value As Object, ByVal parentID As Object, _
                  ByVal childID As Object)

      _fieldName = FieldName
      _value     = Value
      _parentID  = parentID
      _childID   = ChildID
   End Sub

   Public ReadOnly Property ParentID() As Object
      Get
         Return _parentID
      End Get
   End Property

   Public ReadOnly Property ChildID() As Object
      Get
         Return _childID
      End Get
   End Property

   Public ReadOnly Property FieldName() As String
      Get
         Return _fieldName
      End Get
   End Property

   Public ReadOnly Property Value() As Object
      Get
         Return _value
      End Get
   End Property

End Class

The identifiers are used to ensure that you can access the correct business object at the page level. The field name and value properties contain the data that has changed.

Listing 4 defines a delegate that receives an instance of that argument type.

Listing 4: The definition of an event handler (delegate) that accepts ChangeEventArgs objects.

Public Delegate Sub OnFieldChangedEvent( _
   ByVal sender As Object, ByVal e As ChangeEventArgs)

Listing 5 shows the publisher interface, which only needs to publish events that will be available to subscribers.

Listing 5: The IPublisher Interface

Public Interface IPublisher
   Event PostalCodeFieldChangedEvent As OnFieldChangedEvent
   REM Other events for any other fields that need syncronization
End Interface

Listing 6 shows the subscriber interface, which only needs to know when it can subscribe and when it should unsubscribe.

Listing 6: The ISubscriber Interface

Public Interface ISubscriber
   Sub Subscribe(ByVal publisher As IPublisher)
   Sub Unsubscribe(ByVal publisher As IPublisher)
End Interface

Subscription happens when the page is loaded and un-subscribing happens when the page is unloaded.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read