ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Using the Observer Pattern in .NET

by Michael Weier
01/03/2005

I recently visited a friend who received a binary clock as a gift. Throughout the afternoon, I found myself almost hypnotized by the marching of the lights across the clock face. When I left, I was convinced that it would be a great example of the Observer pattern. This article demonstrates the building of a binary clock program with the following goals in mind.

  1. We will use .NET to build a binary clock, suitable for display on any desktop.
  2. We will examine the use of the Observer design pattern.
  3. We will take a look at building custom Windows components within the .NET framework.
  4. We will take a look at some other places where the Observer pattern is utilized within the framework itself.

The Observer Pattern

The fundamental design of this project is based on a modified Observer design pattern. This pattern was first described in the Gang of Four book Design Patterns (Gamma, Helm, Johnson, Vlissides, Design Patterns. Addison-Wesley, 1995). It is used when it is necessary to ensure that multiple components (called observers or subscribers) are kept in sync with a master set of data (the subject or publisher).

In the case of the binary clock, we will assign the responsibility of the publisher to the web form. The form will be responsible for maintaining a copy of the current time, and will notify all observer components when the time changes. Observing components will consist of custom light components. Each light component will be responsible for determining whether or not it should be lit based on the data provided by the publisher.

Design Patterns discusses both push and pull models of the Observer pattern. In the push model, the publishing object is responsible for broadcasting all of the detailed data to all of the subscribing components. This is useful when the observing components are expected to use nearly all of the data maintained in the publisher. On the other hand, when the publisher maintains a lot of data, and each subscriber will only use a small portion of it, a pull methodology is employed. The publisher notifies subscribers that new data exists, and the subscriber calls back to the publisher to retrieve only the data it needs. In the binary clock scenario, the amount of data to be broadcast is small (just the time), so we will push the data from the publisher.

Design pattern purists will be quick to note that this application does not use every feature of an Observer pattern. Notably, the attach and detach functions are missing. Since this application is self-contained to a single form, I consider adding a control to the form the equivalent of attaching to the publisher. Design patterns serve as a blueprint for reusable code, but should be flexible for the needs of an individual application.

What is a Binary Clock?

As seen in Figure 1, a binary clock consists of a series of lights arranged in grid pattern. The columns of the grid represent the different numbers involved in a time. The first pair of columns represent the hour, the second pair of columns represents minutes, and the third pair represents seconds. Each row of the binary clock represents a power of 2. The first row represents 20 (or 1), the second represents 21 (2), and the third and fourth columns represent the numbers four and eight, respectively.

Binary Clock Image
Figure 1. The binary clock

You can read a binary clock by adding up the values of the individual columns. For example, the clock pictured here is displaying the time 23:01:39 (11 p.m.).

Building the Application

Links to the entire source of the application can be found here. In this section, we will discuss the various parts of the code.

ITimeNotify

The ITimeNotify interface is used for transferring data between the publisher and the observers. The interface consists of a single function, named UpdateTime. By breaking the time notification into a separate interface, it becomes very easy to create new components that will receive time information.


Function UpdateTime(ByVal dTime As DateTime)

When the time changes, the Windows form will iterate through all controls implementing ITimeNotify. It will call the UpdateTime function to indicate to the component that it should refresh itself.

Related Reading

ASP.NET Cookbook
The Ultimate ASP.NET Code Sourcebook
By Michael A. Kittel, Geoffrey T. LeBlond

The Light Component

Each individual "light" on the form is an implementation of a user component. The core logic behind the binary clock lies within the light components.

Adding a new user control to .NET is a very simple task.

  1. Right-click on an existing a project and select Add and then User Control.
  2. Enter a name for your control.
  3. Drag components from the toolbox onto the control surface to create the new control, and implement code behind it.
  4. Once the control is compiled, it will reside on the My User Controls tab of the toolbox, and can be dragged onto a web form in the same manner as any pre-built control.

For this application, we are going to take advantage of the graphics functions included in .NET to fulfill two primary functions. First, we will use these tools to size a circle (representing the light) to fill the entire control. Second, we will paint the light red or black, depending on whether or not the light is lit.

The size of the initial circle is created when the component is instantiated. Resizing the circle is maintained in the Resize subroutine. The code is self-explanatory, except for possibly the Invalidate command. This command indicates to the framework that the window is dirty and needs to be refreshed.

Private m_oRect As New Rectangle(0, 0, Me.Width-1, Me.Height-1)

Private Sub Light_Resize(ByVal sender As Object, _
                         ByVal e As EventArgs) _
                         Handles MyBase.Resize
  Me.Height = Me.Width
  m_oRect.X = 0
  m_oRect.Y = 0
  m_oRect.Width = Me.Width
  m_oRect.Height = Me.Height
  Me.Invalidate(m_oRect)
  PaintLight()
End Sub

The Resize subroutine calls the PaintLight subroutine. This subroutine is responsible for determining whether or not to paint a light as lit or unlit. This is based on the internal variable m_bIsLit. We will see in a minute how this variable is set when the UpdateTime function is called.

Private Sub PaintLight()
  Dim oBrush As Brush
  Dim g As Graphics = Me.CreateGraphics

  If m_bIsLit = True Then
      oBrush = New SolidBrush(Color.Red)
  Else
      oBrush = New SolidBrush(Color.Black)
  End If

  g.FillEllipse(oBrush, m_oRect)

  oBrush.Dispose()
  g.Dispose()

  oBrush = Nothing
  g = Nothing
