.NET Remoting and Event Handling in VB .NET, Part 3

This final installment of the .NET Remoting and Event Handling in VB .NET series takes a closer look at some of the supporting code in the simple chat client and server application that Parts 1 and 2 demonstrated, including the use of the command, observer, singleton, and factory patterns. The previous installments also externalized the text for the client help by using an XML resource file and the resource manager, a feature that supports internationalization. Part 3 quickly covers that code as well.

Programming with Patterns

Patterns are general design solutions to well-known problems. The best-known patterns are the Gang of Four (GoF) patterns defined in Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Help, Ralph Johnson, and John Vlissides. Much has been written about patterns by these four and others.

Basically, patterns are reusable designs. The designs are clearly documented, believed, and demonstrated to solve the problems they are purported to solve when used properly, and commonly found in well-architected applications. The Design Patterns book (from Addison-Wesley) is the definitive resource on these patterns; I won't regurgitate everything in it. Instead, I show just the implementation of these patterns in the chat demo from Parts 1 and 2 with brief explanations of each one and how I used it.

Using the Command Behavior Pattern

The command pattern is the encapsulation of actions. One of the most well-known uses is supporting an undo behavior. Good frameworks such as Delphi's VCL use the command pattern for common actions like File|Open, Edit|Cut, and so on. The .NET Framework does not to date. (I suspect .NET will encapsulate commands for WinForms very soon.)

Just because the command pattern isn't part of .NET doesn't mean you can't use it. It's very simple: When you want to define an action, just encapsulate it in a class and attach an instance of the class to your GUI through event handlers or invoke the action from the GUI's events. The former is probably a better use of command objects.

Once you've bundled your commands in a class, supporting an undo behavior is easy—code the opposite action and queue the commands—as is doing things like log actions or even automating them with a macro-like language.

I defined several commands in the chat presentation layer. Listing 1 shows the complete listing for the commands.

Listing 1: The Commands Defined by the Presentation Layer for the Remoting Chat Sample

Imports System.Resources
Imports System.Security.Principal
Imports System.Text.RegularExpressions

Imports Softconcepts.ApplicationBlocks.RadioPattern
Imports SharedCode

Public Class CommandFactory
    Public Shared Function Create(ByVal input As String) As Command
        If (Quit.Wants(input)) Then Return New Quit
        If (Help.Wants(input)) Then Return New Help
        If (Send.Wants(input)) Then Return New Send
        If (Startup.Wants(input)) Then Return New Startup
        If (History.Wants(input)) Then Return New History
        If (Shutdown.Wants(input)) Then Return New Shutdown
        If (NullCommand.Wants(input)) Then Return New NullCommand
    End Function
End Class

Public MustInherit Class Command
    Public MustOverride Function Execute(ByVal input As String) _
           As Boolean
End Class

Public Class Quit
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return input.ToUpper() = "Q"
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        Return False
    End Function

End Class

Public Class Help
    Inherits Command

    Private Shared resourceManager As resourceManager = Nothing

    Shared Sub New()
        If (resourceManager Is Nothing) Then
            resourceManager = New ResourceManager("Client.Help", _
              System.Reflection.Assembly.GetExecutingAssembly())
        End If
    End Sub


    Public Shared Function Wants(ByVal input As String) As Boolean
        Return input = "?" Or Regex.IsMatch(input, "^help", _
                                            RegexOptions.IgnoreCase)
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        Try
            Broadcaster.Broadcast(resourceManager.GetString("HELP"))
        Catch
            Broadcaster.Broadcast("Help not available")
        End Try
        Return True
    End Function

End Class

Public Class NullCommand
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return True
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        Broadcaster.Broadcast(String.Format("Command invalid: '{0}'", input))
        Return True
    End Function

End Class

Public Class Send
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return Regex.IsMatch(input, "^send ", RegexOptions.IgnoreCase)
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        Dim message As String = input.Remove(0, 4).Trim()
        Broadcaster.Broadcast(String.Format("Sending '{0}'", message))
        SharedCode.Client.Send(WindowsIdentity.GetCurrent().Name, message)
        Return True
    End Function

    Private Function IsSelf(ByVal sender As String)
        Return sender = WindowsIdentity.GetCurrent().Name
    End Function
End Class

Public Class Startup
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return Regex.IsMatch(input, "^startup$", RegexOptions.IgnoreCase)
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        SharedCode.Client.Send(WindowsIdentity.GetCurrent().Name, "hello")
        Dim helper As Help = New Help
        helper.Execute("help")
        Return True
    End Function

End Class

Public Class Shutdown
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return Regex.IsMatch(input, "^shutdown$", RegexOptions.IgnoreCase)
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        Broadcaster.Broadcast("Goodbye")
        SharedCode.Client.Shutdown()
    End Function
End Class

