In this chapter we discuss how you can use the Observer pattern to present data in several forms at once. In our new, more sophisticated windowing world, we often would like to display data in more than one form at the same time and have all of the displays reflect any changes in that data. For example, you might represent stock price changes both as a graph and as a table or list box. Each time the price changes, we'd expect both representations to change at once without any action on our part.
We expect this sort of behavior because there are any number of Windows applications, like Excel, where we see that behavior. Now there is nothing inherent in Windows to allow this activity, and, as you may know, programming directly in Windows in C or C++ is pretty complicated. In VB, however, we can easily use the Observer Design Pattern to make our program behave this way.
The Observer pattern assumes that the object containing the data is separate from the objects that display the data and that these display objects observe changes in that data. This is simple to illustrate, as we see in Figure 28-1.
When we implement the Observer pattern, we usually refer to the data as the Subject and each of the displays as an Observer. Each of these observers registers its interest in the data by calling a public method in the Subject. Then each observer has a known interface that the subject calls when the data change. We could define these interfaces as follows.
'Interface Observer Public Sub sendNotify(mesg As String) End Sub '----- 'Interface Subject Public Sub registerInterest(obs As Observer) End Sub
The advantages of defining these abstract interfaces is that you can write any sort of class objects you want as long as they implement these interfaces and that you can declare these objects to be of type Subject and Observer no matter what else they do.
Let's write a simple program to illustrate how we can use this powerful concept. Our program shows a display form containing three radio buttons named Red, Green, and Blue, as shown in Figure 28-2.
Note that our main form class implements the Subject interface. That means that it must provide a public method for registering interest in the data in this class. This method is the registerInterest method, which just adds Observer objects to a Collection.
Private Sub Subject_registerInterest(obs As Observer) observers.Add obs End Sub
Now we create two observers, one that displays the color (and its name) and another that adds the current color to a list box.
Private Sub Form_Load() Set observers = New Collection 'create list observer Dim lso As New lsObserver lso.init Me lso.Show 'create color fram observer Dim cfr As New ColorFrame cfr.init Me cfr.Show End Sub
When we create our ColorForm window, we register our interest in the data in the main program.
'Class ColorForm Implements Observer Public Sub init(s As Subject) s.registerInterest Me End Sub Private Sub Observer_sendNotify(mesg As String) Pic.Cls Select Case LCase(mesg) Case "red" Pic.BackColor = vbRed Case "green" Pic.BackColor = vbGreen Case "blue" Pic.BackColor = vbBlue End Select Pic.PSet (300, 600) Pic.Print mesg End Sub
Our list box window is also an observer, and all it has to do is add the color name to the list. The entire class is shown here.
'Class ListObserver Implements Observer Public Sub init(s As Subject) s.registerInterest Me End Sub '----- Private Sub Observer_sendNotify(mesg As String) 'add color names to list lsColors.AddItem mesg End Sub
Meanwhile, in our main program, every time someone clicks on one of the radio buttons, it calls the sendNotify method of each Observer who has registered interest in these changes by simply running through the objects in the Observer's Collection.
Private Sub btColor_Click(Index As Integer) Dim i As Integer Dim mesg As String Dim obs As Observer mesg = btColor(Index).Caption 'get the button label 'send it to all the observers For i = 1 To observers.Count Set obs = observers(i) obs.sendNotify mesg Next i End Sub
In the case of the ColorForm observer, the sendNotify method changes the background color and the text string in the form Picturebox. In the case of the ListForm observer, however, it just adds the name of the new color to the list box. We see the final program running in Figure 28-3.
In VB7, we still use the Observer and Subject interfaces to define the interaction between the data and the displays of that data.
Public Interface Observer Sub sendNotify(ByVal mesg As String) End Interface '----- Public Interface Subject Sub registerInterest(ByVal obs As observer) End Interface
As in the preceding, the main program with its three radio buttons constitutes the Subject or data class and notifies the observers when the data changes. To make the programming simpler, we add the same event handler to all three radio buttons.
Dim evh As EventHandler = _ New EventHandler(AddressOf radioHandler) AddHandler opRed.Click, evh AddHandler opblue.Click, evh AddHandler opgreen.Click, evh
Then we send the text of their label to the observers.
Protected Sub RadioHandler(ByVal sender As Object, _ ByVal e As EventArgs) Dim i As Integer Dim rbut As RadioButton = CType(sender, RadioButton) For i = 0 To observers.Count - 1 Dim obs As Observer = CType(observers(i), observer) obs.sendNotify(rbut.Text) Next i End Sub
The list box observer is essentially identical, where we add the text to the list box.
Public Class listObs Inherits System.WinForms.Form Implements Observer Public Sub New(ByVal subj As Subject) MyBase.New() listObs = Me InitializeComponent() subj.registerInterest(Me) End Sub '----- Public Sub sendNotify(ByVal mesg As String) _ Implements Observer.sendNotify lscolors.Items.Add(mesg) End Sub End Class
The Color window observer is a little different in that we paint the text in the paint event handler and change the background color directly in the notify event method.
Public Class ColForm Inherits System.WinForms.Form Implements Observer Private colname As String Dim fnt As Font Dim bBrush As SolidBrush '----- Public Sub New(ByVal subj As Subject) Me.new() 'Call base constructor subj.registerInterest(Me) fnt = New Font("arial", 18, Drawing.FontStyle.Bold) bBrush = New SolidBrush(Color.Black) AddHandler Pic.Paint, _ New PaintEventHandler(AddressOf paintHandler) End Sub '----- Public Sub sendNotify(ByVal mesg As System.String) _ Implements VBNetObserver.Observer.sendNotify colname = mesg Select Case mesg.ToLower Case "red" pic.BackColor = color.Red ' Case "blue" pic.BackColor = color.Blue Case "green" pic.BackColor = color.Green End Select End Sub '----- Private Sub paintHandler(ByVal sender As Object, _ ByVal e As PaintEventArgs) Dim g As Graphics = e.Graphics g.DrawString(colname, fnt, bbrush, 20, 40) End Sub End Class
Now, what kind of notification should a subject send to its observers? In this carefully circumscribed example, the notification message is the string representing the color itself. When we click on one of the radio buttons, we can get the caption for that button and send it to the observers. This, of course, assumes that all the observers can handle that string representation. In more realistic situations, this might not always be the case, especially if the observers could also be used to observe other data objects. Here we undertake two simple data conversions.
We get the label from the radio button and send it to the observers.
We convert the label to an actual color in the ColorFrame observer.
In more complicated systems, we might have observers that demand specific, but different, kinds of data. Rather than have each observer convert the message to the right data type, we could use an intermediate Adapter class to perform this conversion.
Another problem observers may have to deal with is the case where the data of the central subject class can change in several ways. We could delete points from a list of data, edit their values, or change the scale of the data we are viewing. In these cases we either need to send different change messages to the observers or send a single message and then have the observer ask which sort of change has occurred.
Observers promote abstract coupling to Subjects. A subject doesn't know the details of any of its observers. However, this has the potential disadvantage of successive or repeated updates to the Observers when there are a series of incremental changes to the data. If the cost of these updates is high, it may be necessary to introduce some sort of change management so the Observers are not notified too soon or too frequently.
When one client makes a change in the underlying data, you need to decide which object will initiate the notification of the change to the other observers. If the Subject notifies all the observers when it is changed, each client is not responsible for remembering to initiate the notification. On the other hand, this can result in a number of small successive updates being triggered. If the clients tell the Subject when to notify the other clients, this cascading notification can be avoided, but the clients are left with the responsibility of telling the Subject when to send the notifications. If one client “forgets,” the program simply won't work properly.
Finally, you can specify the kind of notification you choose to send by defining a number of update methods for the Observers to receive, depending on the type or scope of change. In some cases, the clients will thus be able to ignore some of these notifications.
The VB6 version of our observer example puts up three separate windows. However, unlike the VB7 version, closing one of the windows does not close the other two and end the program. How could you use an observer to ensure that the program shuts down as desired?