Create a GUI for an FTP Client with VB.NET, Page 3
Attach the FTP Client to the GUI
Next, you need to attach the FtpClient class library to your GUI. You can accomplish this easily from an architectural perspective by using the Observer pattern. From a practical point of view, the Observer pattern is manifested as events.
To attach the FtpClient to the GUI, declare and create an instance of the FtpClient and then define and attach event handlers to the FtpClient using the AddHandler statement. The code that demonstrates how to manually add event handlers is shown in the Form1_Load event handler in Listing 3 and in context in the Form's code listing at the end of this article.
Listing 3: The Form1_Load Method Demonstrates How to Manually Attach Event Handlers.
Private ftp As FtpClient = Nothing Private FileControl As FileListUserControl Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Broadcaster.Add(Me) ftp = New FtpClient("localhost", ".", "anonymous", _ "firstname.lastname@example.org", 21) AddHandler ftp.OnTcpEvent, AddressOf OnTcpEvent FileControl = New FileListUserControl FileControl.Caption = "Remote Files" FileControl.Parent = Panel1 Panel1.Controls.Add(FileControl) FileControl.Dock = DockStyle.Fill End Sub
(Rather than remove the code for the FileListUserControl, I show the code for dynamically creating the UserControl in Listing 3 too.) The AddHandler statement requires an object and its event field, and the second parameter is the AddressOf the event handler. Implicitly, AddressOf creates an instance of a delegate object, which is a class that encapsulates event handlers.
Update a StatusStrip
Whidbey has replaced the StatusBar with the StatusStrip. Although it is a little more clumsy to use than the StatusBar, it is more OOPy but will feel a tad foreign. Basically, the StatusStrip is a status bar that has more advanced designers similar to the MenuStrip's designers. Instead of repeating that information here, let's talk about a proper way to update status information on your presentation layer.
Status information could literally come from anywhere. The FtpClient's state will change, and you might elect to report that status information. The FileListUserControl's or Form1's status might change, and you might elect to report that status information. Obviously, you could define a method in the Form that permits updating status, but then every status reporting object would need to have an instance of the Form. That would be a spaghetti code nightmare. If you asked yourself why, consider an example using just one reporting object, the Form. Suppose the FtpClient is told about the main form in the Windows client. This means the Windows client must have a reference to the FtpClient to invoke behaviors and the FtpClient must have a reference to the Windows Form to update status. In addition to being a circular reference, the FtpClient class also breaks if Form1 goes away, and you can't reuse the FtpClient in some other application without dragging the Windows Forms client along with it. This is called tightly coupled code and it is a bad thing.
A better alternative would be to define an OnStatusChange method in the FtpClient, attach the Windows Form to that event, and raise the event when status changes. While this represents looser coupling and better cohesion, it still means that other status-reporting objects would need to follow suit with events and event handlers.
The best solution can be had by again using the Observer pattern. However, in this context, think of the Observer pattern by its alter ego publish-subscribe or what I call broadcaster and listeners. (Think radio station and radio listeners.) Defining a broadcaster and listener means that you can broadcast status messages (from anywhere) and any listener can tune it. By combining the Singleton (to ensure one instance of the broadcaster) and the Observer patterns, you can send status messages from anywhere without any knowledge of who or what is listening. (If you need some validation for this solution, keep in mind that it is precisely how Microsoft implemented the System.Diagnostics.Trace class and TraceListeners to send Trace information to the Output window in VS.NET itself.)
Implement the Listener
Listening is comprised of two parts: an interface named IListener and a typed collection of IListeners. Using an interface means that any class that implements IListener can tune in to broadcasts. You might think inheritance would work here, but remember an obvious listener is a Form and it already inherits from the System.Windows.Forms.Form and multiple inheritance is not allowed in VB.NET.
Next, your broadcaster may want to multicast. Thus, you need a means of knowing about multiple listeners; for example, you might want to log status messages in the EventLog. Such a scenario is ideally suited for a strongly typed collection of IListeners. Listing 4 shows both the IListener and ListenerCollection. (For more information about typed collections, read my book Visual Basic .NET Power Coding from Addison-Wesley, and for more information about the Observer pattern, refer to Design Patterns by Erich Gamma, et. al., also from Addison-Wesley.)
Listing 4: The IListener and ListenerCollection Classes
Public Interface IListener ReadOnly Property Listening() As Boolean Sub Listen(ByVal message As String) End Interface Imports System 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) Dim I As Integer For I = 0 To List.Count - 1 If (Item(I) Is value) Then List.RemoveAt(I) Exit For End If Next End Sub End Class
Whidbey also has added a new project item template for interfaces (see Figure 4).
Figure 4: Whidbey Supports Several More Template Items, Generating More Code for You.
Implementing the Broadcaster
The Broadcaster class uses the Singleton pattern to ensure only one Broadcaster exists. You typically implement the Singleton pattern by making the constructor non-public and using a Shared method to request an instance of the class, which is created only once. Also, the Broadcaster contains Add and Remove methods that add and remove IListeners from an internal ListenerCollection held by the Broadcaster. Listing 5 provides the implementation of the Broadcaster class.
Listing 5: The Broadcaster Piece of Your Solution Plays the Role of Subject in the Observer Pattern.
Public Class Broadcaster Private listeners As ListenerCollection = Nothing Private Shared FInstance As Broadcaster = Nothing Protected Sub New() listeners = New ListenerCollection End Sub Public Shared Sub Add(ByVal value As IListener) Instance.listeners.Add(value) End Sub Public Shared Sub Remove(ByVal value As IListener) Instance.listeners.Remove(value) End Sub Public Shared Sub Broadcast(ByVal message As String) Dim listener As IListener For Each listener In Instance.listeners If (listener.Listening) Then listener.Listen(message) End If Next 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 End Class
All that remains is referring to the assembly that contains the broadcaster and listener and beginning broadcasting and listening. To Broadcast information, call Broadcaster.Broadcast. To Listen, reference the assembly containing the Broadcaster and Listener, realize the IListener interface, and register as a listener with the Broadcaster. The Form code in Listing 6 demonstrates the mechanics of the Broadcaster and IListener in practice.
Reference: Form Code Listing
Listing 6 contains the complete listing for the form for your Windows FTP GUI. Keep in mind that generated code is placed in a *.Designer.vb file in Whidbey.
Listing 6: The Form Code for the GUI
Imports Ftp Public Class Form1 Implements IListener Private ftp As FtpClient = Nothing Private FileControl As FileListUserControl Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Broadcaster.Add(Me) ftp = New FtpClient("localhost", ".", "anonymous", _ "email@example.com", 21) AddHandler ftp.OnTcpEvent, AddressOf OnTcpEvent FileControl = New FileListUserControl FileControl.Caption = "Remote Files" FileControl.Parent = Panel1 Panel1.Controls.Add(FileControl) FileControl.Dock = DockStyle.Fill End Sub Public Sub Listen(ByVal message As String) Implements _ IListener.Listen UpdateStatus(message) End Sub Private Sub UpdateStatus(ByVal message As String) StatusStripPanel1.Text = message End Sub Public ReadOnly Property Listening() As Boolean _ Implements IListener.Listening Get Return True End Get End Property Private Sub AboutToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles AboutToolStripMenuItem.Click Dim About As String = _ "WinFTP Client" + Environment.NewLine + _ "Copyright (c) 2004. All Rights Reserved." + _ Environment.NewLine + "firstname.lastname@example.org" MessageBox.Show(About, "About WinFTP", _ MessageBoxButtons.OK, _ MessageBoxIcon.Information) End Sub Private Sub GetRemoteFilesToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles GetRemoteFilesToolStripMenuItem.Click FileControl.SetFiles(ftp.GetFileList("*.*")) End Sub Private Sub ConnectToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles ConnectToolStripMenuItem.Click ftp.Connect() ftp.Login() End Sub Private Sub DisconnectToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles DisconnectToolStripMenuItem.Click ftp.Disconnect() End Sub Private Sub ExitToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles ExitToolStripMenuItem.Click Close() End Sub Private Sub OnTcpEvent(ByVal sender As Object, ByVal e _ As TcpEventArgs) UpdateStatus(e.Data) End Sub Private Sub FileToolStripMenuItem_Click(ByVal sender _ As System.Object, ByVal e As System.EventArgs) _ Handles FileToolStripMenuItem.Click End Sub End Class
The Elements for a Complete Solution
Writing software is hard. Writing good software is especially difficult, and great software is exceedingly rare. However, great software has some common elements and one of these is the use of well-known patterns in its design and implementation.
This article has demonstrated several kinds of skills you might need to write good software. Although a complete solution would not fit in an article, what you have here are the elements that you need to implement a whole solution. For example, you used the Observer and Singleton patterns, as well as delegates (a.k.a., events) to decouple the FtpClient from any particular GUI. You extended your FtpClient a bit more and got a chance to explore some of the new controls in Whidbey. (Keep in mind that Whidbey is beta software and is subject to change.)
Paul Kimmel, the VB Today columnist, is a software architect who has written several books on .NET programming. Look for more information on patterns and software design in his upcoming book UML DeMystified from McGraw-Hill/Osborne (Spring 2005). You may contact him at email@example.com if you need assistance developing software or are interested in joining the Lansing Area .NET Users Group (glugnet.org).
Copyright © 2004 by Paul Kimmel. All Rights Reserved.