Public Class History
    Inherits Command

    Public Shared Function Wants(ByVal input As String) As Boolean
        Return Regex.IsMatch(input, "^history$", RegexOptions.IgnoreCase)
    End Function

    Public Overrides Function Execute(ByVal input As String) As Boolean
        SharedCode.Client.ShowHistory()
        Return True
    End Function
End Class

The commands defined are Quit, Help, NullCommand, Send, Startup, Shutdown, and History. Each of these commands is a class and provides the behavior defined by the command's name. The NullCommand is actually a pattern (command behavior pattern) and a refactoring instruction (introduce null object). In this case, the NullCommand represents an invalid command, and the operation it invokes tells the user the command wasn't recognized. Null objects are a good way to eliminate tests for null. For example, instead of checking to see whether a command is returned, I can always just call Execute. If the NullCommand is returned, Execute is still a valid method call and I know an invalid command was entered in the example.

Each of the command classes overrides the abstract method Execute. Additionally, I added a static method called Wants. The factory uses this to create the correct instance of a command based on the user input.

In the example, the Send command sends a message to the chat server. From the presentation implementer's point of view, Send and all the other commands are equal. The result is a homogenization of action invocation. That is, a GUI program can invoke Send as easily as Quit without worrying about the nuts and bolts of remoting. This is a division of focus that promotes team development or at least a consistent style for one programmer (in other words, all behaviors are command objects.)

.NET Remoting and Event Handling in VB .NET, Part 3

Using the Factory Method Creational Pattern

There are three categories of patterns: behavioral, creational, and structural. Behavioral patterns define how objects interact; creational patterns define how objects are created; and structural patterns deal with class composition. The factory method is a creational pattern.

The factory method pattern manages the creation of related objects based on some criteria or a compound creational process. For example, the factory method helps when you need to perform several operations to create an object and ensure its proper initialization. In this example, for criteria like the text the user enters and the value that text represents, the factory method can contain the logic in one place to figure out which object to create. As Listing 1 shows, this example checks every possible command to see whether the command-line arguments represent a command by calling the static (Shared) Wants method on each command class. If it doesn't find a suitable command, an instance of the NullCommand is returned.

A final benefit is that no matter how many commands are ultimately created, the code for instantiating that command is modified in one place: the CommandFactory.Create method. Getting an instance of a command object looks the same throughout your application.

Using the Singleton Creational Pattern

Another creational pattern is the singleton pattern. Singleton's job is to ensure that only one instance of an object exists forever. It easily accomplishes this by using a non-public modifier on the constructor (Sub New) and a static method to access an instance of that class. As a static method, it can check to see whether an internal instance to the object exists. If the singleton object doesn't exist, the static method—as a member—can access the constructor and create the one and only instance. If the object does exist, the instance is returned.

The chat example uses the singleton pattern to ensure that only one instance of the Client class is created. In the elided example in Listing 2, you can see how the singleton is implemented as described by the private Client.New method and the public, shared, read-only Instance property.

Listing 2: The Singleton Pattern Ensures That Only One Instance of the Client Class Is Created Per Client Application Instance

Public Class Client
    Inherits MarshalByRefObject
    Implements IDisposable

    Private FChatter As Chatter
    Private Shared FClient As Client = Nothing

    Private Sub New()
        RemotingConfiguration.Configure("client.exe.config")
        FChatter = New Chatter
        AddHandler FChatter.MessageEvent, AddressOf OnMessageEvent
    End Sub

    Private Shared ReadOnly Property Instance() As Client
        Get
            If (FClient Is Nothing) Then
                FClient = New Client
            End If
            Return FClient
        End Get
    End Property
//...

Using the Observer Pattern

Programmers run into trouble when they start cross-referencing things like form references. For instance, Form1 creates Form2 so Form1 has a reference to Form2. Form2 wants to update a status bar on Form1, so Form2 in turn has a reference to Form1. All of this criss-crossing of references results in a tightly coupled implementation. The problem is exacerbated when object1 knows about object2 and vice-versa, and one of the objects is a Form and the other is a control that you want to install in the Toolbox or use in another application. Now, your control is aware of a specific form that exists in just one application, which means the first form's assembly is loaded by VS.NET and any other application that uses that control.

Because tightly coupled implementations have lots of problems, events (now called delegates) exist. Delegates are an example of an implementation of the observer pattern, which is another behavioral pattern. The observer pattern is also known as publish and subscribe. It is the publish-subscribe version of the observer pattern, which I originally called broadcaster and listener (or radio pattern) when I discovered it independently seven years ago this month. (I will use the broadcaster and listener metaphor, as I think it works best here.)

The basic idea of radio is that a station sends out a signal. Listeners can tune in and receive the signal and then tune out. The radio station doesn't have to—although it would like to—know who is listening when, and the listeners don't have to know where the station is physically located. The listeners tune in and receive the signal, and multiple listeners can listen at the same time.

