Using the Observer Pattern in .NETby Michael Weier
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.
- We will use .NET to build a binary clock, suitable for display on any desktop.
- We will examine the use of the Observer design pattern.
- We will take a look at building custom Windows components within the .NET framework.
- 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.
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 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.
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.
- Right-click on an existing a project and select Add and then User Control.
- Enter a name for your control.
- Drag components from the toolbox onto the control surface to create the new control, and implement code behind it.
- 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
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.
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.
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
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.