Chapter 14. The Adapter Pattern

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.

Moving Data between Lists

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 has names with times or scores.

In this simple program, shown in Figure 14-1, the program reads in the names from a roster file during initialization. To move a name to the right-hand list box, you click on the name and then click on the arrow button. To move a name from the right-hand list box to the left-hand list, click on the name and then click on the back-arrow button.

Figure 14-1. A simple program to choose names for display

image

This is a very simple program to write in C#. 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 an ArrayList collection called swdata. 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 void btClone_Click(object sender, EventArgs e) {
int i = lskids.SelectedIndex ();
  if( i >= 0) {
      Swimmer sw = swdata.getSwimmer (i);
      lsnewKids.Item.Add (sw.getName() +" "+sw.getTime ());
      lskids.SelectedIndex = -1;
  }
}

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 void putBack_Click(object sender, EventArgs e) {
      int i = lsnewKids.SelectedIndex ();
      if(i >= 0)
             lsNewKids.Items.RemoveAt (i);
}

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 here for the third name in the list.

Making an Adapter

It may be a little difficult to remember to use the Items collection of the list box for some operations but not for others. For this reason, we might prefer to have a class that hides some of these complexities and adapts the interface to the simpler one we wish we had, rather like the ListBox interface in VB6. We’ll create a simpler interface in a ListAdapter class that then operates on an instance of the ListBox class.


public class ListAdapter    {
      private ListBox listbox;   //operates on this one
      public ListAdapter(ListBox lb)          {
             listbox = lb;
      }
      //-----
      public void Add(string s) {
             listbox.Items.Add (s);
      }
      //-----
      public int SelectedIndex() {
             return listbox.SelectedIndex;
      }
      //-----
      public void Clear() {
             listbox.Items.Clear ();
      }
      //-----
      public void clearSelection() {
             int i = SelectedIndex();
             if(i >= 0) {
                    listbox.SelectedIndex =-1;
             }
      }
}

Then we can make our program a little simpler.


private void btClone_Click(object sender, EventArgs e) {
      int i = lskids.SelectedIndex ();
      if( i >= 0) {
             Swimmer sw = swdata.getSwimmer (i);
             lsnewKids.Add (sw.getName() + " " + sw.getTime ());
             lskids.clearSelection ();
      }
}

Now let’s recognize that if we are always adding swimmers and times spaced apart like this, maybe there should be a method in our ListAdapter that handles the Swimmer object directly.


public void Add(Swimmer sw) {
      listbox.Items.Add (sw.getName() + "" + sw.getTime());
}

This simplifies the Click Event handler even more.


private void btClone_Click(object sender, EventArgs e) {
      int i = lskids.SelectedIndex ();
      if( i >= 0) {
             Swimmer sw = swdata.getSwimmer (i);
             lsnewKids.Add (sw);
             lskids.clearSelection ();
      }
}

What we have done is create an Adapter class that contains a ListBox class and simplifies how you use the ListBox.  Next, we’ll see how we can use the same approach to create adapters for two of the more complex visual controls.

Using the DataGrid

To circumvent the problem with the tab columns in the simple list box, we can turn to a grid display. The grid table that comes with Visual Studio.NET is called the DataGrid. 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.


DataTable dTable = new DataTable("Kids");
dTable.MinimumCapacity = 100;
dTable.CaseSensitive = false;

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 col-umn’s data. Note that you can refer to the columns by name or by index number.


DataRow row = dTable.NewRow();
row["Frname"] = sw.getFrname();
row[1] = sw.getLName();
row[2] = sw.getAge();  //This one is an integer
dTable.Rows.Add(row);
dTable.AcceptChanges();

However, we would like to be able to use the grid without changing any of the code we used for the simple list box. We do this by creating a GridAdapter that follows that same interface.


public interface LstAdapter      {
       void Add(Swimmer sw) ;
       int SelectedIndex() ;
       void Clear() ;
       void clearSelection() ;
}

The GridAdapter class implements this interface and is instantiated with an instance of the grid.