In code, this would mean that the sender of a message doesn't have to know who is receiving the message (loose coupling), multiple listeners (a status bar, the event log, or something else) can tune in, and listeners can come and go with impunity.

All of this code can be implemented one time as a typed collection of listeners. The listeners are defined as an interface, and a centralized broadcaster keeps track of listeners and acts as a central repository for messages. Simply send a message to the broadcaster and anything tuned in will be sent a copy of the message. The originator and broadcaster don't care what the listeners do with the message or even who is listening. (Listing 3 defines the IListener interface; Listing 4 defines the ListenerCollection; and Listing 5 defines the Broadcaster.)

Listing 3: The IListener Interface

Public Interface IListener
    ReadOnly Property Listening() As Boolean
    Sub Listen(ByVal message As String)
    Sub Listen(ByVal message As String, ByVal formatter As IFormatter)
End Interface

Listing 4: The Strongly Typed ListenerCollection

Imports System.Collections

Public Class ListenerCollection
    Inherits CollectionBase

    Default Public Property Item(ByVal index As Integer) As IListener
        Get
            Return (CType(List(index), IListener))
        End Get
        Set(ByVal Value As IListener)
            List(index) = Value
        End Set
    End Property

    Public Function Add(ByVal value As IListener) As Integer
        Return List.Add(value)
    End Function

    Public Sub Remove(ByVal value As IListener)
        List.Remove(value)
    End Sub

End Class

Listing 5: The Broadcaster Receives and Sends Messages to All Listeners

Public Class Broadcaster

    Private Shared FInstance As Broadcaster = Nothing
    Private listeners As ListenerCollection

    Protected Sub New()
        listeners = New ListenerCollection
    End Sub

    Protected Shared ReadOnly Property Instance() As Broadcaster
        Get
            If (FInstance Is Nothing) Then
                FInstance = New Broadcaster
            End If

            Return FInstance
        End Get
    End Property

    Public Shared Function Add(ByVal listener As IListener) As Integer
        Return Instance.listeners.Add(listener)
    End Function

    Public Shared Sub Remove(ByVal listener As IListener)
        Instance.listeners.Remove(listener)
    End Sub

    Public Shared Sub Broadcast(ByVal format As String, _
                                ByVal ParamArray args() As Object)
        Broadcast(String.Format(format, args))
    End Sub

    Public Shared Sub Broadcast(ByVal message As String)
        Broadcast(message, New GenericFormatter)
    End Sub

    Public Shared Sub Broadcast(ByVal message As String, _
                                ByVal formatter As IFormatter)
        Dim listener As IListener
        For Each listener In Instance.listeners
            If (listener.Listening) Then
                listener.Listen(message, formatter)
            End If
        Next
    End Sub

End Class
Note: The Broadcaster uses the singleton pattern to ensure that only one Broadcaster instances exists. This ensures every message and listener go to the same place.

Anything that implements IListener can be added to and removed from the Broadcaster's collection of listeners. When a message arrives from anywhere—because the static method Broadcaster.Broadcast was called—the Broadcaster iterates through every listener in the collection and repeats the message. This means that if a form implements IListener (the Listening property and the Listen methods), then that form can receive the message. It also means any class can listen. The elided class ClientApp in Listing 6 shows how the presentation layer in the chat sample listens for messages from the remote server without ever knowing that they came from the remote server.

Listing 6: Realizing the Radio Pattern in the Console Presentation Layer

Imports Softconcepts.ApplicationBlocks.RadioPattern

Class ClientApp
    Implements IListener

    Public Overloads Sub Listen(ByVal message As String) _
           Implements IListener.Listen
        Console.WriteLine(message)
    End Sub

    Public ReadOnly Property Listening() As Boolean _
           Implements IListener.Listening
        Get
            Return True
        End Get
    End Property

    Public Shared Sub Main()
        With New ClientApp
            .Run()
        End With
    End Sub

    Public Sub Run()
        Broadcaster.Add(Me)

        ProcessCommand("Startup")

        While (True)
            Console.Write("chat>")
            If (ProcessCommand(Console.ReadLine()) = False) _
            Then Exit While
        End While

        ProcessCommand("Shutdown")

        Broadcaster.Remove(Me)
    End Sub

    Public Overloads Sub Listen(ByVal message As String, _
      ByVal formatter _
      As Softconcepts.ApplicationBlocks.RadioPattern.IFormatter) _
      Implements Softconcepts.ApplicationBlocks.RadioPattern.IListener.Listen
        If (message.Equals("chat>")) Then
            Console.Write(message)
        Else
            Console.WriteLine(formatter.ApplyFormatting(message))
        End If
    End Sub
//...

.NET Remoting and Event Handling in VB .NET, Part 3

Finally, notice that one overloaded version of Listen uses an interface called IFormatter. (Listing 7 shows the definition and some examples of IFormatter.) This interface has a single method named ApplyFormatting. Using an interface enables any class that implements IFormatter to be passed to the Listen method. The result is that the message could be formatted as HTML, XML, plain text, or any future style of formatted text without ever changing the client. This is what extensible and loosely coupled code means.

Listing 7: The IFormatter Interface and Two Formatters

Public Interface IFormatter
    Function ApplyFormatting(ByVal text As String) As String
End Interface

Public Class GenericFormatter
    Implements IFormatter

    Public Function ApplyFormatting(ByVal text As String) As String _
           Implements IFormatter.ApplyFormatting
        Return text
    End Function
End Class


Public Class TimeStampedFormatter
    Implements IFormatter

    Public Function ApplyFormatting(ByVal text As String) _
           As String Implements IFormatter.ApplyFormatting
        Return String.Format("{0}: {1}", DateTime.Now, text)
    End Function
End Class

Internationalizing with the ResourceManager

The Help command from earlier in the article reads help information from an external resource file using the ResourceManager (see the Help command in Listing 1).

.NET makes externalizing resources like text and graphics easy. Simply add an Assembly Resource File to your project and define the resources. The example used text, which can easily be added in the resource editor (see Figure 1).

The next step is to load the resources using the ResourceManager defined in System.Resources. You can load the resources by referring to the assembly name followed by the resource file name. (In the sample, the assembly is Client and the resource file is Help, so "Client.Help " is used to construct the resource manager.)

Figure 1: Using an Assembly Resource File to Externalize Resources Like Text

Invoke ResourceManager.GetString to read a string resource. (For more ResourceManager methods refer to the .NET help.)

To internationalize resources, create additional resource files with the two-character international code for the country between the resource file name and extension. For example, Help.de.resx, would represent the resource file for Germany. If your computer's language is German, that resource file would be loaded instead of the default resource file, which is the one you defined.

There isn't much more to internationalization than that in .NET. You can also create satellite assemblies post-deployment. You could add resources after you deploy your .NET application without re-deploying the whole application.

Note: The ease with which applications can support multiple languages is just one of the compelling reasons to switch to Visual Basic .NET, and I have it on good authority that 2005 is going to be even better.

Finding Goofy OOP Behavior

I read somewhere that the acronym OOP is POO in French. I won't make any O'Reilly-esque political commentary here, but OOP can be POO sometimes.

Have you ever noticed that some applications behave a bit oddly? For example, the chat application had a quirky behavior: it would place the blinking cursor on the line following the 'chat>' prompt. The reason for this is that—in a very OOP way—I reused code from the Broadcaster to display all of the text, including the prompt. For some text, I needed a new line, but the flashing cursor needed to stay on the same line as the prompt. This meant that everything used Console.WriteLine except the 'chat>' prompt, which needed to use Console.Write (no new line).

Because I originally used the Listen method to display all feedback to the client, the new line was used to write the prompt. The result was the cursor wound up in the wrong place. The problem was fixed in the implementation, but I had to add the second Listen method to fix it. Quirky behavior can happen in OOP when you try to reuse code that doesn't fit perfectly.

As a general rule, reuse code and write good OOP. When doing so results in quirky behavior, write and use something else from scratch—even if it means duplicating some behavior. Trying to find perfect inheritance, encapsulation, overloading, or realization relationships often falls into the category of diminishing returns. Sometimes it is just easier to write a one-off version of something to resolve quirky behavior.

Investigate Patterns

More frequently, I am finding that many problems are already solved and documented as patterns. I also find that patterns (and refactoring) can elevate the quality of a solution. Whether you are working on a team or as a one-person show, investigate the patterns in this article and the dozens more published in Design Patterns.

If you have a chance, pick up a copy of one of my upcoming books, UML Demystified or Expert One on One Visual Studio 2005. You will learn more about the practical design and implementation practices related to using design patterns and refactoring.

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 book Visual Basic .NET Power Coding from Addison-Wesley and his upcoming book UML DeMystified from McGraw-Hill/Osborne (Spring 2005). Paul is also the founder and chief architect for Software Conceptions, Inc, founded 1990. He is available to help design and build software worldwide. You may contact him for consulting opportunities or technology questions at pkimmel@softconcepts.com.

If you are interested in joining or sponsoring a .NET Users Group, check out www.glugnet.org.

Copyright © 2005 by Paul Kimmel. All Rights Reserved.



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • QA teams don't have time to test everything yet they can't afford to ship buggy code. Learn how Coverity can help organizations shrink their testing cycles and reduce regression risk by focusing their manual and automated testing based on the impact of change.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds