Managing Nested GridView Controls, Page 3
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
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
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
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
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.
Implementing the Subscriber
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
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
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
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
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.
As Advanced As You Wanna Be
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.
About the Author
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.
