The Adapter pattern is used to convert the programming interface of one class into that of another. We use adapters whenever we want unrelated classes to work together in a single program. The concept of an adapter is thus pretty simple: We write a class that has the desired interface and then make it communicate with the class that has a different interface.
There are two ways to do this: by inheritance and by object composition. In the first case, we derive a new class from the nonconforming one and add the methods we need to make the new derived class match the desired interface. The other way is to include the original class inside the new one and create the methods to translate calls within the new class. These two approaches, called class adapters and object adapters, are both fairly easy to implement in other languages, but before VB7 you were forced to use object composition preferentially, since inheritance was not available.
Let's consider a simple program that allows you to select some names from a list to be transferred to another list for a more detailed display of the data associated with them. Our initial list consists of a team roster, and the second list the names plus their times or scores.
In this simple program, shown in Figure 15-1, the program reads in the names from a roster file during initialization. To move names to the right-hand list box, you click on them and then click on the right arrow button. To remove a name from the right-hand list box, click on it and then on the left arrow button. This moves the name back to the left-hand list.
This is a very simple program to write in VB. It consists of the visual layout and action routines for each of the button clicks. When we read in the file of team roster data, we store each child's name and score in a Swimmer object and then store all of these objects in a collection. When you select one of the names to display in expanded form, you simply obtain the list index of the selected child from the left-hand list and get that child's data to display in the right-hand list.
Private Sub Moveit_Click() Dim i As Integer i = lsKids.ListIndex + 1 If i > 0 And i <= swmrs.Count Then Set sw = swmrs(i) lsTimes.AddItem sw.getName & vbTab & str$(sw.getTime) End If End Sub
In a similar fashion, if we want to remove a name from the right-hand list, we just obtain the selected index and remove the name.
Private Sub putback_Click() Dim i As Integer i = lsTimes.ListIndex If i >= 0 Then lsTimes.RemoveItem i End If End Sub
Note that we obtain the column spacing between the two rows using the tab character. This works fine as long as the names are more or less the same length. However, if one name is much longer or shorter than the others, the list may end up using a different tab column, which is what happened for the third name in the list.
To circumvent this problem with the tab columns in the simple list box, we might turn to a grid display. One simple grid that comes with VB is called the MSFlexGrid. It is a subset of a more elaborate control available from a third-party vendor. The MSFlexGrid has Rows and Col properties that you can use to find out its current size. Then you can set the Row and Col properties to the row and column you want to change and use the Text property to change the text in the selected cell of the grid.
Private Sub Movetogrid_Click() Dim i As Integer, row As Integer i = lsKids.ListIndex + 1 If i > 0 And i <= swmrs.Count Then Set sw = swmrs(i) grdTimes.AddItem "" row = grdTimes.Rows grdTimes.row = row - 1 grdTimes.Col = 0 grdTimes.Text = sw.getName grdTimes.Col = 1 grdTimes.Text = Str$(sw.getTime) End If End Sub
However, we would like to be able to use the grid without changing our code at all from what we used for the simple list box. It turns out that you can do that because the AddItem method of the MSFlexGrid interprets tab characters in an analogous fashion to the way the list box does.
The following statement
grdTimes.AddItem sw.getName & vbTab & Str$(sw.getTime)
works the same as the seven lines of code do that we showed in the previous example, and the resulting display will put the names in one column and the scores in the other, as shown in Figure 15-2.
In other words, the MSFlexGrid control provides the same programming interface as a convenience and is in fact its own Adapter between the list box and the MSFlexGrid control.
In fact, since the list and grid have the same programming interface, it is quite easy to write a private subroutine to add the data to either of them.
Private Sub addText(ctl As Control, sw As Swimmer) ctl.AddItem sw.getName & vbTab & str$(sw.getTime) End Sub
Then we could write the button click routines so they each call this method using a different list as an argument.
Private Sub Moveit_Click() Dim i As Integer i = lsKids.ListIndex + 1 If i > 0 And i <= swmrs.Count Then Set sw = swmrs(i) addText lsTimes, sw End If End Sub '------ Private Sub Movetogrid_Click() Dim i As Integer, row As Integer i = lsKids.ListIndex + 1 If i > 0 And i <= swmrs.Count Then Set sw = swmrs(i) addText grdTimes, sw End If End Sub
However, this is clearly not very object oriented. The addText method really should be part of the class we are using. We shouldn't have to pass an instance of the list or grid into a method in the same class. Now in VB6 and before, there is no way to add methods to a control. Instead, we can create a simple Control Adapter class that will handle both the grid and the list and contain the addText method we wrote as a simple subroutine above. This class is the following.
'Class ControlAdapter Private ctrl As Control Public Sub init(ctl As Control) Set ctrl = ctl 'copy control into class End Sub '----- Public Sub addText(sw As Swimmer) 'add new line to list or grid ctrl.AddItem sw.getName & vbTab & str$(sw.getTime) End Sub
We initialize this class with an instance of a list or grid in the Form_Load event.
Private grdAdapt As New ControlAdapter 'pass grid into Control Adapter grdAdapt.init grdTimes
Then we can simply call the class's addText method when we click on the right arrow button, regardless of which display control we are using.
Private Sub Moveit_Click() Dim i As Integer i = lsKids.ListIndex + 1 If i > 0 And i <= swmrs.Count Then Set sw = swmrs(i) grdAdapt.addText sw End If End Sub
If, however, you choose to use a TreeView control to display the data you select, you will find that there is no conveniently adapted interface that you can use to keep your code from changing. Thus, our convenient ControlAdapter class can not be used for the TreeView. Instead, we need to write a new TreeAdapter class that has the same interface but carries out the adding of a line to the tree correctly.
TheTreeView class contains a Nodes collection to which you add data by adding a node, setting its text, and defining whether it is a child node. Child nodes are related to the index of the parent node. The following code adds a parent node and then adds a child node to it.
'Class TreeAdapter Private Tree As TreeView Public Sub init(tr As TreeView) Set Tree = tr End Sub Public Sub addText(sw As Swimmer) Dim scnt As String, nod As Node scnt = Str$(Tree.Nodes.Count) Set nod = Tree.Nodes.Add(, tvwNext, "r" & _ sw.getName, sw.getName) Tree.Nodes.Add "r" & sw.getName, tvwChild, , Str$(sw.getTime) nod.Expanded = True End Sub
We illustrate our TreeView program in Figure 15-3.
In the object adapter approach, (Figure 15-4), we create a class that contains a List Box class but that implements the methods of the ControlAdapter interface. This is the approach we took in the preceding example.
Adapters can be even more powerful in VB7. Let's first consider the VB7 ListBox itself. This control has rather different methods from the VB6 list box, and we might very well want to hide this difference by using an adapter so that the methods that are brought to the surface appear to be the same.
In VB7, you add a line to a list box by adding a String to the listbox's Items collection.
list.Items.Add(s)
The ListIndex property is replaced by the SelectedIndex property. So we could easily write a simple wrapper class that translates these methods into the ListBox methods for VB7. The beginning of a ListAdapter class has a ListBox instance in its constructor and saves that instance within the class, thus using encapsulation or object composition.
Public Class ListAdapter 'Adapter for ListBox emulating some of 'the methods of the VB6 list box. Private List As ListBox 'instance of list box '-------- Public Sub New(ByVal ls As ListBox) List = ls End Sub '-------- Public Sub addItem(ByVal s As String) list.Items.Add(s) 'add into list box End Sub '-------- Public Function ListIndex() As Integer Return list.SelectedIndex 'get list index End Function '-------- Public Sub addText(ByVal sw As Swimmer) List.Items.Add(sw.getName & vbTab & sw.getTime.ToString) End Sub End Class
However, in the program we have been discussing, we want to display the name of the swimmer and the swimmer's time. Thus, it is convenient to add a method that takes a Swimmer object as an argument and puts the name and time in the list box.
The code that reads the swimmers in from the file and loads their names into the left-hand list box also uses an instance of the ListAdapter. Here are declarations and initialization.
Private lsAdapter, ksAdapter As ListAdapter lsAdapter = New ListAdapter(lsNames) ksAdapter = New ListAdapter(lsKids)
This simple routine reads in the lines of data.
Private Sub ReadFile() Dim s As String Dim sw As Swimmer Dim fl As New vbFile("swimmers.txt") fl.openForRead() s = fl.readLine While Not fl.fEof sw = New Swimmer(s) swimmers.add(sw) ksAdapter.addItem(sw.getName) s = fl.readLine End While End Sub
The running program is shown in Figure 15-5.
The TreeView class in VB7 is only slightly different from that in VB6. For each node you want to create, you create an instance of the TreeNode class and add the root TreeNode collection to another node. In our example version using the TreeView, we'll add the swimmer's name to the root node collection and the swimmer's time as a subsidiary node. Here is the entire TreeAdapter class. Note that we only need implement the addText method.
Public Class TreeAdapter 'An adapter to use TreeView 'instead of list boxes Private Tree As TreeView 'instance of tree Public Sub New(ByVal tr As TreeView) Tree = tr End Sub '------------- Public Sub addText(ByVal sw As Swimmer) Dim scnt As String Dim nod As TreeNode 'add a root node nod = Tree.Nodes.add(sw.getName) 'add a child node to it nod.Nodes.add(sw.getTime.toString) Tree.expandAll() End Sub End Class
The TreeDemo program is shown in Figure 15-6.
The VB7 DataGrid control is considerably more elaborate than the MSFlexGrid control in VB6. It can be bound to a database or to an in-memory data array. To use the DataGrid without a database, you create an instance of the DataTable class and add DataColumns to it. DataColumns are by default of String type, but you can define them to be of any type when you create them. Here is the general outline of how you create a DataGrid using a DataTable.
dTable = New DataTable("Kids") dTable.MinimumCapacity = 100 dTable.CaseSensitive = False Dim column As DataColumn column = New DataColumn("Frname", _ System.Type.GetType("System.String")) dTable.Columns.Add(column) column = New DataColumn("Lname", _ System.Type.GetType("System.String")) dTable.Columns.Add(column) column = New DataColumn("Age", _ System.Type.GetType("System.Int16")) dTable.Columns.Add(column) DGrid.DataSource = dTable DGrid.CaptionVisible = False 'no caption DGrid.RowHeadersVisible = False 'no row headers DGrid.EndInit()
To add text to the DataTable, you ask the table for a row object and then set the elements of the row object to the data for that row. If the types are all String, then you copy the strings, but if one of the columns is of a different type, such as the integer age column here, you must be sure to use that type in setting that column's data.
The complete GridAdapter class fills in each row in this fashion.
Public Class GridAdapter Private dtable As DataTable Private Dgrid As DataGrid '----- Public Sub New(ByVal grid As DataGrid) dtable = CType(grid.DataSource, DataTable) dgrid = grid End Sub '----- Public Sub addText(ByVal sw As Swimmer) Dim scnt As String Dim row As DataRow row = dtable.NewRow row("Frname") = sw.getFirstName row(1) = sw.getLastName row(2) = sw.getAge 'This one is an integer dtable.Rows.Add(row) dtable.AcceptChanges() End Sub End Class
Note that you can refer to each column either by numeric position or by name. The running program is shown in Figure 15-7.
In the class adapter approach, we derive a new class from Listbox (or the grid or tree control) and add the desired methods to it. This is possible in VB7 but not in earlier versions of Visual Basic. In the class adapter example on the CD-ROM, we create a new class called OurList which is derived from the Listbox class and which implements the following interface:
Public Interface ListAdapter 'Interface Adapter for ListBox emulating some of 'the methods of the VB6 list box. Sub addItem(ByVal s As String) Function ListIndex() As Integer Sub addText(ByVal sw As Swimmer) End Interface
The class diagram is shown in Figure 15-8. The remaining code is much the same as in the object adapter version.
There are also some differences between the class and the object adapter approaches, although they are less significant than in C++.
The class adapter
Won't work when we want to adapt a class and all of its subclasses, since you define the class it derives from when you create it.
Lets the adapter change some of the adapted class's methods but still allows the others to be used unchanged.
An object adapter
Could allow subclasses to be adapted by simply passing them in as part of a constructor.
Requires that you specifically bring any of the adapted object's methods to the surface that you wish to make available.
The two-way adapter is a clever concept that allows an object to be viewed by different classes as being either of type ListBox or type MSFlexGrid. This is most easily carried out using a class adapter, since all of the methods of the base class are automatically available to the derived class. However, this can only work if you do not override any of the base class's methods with any that behave differently.
The VB.NET List, Tree, and Grid adapters we previously illustrated are all object adapters. That is, they are all classes that contain the visual component we are adapting. However, it is equally easy to write a List or Tree Class adapter that is derived from the base class and contains the new addText method.
In the case of the DataGrid, this is probably not a good idea because we would have to create instances of DataTables and Columns inside the DataGrid class, which makes one large complex class with too much knowledge of how other classes work.
A pluggable adapter is one that adapts dynamically to one of several classes. Of course, the adapter can only adapt to classes it can recognize, and usually the adapter decides which class it is adapting based on differing constructors or setParameter methods.
In a broad sense, there are already a number of adapters built into the VB7 language to allow for compatibility with VB6. These wrap new functions in the API of the older ones in much the same way we did for the Listbox previously.
How would you go about writing a class adapter to make the Grid look like a two-column list box?