http://www.developer.com/net/asp/article.php/3634441/Managing-Nested-GridView-Controls.htm
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. 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. 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: 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) 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 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. 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 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. Listing 5 shows the publisher interface, which only needs to publish events that will be available to subscribers. Listing 5: The IPublisher 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 Subscription happens when the page is loaded and un-subscribing happens when the page is unloaded. If you are new to .NET, Listings 7, 8, and 9 present the Customer, Order, and data-access classes, respectively. Listing 7: The Customer Class
Listing 8: The Implementation of the Order Class
The new nullable types are used for value type objects to permit assigning a null value to them without throwing an exception. Microsoft uses this technique when you use generated typed DataSet objects too. Nullable types can be assigned Nothing and work fine between nullable and non-nullable value types. You don't need to make string-types nullable because they are reference types and thus already nullable. Listing 9: The Custom Data Access Class
Now you know what a publisher should look like to subscribers. However, you need additional plumbing. You need methods to permit downstream nested controls to send events, and you need code to publish those events to subscribers. Listing 10 contains the implementation of the publisher (I named Broadcaster by personal convention). Listing 10: An Implementation of IPublisher The code in bold implements IPublisher. This code literally just re-raises the event to anything that might be listening. Generally, the listener is the main page, as previously mentioned. Because you need only one publisher, shared methods are defined to get a single Broadcaster/IPublisher instance from session. To receive these published events, the main page will subscribe and unsubscribe as need be. The subscriber is the main Page itself. To create the subscriber, all you need to do is implement ISubscriber in the Page and write the calls to Broadcaster.Subscribe and Broadcaster.Unsubscribe. Listing 11 shows the implementation of the main page representing the subscriber. Listing 11: The Page Containing All of the Nested Controls Implements ISubscriber
The code in bold coordinates "listening" to published events from anywhere in the nested control hierarchy via the broadcaster. You unsubscribe because delegates are multicast in .NET and if you repeatedly subscribe without unsubscribing you will get multiple event calls for the same event; you need only one. Listing 12 shows the downline nested user control. The user control responds to events in its area of concern and forwards those to the publisher: your broadcaster object. This part of the process is shown in bold. Listing 12: The OrdersControl Forwards its Events to the Broadcaster I am not going to insist that all of this plumbing is easy—nothing worth doing is. But, once you get the hang of patterns and wiring object-oriented code at this level of abstraction, you will find the results very orderly and predictable. It will become increasingly important that all VB programmers master OOP and design patterns. This article demonstrated some pretty advanced techniques, such as the observer behavior pattern, block script, and (I hope you agree) an imaginative way to manage nested controls reliably. You won't need to nest controls and grids all the time, but you may want to sometimes. You can create some very advanced and imaginative user interfaces if you have this technique in your toolbox. The real benefit is that no matter how complex your presentation layer gets, all you have to do is add more events. All events are raised by each user control, and then they go directly to the broadcaster and from there the main page. For your part, you will have to try the code and step through it a couple of times to get it—especially if interfaces and design patterns are unfamiliar territory. Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his new book UML DeMystified from McGraw-Hill/Osborne. Paul is an architect for Tri-State Hospital Supply Corporation. You may contact him for technology questions at pkimmel@softconcepts.com. If you are interested in joining or sponsoring a .NET Users Group, check out www.glugnet.org.
Managing Nested GridView Controls
September 27, 2006
Get Up to Speed
Adding the Plumbing to Nested Controls
Adding a Public Property on the User Control
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
<%@ 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 xmlns="http://www.w3.org/1999/xhtml" >
<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>
Implementing IPublish and ISubscribe
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
Public Delegate Sub OnFieldChangedEvent( _
ByVal sender As Object, ByVal e As ChangeEventArgs)
Public Interface IPublisher
Event PostalCodeFieldChangedEvent As OnFieldChangedEvent
REM Other events for any other fields that need syncronization
End Interface
Public Interface ISubscriber
Sub Subscribe(ByVal publisher As IPublisher)
Sub Unsubscribe(ByVal publisher As IPublisher)
End Interface
Imports Microsoft.VisualBasic
Imports System.Collections.Generic
' This code was coded using coderush
Public Class Customer
Private _myOrders As List(Of _Order) = New List(Of _Order)
Public Property MyOrders() As List(Of _Order)
Get
Return _myOrders
End Get
Set(ByVal Value As List(Of _Order))
_myOrders = Value
End Set
End Property
Private _customerID As String
Public Property CustomerID() As String
Get
Return _customerID
End Get
Set(ByVal Value As String)
_customerID = Value
End Set
End Property
Private _companyName As String
Public Property CompanyName() As String
Get
Return _companyName
End Get
Set(ByVal Value As String)
_companyName = Value
End Set
End Property
Private _contactName As String
Public Property ContactName() As String
Get
Return _contactName
End Get
Set(ByVal Value As String)
_contactName = Value
End Set
End Property
Private _contactTitle As String
Public Property ContactTitle() As String
Get
Return _contactTitle
End Get
Set(ByVal Value As String)
_contactTitle = Value
End Set
End Property
Private _address As String
Public Property Address() As String
Get
Return _address
End Get
Set(ByVal Value As String)
_address = Value
End Set
End Property
Private _city As String
Public Property City() As String
Get
Return _city
End Get
Set(ByVal Value As String)
_city = Value
End Set
End Property
Private _region As String
Public Property Region() As String
Get
Return _region
End Get
Set(ByVal Value As String)
_region = Value
End Set
End Property
Private _postalCode As String
Public Property PostalCode() As String
Get
Return _postalCode
End Get
Set(ByVal Value As String)
_postalCode = Value
End Set
End Property
Private _country As String
Public Property Country() As String
Get
Return _country
End Get
Set(ByVal Value As String)
_country = Value
End Set
End Property
Private _phone As String
Public Property Phone() As String
Get
Return _phone
End Get
Set(ByVal Value As String)
_phone = Value
End Set
End Property
Private _fax As String
Public Property Fax() As String
Get
Return _fax
End Get
Set(ByVal Value As String)
_fax = Value
End Set
End Property
End Class
Public Class _Order
Private _orderID As Nullable(Of Integer)
Public Property OrderID() As Nullable(Of Integer)
Get
Return _orderID
End Get
Set(ByVal Value As Nullable(Of Integer))
_orderID = Value
End Set
End Property
Private _customerID As String
Public Property CustomerID() As String
Get
Return _customerID
End Get
Set(ByVal Value As String)
_customerID = Value
End Set
End Property
Private _employeeID As Nullable(Of Integer)
Public Property EmployeeID() As Nullable(Of Integer)
Get
Return _employeeID
End Get
Set(ByVal Value As Nullable(Of Integer))
_employeeID = Value
End Set
End Property
Private _orderDate As Nullable(Of DateTime)
Public Property OrderDate() As Nullable(Of DateTime)
Get
Return _orderDate
End Get
Set(ByVal Value As Nullable(Of DateTime))
_orderDate = Value
End Set
End Property
Private _requiredDate As Nullable(Of DateTime)
Public Property RequiredDate() As Nullable(Of DateTime)
Get
Return _requiredDate
End Get
Set(ByVal Value As Nullable(Of DateTime))
_requiredDate = Value
End Set
End Property
Private _shippedDate As Nullable(Of DateTime)
Public Property ShippedDate() As Nullable(Of DateTime)
Get
Return _shippedDate
End Get
Set(ByVal Value As Nullable(Of DateTime))
_shippedDate = Value
End Set
End Property
Private _shipVia As Nullable(Of Integer)
Public Property ShipVia() As Nullable(Of Integer)
Get
Return _shipVia
End Get
Set(ByVal Value As Nullable(Of Integer))
_shipVia = Value
End Set
End Property
Private _freight As Nullable(Of Decimal)
Public Property Freight() As Nullable(Of Decimal)
Get
Return _freight
End Get
Set(ByVal Value As Nullable(Of Decimal))
_freight = Value
End Set
End Property
Private _shipName As String
Public Property ShipName() As String
Get
Return _shipName
End Get
Set(ByVal Value As String)
_shipName = Value
End Set
End Property
Private _shipAddress As String
Public Property ShipAddress() As String
Get
Return _shipAddress
End Get
Set(ByVal Value As String)
_shipAddress = Value
End Set
End Property
Private _shipCity As String
Public Property ShipCity() As String
Get
Return _shipCity
End Get
Set(ByVal Value As String)
_shipCity = Value
End Set
End Property
Private _shipRegion As String
Public Property ShipRegion() As String
Get
Return _shipRegion
End Get
Set(ByVal Value As String)
_shipRegion = Value
End Set
End Property
Private _shipPostalCode As String
Public Property ShipPostalCode() As String
Get
Return _shipPostalCode
End Get
Set(ByVal Value As String)
_shipPostalCode = Value
End Set
End Property
Private _shipCountry As String
Public Property ShipCountry() As String
Get
Return _shipCountry
End Get
Set(ByVal Value As String)
_shipCountry = Value
End Set
End Property
End Class
Imports Microsoft.VisualBasic
Imports System.Collections.Generic
Imports System.Data
Imports System.Data.SqlClient
Public Class Data
Private Const connectionString As String = _
"Password=test;Persist Security Info=True;User ID=dummy;" + _
"Initial Catalog=Northwind;Data Source=localhost;"
Public Shared Function GetCustomerList() As List(Of Customer)
Dim customers As List(Of Customer) = GetCustomer()
Dim orders As List(Of _Order) = GetOrder()
Dim c As Customer
Dim o As _Order
For Each c In customers
For Each o In orders
If (c.CustomerID = o.CustomerID) Then
c.MyOrders.Add(o)
End If
Next
Next
Return customers
End Function
Private Shared Function GetCustomer() As List(Of Customer)
Using connection As SqlConnection = _
New SqlConnection(connectionString)
connection.Open()
Dim command As SqlCommand = _
New SqlCommand("SELECT * FROM Customers", connection)
Dim reader As SqlDataReader = command.ExecuteReader()
Dim customers As List(Of Customer) = New List(Of Customer)
While (reader.Read())
Dim customer As Customer = New Customer
customer.Address = HandleDBNull(reader("Address"))
customer.City = HandleDBNull(reader("City"))
customer.CompanyName = HandleDBNull(reader("CompanyName"))
customer.ContactName = HandleDBNull(reader("ContactName"))
customer.ContactTitle = HandleDBNull(reader("ContactTitle"))
customer.Country = HandleDBNull(reader("Country"))
customer.CustomerID = HandleDBNull(reader("CustomerID"))
customer.Fax = HandleDBNull(reader("Fax"))
customer.Phone = HandleDBNull(reader("Phone"))
customer.PostalCode = HandleDBNull(reader("PostalCode"))
customer.Region = HandleDBNull(reader("Region"))
customers.Add(customer)
End While
Return customers
End Using
End Function
Private Shared Function HandleDBNull(ByVal o As Object)
If (o Is System.DBNull.Value) Then
Return Nothing
Else
Return o
End If
End Function
Private Shared Function GetOrder() As List(Of _Order)
Using connection As SqlConnection = _
New SqlConnection(connectionString)
connection.Open()
Dim command As SqlCommand = New SqlCommand( _
"SELECT * FROM Orders Order BY CustomerID", connection)
Dim reader As SqlDataReader = command.ExecuteReader()
Dim orders As List(Of _Order) = New List(Of _Order)
While (reader.Read())
Dim o As _Order = New _Order()
o.CustomerID = HandleDBNull(reader("CustomerID"))
o.EmployeeID = HandleDBNull(reader("EmployeeID"))
o.Freight = HandleDBNull(reader("Freight"))
o.OrderDate = HandleDBNull(reader("OrderDate"))
o.OrderID = HandleDBNull(reader("OrderID"))
o.RequiredDate = HandleDBNull(reader("RequiredDate"))
o.ShipAddress = HandleDBNull(reader("ShipAddress"))
o.ShipCity = HandleDBNull(reader("ShipCity"))
o.ShipCountry = HandleDBNull(reader("ShipCountry"))
o.ShipName = HandleDBNull(reader("ShipName"))
o.ShippedDate = HandleDBNull(reader("ShippedDate"))
o.ShipPostalCode = HandleDBNull(reader("ShipPostalCode"))
o.ShipRegion = HandleDBNull(reader("ShipRegion"))
o.ShipVia = HandleDBNull(reader("ShipVia"))
orders.Add(o)
End While
Return orders
End Using
End Function
End Class
Tip: For the custom data-access class in a production system, you would move the connection string to the web.config file and encrypt it. That technique is fodder for another article.
Building the Publisher
Public Class Broadcaster
Implements IPublisher
Private Const KEY As String = "BROADCASTER"
Public Shared Function GetBroadcaster( _
ByVal session As HttpSessionState) As Broadcaster
If (session Is Nothing) Then Return Nothing
If (session(KEY) Is Nothing) Then
session(KEY) = New Broadcaster
End If
Return CType(session(KEY), Broadcaster)
End Function
Public Shared Sub PostalCodeFieldChanged( _
ByVal session As HttpSessionState, ByVal sender As Object, _
ByVal e As ChangeEventArgs)
If (session Is Nothing) Then Return
Dim instance As Broadcaster = GetBroadcaster(session)
If (instance Is Nothing = False) Then
instance.PostalCodeFieldChanged(sender, e)
End If
End Sub
Private Sub PostalCodeFieldChanged( _
ByVal sender As Object, ByVal e As ChangeEventArgs)
RaiseEvent PostalCodeFieldChangedEvent(sender, e)
End Sub
Public Shared Sub Subscribe(ByVal session As HttpSessionState, _
ByVal subscriber As ISubscriber)
subscriber.Subscribe(GetBroadcaster(session))
End Sub
Public Shared Sub Unscubscribe(ByVal Session As HttpSessionState, _
ByVal subscriber As ISubscriber)
subscriber.Unsubscribe(GetBroadcaster(Session))
End Sub
Public Event PostalCodeFieldChangedEvent( _
ByVal sender As Object, ByVal e As ChangeEventArgs) _
Implements IPublisher.PostalCodeFieldChangedEvent
End Class
Implementing the Subscriber
Imports System.Collections.Generic
Partial Class _Default
Inherits System.Web.UI.Page
Implements ISubscriber
Private customers As List(Of Customer)
Private KEY As String = "Customers"
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
Broadcaster.Subscribe(Session, Me)
If (IsPostBack = False) Then
customers = Data.GetCustomerList
GridView1.DataSource = customers
GridView1.DataBind()
Else
customers = CType(Session(KEY), List(Of Customer))
End If
End Sub
Protected Sub Page_Unload(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Unload
Broadcaster.Unscubscribe(Session, Me)
End Sub
Public Sub OnPostalCodeFieldChangedEvent( _
ByVal sender As Object, ByVal e As ChangeEventArgs)
REM Update the data here
End Sub
Public Sub Subscribe(ByVal publisher As IPublisher) _
Implements ISubscriber.Subscribe
AddHandler publisher.PostalCodeFieldChangedEvent, _
AddressOf OnPostalCodeFieldChangedEvent
End Sub
Public Sub Unsubscribe(ByVal publisher As IPublisher) _
Implements ISubscriber.Unsubscribe
RemoveHandler publisher.PostalCodeFieldChangedEvent, _
AddressOf OnPostalCodeFieldChangedEvent
End Sub
End Class
Imports System.Collections.Generic
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
Protected Sub TextBox2_TextChanged(ByVal sender As Object, _
ByVal e As System.EventArgs)
Broadcaster.PostalCodeFieldChanged(Session, sender, _
New ChangeEventArgs("ShipPostalCode", _
CType(sender, TextBox).Text, GetParentID(sender), _
GetChildID(sender)))
End Sub
Private Function GetChildID(ByVal sender As Object) As Object
Try
Return CType(sender.Parent.Parent.Cells(1), _
DataControlFieldCell).Text
Catch ex As Exception
Return Nothing
End Try
End Function
Private Function GetParentID(ByVal sender As Object) As Object
Try
Return CType(sender.Parent.Parent.Cells(0), _
DataControlFieldCell).Text
Catch ex As Exception
Return Nothing
End Try
End Function
End Class
Imports System.Collections.Generic
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
Protected Sub TextBox2_TextChanged(ByVal sender As Object, _
ByVal e As System.EventArgs)
Broadcaster.PostalCodeFieldChanged(Session, sender, _
New ChangeEventArgs("ShipPostalCode", _
CType(sender, TextBox).Text, GetParentID(sender), _
GetChildID(sender)))
End Sub
Private Function GetChildID(ByVal sender As Object) As Object
Try
Return CType(sender.Parent.Parent.Cells(1), _
DataControlFieldCell).Text
Catch ex As Exception
Return Nothing
End Try
End Function
Private Function GetParentID(ByVal sender As Object) As Object
Try
Return CType(sender.Parent.Parent.Cells(0), _
DataControlFieldCell).Text
Catch ex As Exception
Return Nothing
End Try
End Function
End Class
As Advanced As You Wanna Be
About the Author