End Sub

The final three pieces of the publicly exposed interface work very closely together. The Base2Factor property indicates what value (1, 2, 4, or 8) this instance of the light represents. The TimeFactor property indicates what piece of the time the instance of this control should examine (the ones place of the seconds, the tens place of the hours, etc.). Finally, the UpdateTime function will be called when the control should refresh itself.

The Base2Factor and TimeFactor properties are exposed in exactly the same manner as properties are exposed in a standard class.

Public Enum eBase2Factor
  One = 1
  Two = 2
  Four = 4
  Eight = 8
End Enum

Public Enum eTimeFactor
  SecondsOnes
  SecondsTens
  MinutesOnes
  MinutesTens
  HoursOnes
  HoursTens
End Enum

Public Property Base2Factor() As eBase2Factor
  Get
    Return m_Base2Factor
  End Get
  Set(ByVal Value As eBase2Factor)
    m_Base2Factor = Value
  End Set
End Property

Public Property TimeFactor() As eTimeFactor
  Get
    Return m_TimeFactor
  End Get
  Set(ByVal Value As eTimeFactor)
    m_TimeFactor = Value
  End Set
End Property

Public properties in a user control have some unique functionality. When the user control is compiled, public properties will be exposed on the properties window when an instance of the control is added to the form. This allows custom properties to be set at either design time or run time.

Properties snapshot
Figure 2. Properties snapshot

The final step in the light component is the implementation of the UpdateTime function. First, we grab the appropriate portion of the time, based on the TimeFactor property. This function uses two less commonly used operators. The modulus operator divides by the value on the right-hand side and returns the remainder after the division. (In this case, the ones place.) The integer division operator (note that this is a backslash, instead of a forward slash) divides by the value on the right-hand side and discards any remainder. (In this case, the tens place.)

Select Case TimeFactor
  Case eTimeFactor.HoursOnes
      iTimePiece = dTime.Hour Mod 10
  Case eTimeFactor.HoursTens
      iTimePiece = dTime.Hour \ 10
...
End Select

Once the appropriate time selection is made, a bitwise comparison of the two numbers are made. When running an and statement on two integers, a comparison of the individual bits within the integer is made. When both bits are ones, a one will be entered in the column being compared. In any other case, a zero will be returned.

If (iTimePiece And Base2Factor) = 0 Then
  m_bIsLit = False
Else
  m_bIsLit = True
End If

For example, if the portion of the time we are looking at is 6, and the individual light has a Base2Factor of four the comparison occurs as follows.

6 = 1 1 0
4 = 1 0 0
----------
    1 0 0 

Since the bits in the first column match, the returned result is greater than zero, and the light will be lit.

The Web Form

The final step includes implementing the components on a web form. Once the light component has been compiled, instances can be added to the form by dragging them from the My User Controls toolbox onto the web form. We can than set the Base2Factor and TimeFactor properties. All lights in the bottom row should have a Base2Factor of 1. The second row should have a base factor of two, the third should have a factor of four, and the fourth should have a factor of eight. The columns should have Time factors of HoursTens on the far left to SecondsOnes on the far right.

A timer control is used on the web form to check the system clock. The timer interval is set to one-fifth of a second. This can be tweaked to any desired accuracy. When the time has changed, any control implementing the INotify interface is asked to update itself.

Dim oControl As Control
Dim oTime As ITimeNotify

If System.DateTime.Now.Second <> m_iLastSecond Then
  m_dCurrentTime = System.DateTime.Now
  m_iLastSecond = m_dCurrentTime.Second

  For Each oControl In Me.Controls
    If TypeOf oControl Is ITimeNotify Then
      oTime = oControl
      oTime.UpdateTime(m_dCurrentTime)
    End If
  Next
End If

Note that the UpdateTime function is called on an object of the type ITimeNotify, not an object of the type CLight. By going through the interface, it is very easy to add controls of different types, and still have them respond to the Notify function. I have included in the sample code a user control titled TimeLabel. Simply dragging a TimeLabel control onto the web form and recompiling causes this control to be active, without changing any of the code in the web form itself.

.NET and the Observer Pattern

The study of design patterns is particularly interesting, as .NET was built with them as a foundation. The Observer pattern seems to appear in many cases throughout the framework.

One of the most visible occurrences of an Observer pattern in .NET is in the implementation of the TraceListener class. Appending individual TraceListener classes to the collection allow them to receive notifications of messages, which can then be consumed. Overriding the default trace listener allows one to write completely customizable code that reacts to a message broadcast from the collection.

Dim myWriter As New TextWriterTraceListener(System.Console.Out)
Trace.Listeners.Add(myWriter)

The trace listener again illustrates the main capabilities of the Observer pattern. The ListenersCollection maintains the master record of the data and broadcasts it to any Observers who have subscribed.

Many real-world opportunities similar to this exist in day-to-day programming. The next time you are designing a new application, I encourage you to look for places where this pattern may apply. The Observer pattern will allow you cleaner, more understandable code with very little effort.

Michael Weier is currently the technical lead for E-Commerce applications of a Fortune 1000 company and is a Microsoft Certified Solutions Developer.


Return to ONDotnet.com.

Copyright © 2009 O'Reilly Media, Inc.