public class GridAdapter:LstAdapter     {
      private DataGrid grid;
      private DataTable dTable;
      private int row;
      //-----
      public GridAdapter(DataGrid grd)         {
             grid = grd;
             dTable = (DataTable)grid.DataSource;
             grid.MouseDown +=
                    new System.Windows.Forms.MouseEventHandler
                    (Grid_Click);
             row = -1;
      }
      //-----
      public void Add(Swimmer sw) {
             DataRow row = dTable.NewRow();
             row["Frname"] = sw.getFrname();
             row[1] = sw.getLName();
             row[2] = sw.getAge();  //This one is an integer
             dTable.Rows.Add(row);
             dTable.AcceptChanges();
      }
      //-----
      public int SelectedIndex() {
             return row;
      }
      //-----
      public void Clear() {
             int  count = dTable.Rows.Count ;
             for(int i=0; i< count; i++) {
                    dTable.Rows[i].Delete ();
             }
      }
      //-----
      public void clearSelection() {}
}

Detecting Row Selection

The DataGrid does not have a SelectedIndex property, and the rows do not have Selected properties. Instead, you must detect a MouseDown event with a MouseEvent handler and then get the HitTest object and see if the user has clicked on a cell.


public void Grid_Click(object sender, MouseEventArgs  e) {
      DataGrid.HitTestInfo hti = grid.HitTest (e.X, e.Y);
      if(hti.Type  == DataGrid.HitTestType.Cell ){
             row = hti.Row ;
      }
}

Note that we can now simply call the GridAdapter class’s Add method when we click on the → button, regardless of which display control we are using.


private void btClone_Click(object sender, System.EventArgs e) {
      int i = lskids.SelectedIndex ();
      if( i >= 0) {
             Swimmer sw = swdata.getSwimmer (i);
             lsNewKids.Add (sw);
             lskids.clearSelection ();
      }
}

We see the final grid display in Figure 14-2.

Figure 14-2. A grid adapter

image

Using a TreeView

If, however, you choose to use a TreeView control to display the data you select, you will find that there is no convenient interface that you can use to keep your code from changing.

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.


public class TreeAdapter:LstAdapter     {
      private TreeView tree;
      //------
      public TreeAdapter(TreeView tr)           {
             tree=tr;
      }
      //------
      public void Add(Swimmer sw) {
             TreeNode nod;
       //add a root node
             nod = tree.Nodes.Add(sw.getName());
       //add a child node to it
       nod.Nodes.Add(sw.getTime().ToString ());
       tree.ExpandAll ();
      }
      //------
      public int SelectedIndex() {
     return tree.SelectedNode.Index ;
}
//------
public void Clear() {
       TreeNode nod;
       for (int i=0; i< tree.Nodes.Count ; i++) {
              nod = tree.Nodes [i];
              nod.Remove ();
       }
}
//------
public void clearSelection() {}
}

The TreeDemo program is shown in Figure 14-3.

Figure 14-3. The TreeDemo program

image

The Class Adapter

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. In this class adapter example, we create a new class called MyList, which is derived from the ListBox class and that implements the following interface.


public interface ListAdapter {
      void Add(Swimmer sw) ;
      void Clear() ;
      void clearSelection() ;
}

The derived MyList class is as follows.


public class MyList : System.Windows.Forms.ListBox, ListAdapter {
      private System.ComponentModel.Container components = null;
  //-----
      public MyList()            {
             InitializeComponent();
      }
      //-----
      public void Add(string s) {
             this.Items.Add (s);
      }
      //-----
      public void Add(Swimmer sw) {
             this.Items.Add (sw.getName() +
                    " " + sw.getAge ().ToString () );
      }
      //-----
      public void Clear() {
             this.Items.Clear ();
      }
      //-----
      public void clearSelection() {
             this.SelectedIndex = -1;
      }

The class diagram is shown in Figure 14-4. The remaining code is much the same as in the object adapter version.

Figure 14-4. The class adapter approach to the list adapter

image

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.

The class adapter lets the adapter change some of the adapted class’s methods but still allows the others to be used unchanged.

The object adapter could allow subclasses to be adapted by simply passing them in as part of a constructor.

The object adapter requires that you specifically bring any of the adapted object’s methods to the surface that you wish to make available.

Two-Way Adapters

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 DataGrid. This is most easily done with 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.

Object versus Class Adapters in C#

The C# List, Tree, and Grid adapters illustrated previously 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 add 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.

Pluggable Adapters

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 set-Parameter methods.

Thought Question

How would you go about writing a class adapter to make the DataGrid look like a two-column list box?

Programs on the CD-ROM

image

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset