Java Programming, Lecture Notes # 210, Revised 12/16/98.
Students in Prof. Baldwin's Advanced Java Programming classes at ACC will be responsible for knowing and understanding all of the material in this lesson beginning with the Spring semester of 1999.
This lesson was originally written on October 3, 1998, using the JDK 1.1.6 download package along with Swing 1.0.3. It was upgraded to JDK 1.2 on 12/16/98. The purpose of this lesson is to illustrate the use of MVC models with Swing components.
The material in this lesson assumes that you have studied my earlier lesson on Implementing The Model-View-Controller Paradigm using Observer and Observable as well as my earlier lesson on using the JList class to create simple lists. Of course, it also assumes that you know just about all there is to know about the delegation event model and other similar Intermediate level material.
You may have read that Swing components are designed around a modified version of Model-View-Control (MVC) in which the view and the control are combined into an object called a delegate. Delegates represent the model to the user (as a view normally does) and also translate user input into the model (as a control normally does). As such, the Swing approach is based on a model-delegate paradigm instead of a true MVC paradigm.
Swing gives you the ability to control how widgets look, how they respond to input, and in some cases, how their data is represented. This third capability involving data representation is the primary topic of this lesson. We will see how, with some of the Swing components, you have control over the model used to maintain the data. The other two topics are covered in other lessons
I will use an object of the JList class as the vehicle for this discussion, although the general concepts discussed here apply to several of the more complex Swing components including:
Swing widgets, such as JList, are subclasses of JComponent. At any given point in time, a JComponent object is associated with a single data model and a single delegate. (Actually, some components are associated with models of other types, such as list selection models, which manage the rules of selection within the list. A discussion of these other model types is beyond the scope of this lesson and I will defer that discussion for a subsequent lesson.)
To serve as a data model for a particular JComponent, an object must be of a class that implements a model interface specific to that component. For example, for an object of a class to serve as a model for a JList, the class from which the object is instantiated must implement the ListModel interface.
Most (and probably all) Swing components provide a default model. For example, earlier lessons in this series showed how to instantiate and use JTree objects and JList objects using their default data models.
The default model class for the JList class is the DefaultListModel class. The class hierarchy is as follows:
the AbstractListModel class implements the ListModel interface and the DefaultListModel class extends the AbstractListModel class to provide additional functionality.
You have at least three options for defining a class whose objects can serve as data models for the JList class. You can define a class that implements the ListModel interface to provide an alternative list model. You can also extend the AbstractListModel class to create an alternative list model with less effort than implementing the ListModel interface. In this lesson, I create an alternative list model by extending the DefaultListModel class which provides most of the functionality of the new model and produces a new model with minimal effort.
The ListModel interface declares four methods, so at first glance, it doesn't appear to be too overwhelming. The methods are listed below.
|
Note, however, that the first two methods encompass a requirement maintain a list of registered listeners and to notify those listeners whenever a change in the data occurs. This is no small task. It requires knowledge of the ListDataListener interface and the ListDataEvent class for instantiating and passing listener objects when an event of this type occurs.
(Note that this not the type of event that I serviced in the sample program in this lesson. In this lesson, I serviced a selection event on the JList object. As you can see, one type of event can be registered on the data model, and a different type of event can be registered on the JList object.)
The ListDataListener interface declares the following three methods. This requires the model object to recognize events of these types and to invoke the corresponding method on each registered listener when an event occurs.
|
Whenever an event of one of these types occurs, the model object must instantiate a ListDataEvent object and pass it to each of the registered listeners when their method is invoked. There are symbolic constants available for the three types of events, which correspond to the listener methods listed above. In addition to the type, the constructor for the ListDataEvent requires information about the index values involved in the change in the data, and has the following signature:
public ListDataEvent(java.lang.Object source, int type, int index0, int index1) |
The ListDataEvent class defines the following methods which make it possible for the code in the event handlers to extract the information from the incoming object.
|
Fortunately, we can avoid having to deal with the complexity of creating and maintaining a list of registered listener objects, and invoking listener methods on those objects, by extending the AbstractListModel to create our alternative data model. The methods defined by AbstractListModel are shown below
|
By extending this class, we gain the benefit of these five methods. We can completely defer the creation and maintenance of the list of registered listeners to the two inherited methods at the beginning of the above list. We still must recognize when an event occurs, and invoke the correct event firing method on the superclass of our extended class, but that is much simpler than processing the list and invoking the correct method on every listener object on the list. Invoking the event firing method on the superclass causes the code in the superclass to use our parameters to construct the event object, and to notify all of the registered listeners of the occurrence of the event by invoking the appropriate method on all of those listener objects.
As mentioned earlier, there is a default model class available for JList. It is the DefaultListModel class. This class is designed to provide many useful methods for maintaining the data in the JList object. The methods defined by DefaultListModel are shown below.
The class includes these methods plus the methods inherited from the AbstractListModel class described above. I show these methods here to emphasize that a class that serves as a data model for a JList object needs a lot of data handling capability. If you were to completely define your own data model class, this is a sampling of the types of methods that you would probably need to provide. In addition, you would need to provide the container for the data which would be manipulated by these methods.
|
However, things aren't necessarily as bad as they may seem. If you compare these methods with the methods in the java.util.Vector class, you will find many similarities. This suggests that you might be able to use a Vector object as your data container, and define many of your methods simply as pass-through methods which invoke methods having the same functionality on the Vector container. Of course, you would probably need to invent some new ones. Otherwise, there wouldn't be any need to define your own model. You could simply use the default model.
The name of this sample program is SwingList02.java. The purpose the program is to show you how to define and use your own alternative data model with a JList object. It is designed simply as a vehicle for explaining code, and is not intended to be worthwhile for any other purpose.
The program also illustrates the use of the ListSelectionListener interface to instantiate listener objects that listen for the selection of an element in the list, retrieve the index value of the selected element, and display that index value.
This program populates two JList objects with identical data. The data used to populate the two lists is obtained from the names of the files and directories in the root directory of the C-drive. Therefore, the list will typically be fairly long. One of the objects is based on the DefaultListModel. The other object is based on a CustomListModel of my own design.
The two list models are identical except for their ability to remove elements from the list based on an index value. They both have a method named removeElementAt(int index). Both models will throw an exception if you attempt to remove an element from an empty list. They will also throw an exception if you attempt to remove an element with an invalid index in the associated text field.
For the DefaultListModel, this method removes the element having the specified index from the list. However, for the CustomListModel, this method removes the element from the list having an index that is one less than the specified index (unless the specified index is 0, in which case it removes the element with the specified index).
The purpose is not to provide a confusing way to remove elements from the list, but rather to illustrate that the management of the data for a list is accomplished by a list model object, and the list model object for one list can be radically different from the list model object for another list, even though both lists are of type JList and are otherwise identical.
As mentioned earlier, the data to populate the two lists is automatically obtained from the file and directory names in the root directory on the C-drive. When the program starts, the two populated lists, two buttons, and two text fields appear on the screen. One list should be above the other, and the buttons and text fields should appear to the right of the list with which they are associated.
To remove an element from a list, enter the index of the element in the associated text field and click the associated button.
To determine the index value of an element, select it with the mouse or an arrow key. The index number will be placed in the text field. Clicking the associated button at that point will cause removal to take place according to the rules described above. You can also manually enter an index value into the text field.
This program was tested using JDK 1.1.6 and Swing 1.0.3 under Win95. It was also tested using JDK 1.2 under Win95.
I will discuss a number of interesting code fragments from the program. You can view the remainder of the code in the full listing of the program that follows later.
The first fragment shows the beginning of the controlling class which extends JFrame and declares several instance variables used later in the program. I present this fragment here simply to familiarize you with the names and types of the instance variables.
public class SwingList02 extends JFrame{ JList listA; JList listB; JScrollPane scrollPaneA; JScrollPane scrollPaneB; MyListSelectionListener listSelectionListenerA; MyListSelectionListener listSelectionListenerB; JPanel panelA = new JPanel(); JPanel panelB = new JPanel(); JButton buttonA; JButton buttonB; JTextField fieldA; JTextField fieldB; Dimension prefSize; //The following is a reference to the top-level JFrame // which contains everything else. SwingList02 thisObj; |
The next fragment is the main() method which is provided so that you can run this program as an application. It simply instantiates an object of the controlling class.
public static void main(String args[]) { //Instantiate the top-level JFrame object. new SwingList02(); }//end main |
That brings us to the constructor. I have omitted the code for that portion of the constructor which creates and populates a Vector object with all the file and directory names in the root directory of the C-drive. In case you don't know how to do this, the code is shown in the program listing later in the lesson.
public SwingList02 () {//constructor getContentPane().setLayout(new FlowLayout()); //Create a Vector object containing data for // populating two JList objects. Vector theFileNames = new Vector(); //Code to populate the Vector omitted |
The JList class has four constructors. Two of them accept an array or a vector as a parameter and use a default data model which gets populated with the data from the array or the vector. I'm not certain what data model is used, but my research indicates that it doesn't support all of the methods of the DefaultListModel. Another constructor builds a new JList object with no data model. The fourth allows you to specify the data model as a parameter. After confirming that the vector contains valid data, the following fragment instantiates the two JList objects, assigning their reference to listA and listB.
Note that in both cases, I used the constructor that allows for specification of an alternative data model as a parameter to the constructor. In the case of listA, I specified the DefaultListModel. In the case of listB, I specified the CustomListModel. Possibly, I could have used one of the other constructors in the first case (if I understood its data model), but I wanted to maintain total parallelism between the two.
After instantiating the two JList objects, I assigned unique names to them that are used later to distinguish between them in an event handler.
if(theFileNames != null){ //Create and populate two JList objects listA = new JList(new DefaultListModel()); listB = new JList(new CustomListModel()); //Give them names to be used in event handler listA.setName("MyNameIsListA"); listB.setName("MyNameIsListB"); |
The next fragment uses enumeration of the Vector object along with methods of the JList and model classes to transfer the data from the Vector object to each of the JList objects. If you aren't familiar with enumeration, you can read about it in an earlier lesson.
It is important to note here that I did not add data elements to the JList objects. Rather, I invoked the getModel() method on each of the JList objects to gain access to their model objects, and added to data to the model objects by invoking addElement() method on each of them. This is the first real clue that the two model objects are really managing the data for the two JList objects.
Enumeration theEnum = theFileNames.elements(); while(theEnum.hasMoreElements()){ Object theObject = theEnum.nextElement(); ((DefaultListModel)listA.getModel()). addElement(theObject); ((CustomListModel)listB.getModel()). addElement(theObject); }//end while loop |
At this point, each JList object has been instantiated, and the data model associated with the JList object has been populated. It's time to display each of the JList objects along with a button and a textfield that can be used to remove elements from the list.
The next fragments puts listA in a JScrollPane object, and puts the scroll pane in a JPanel object.
Then it instantiates a ListSelectionListener object and registers it on the JList object. This is plain vanilla delegation event model stuff.
After this, it sets the height (visible row count) of the JList object to six rows, and specifies that only one element in the list can be selected at any one time. (There are options for multiple item selection as well but I didn't use them.) Since I didn't specify the width of the cells containing the elements in the list, the width of the list is dictated by the longest file name in the list.
scrollPaneA = new JScrollPane(listA); panelA.add(scrollPaneA); listSelectionListenerA = new MyListSelectionListener(); listA.addListSelectionListener( listSelectionListenerA); listA.setVisibleRowCount(6); //Allow selection of one element index at a time. listA.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); |
I placed the scroll pane containing the list in a JPanel object and placed the panel in a JFrame. At this point, I needed to set the size of the panel to match the size of the scroll pane containing the list. I used the getPreferredScrollableViewportSize() method on the list to obtain a Dimension object specifying the size of the list. I used this to setBounds() on the panel as shown in the next fragment. Then I added the entire assembly to the content pane of the JFrame.
prefSize = listA.getPreferredScrollableViewportSize(); panelA.setBounds(0,0,prefSize.width,prefSize.height); getContentPane().add(panelA); |
I needed a button and a text field for the user to remove elements from the list by specifying the index and clicking the button. The code to create these components is shown in the fragment that follows.
This fragment also shows the instantiation and registration of an anonymous ActionListener object on the button. The behavior of the actionPerformed() method is to extract the index value for an element from the text field and invoke the removeElementAt(int index) method on the data model. Again, note that the method is being invoked on the data model object, and not on the JList object, because it is the model object that manages the data.
As discussed earlier, the behavior of this method differs between the two data models.
buttonA = new JButton("A"); getContentPane().add(buttonA); fieldA = new JTextField("Field A"); getContentPane().add(fieldA); //Listener to remove elements from listA buttonA.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int theElement = Integer.parseInt(fieldA.getText()); ((DefaultListModel)listA.getModel()). removeElementAt(theElement);}}); //Display listB, code omitted here //Omit routine JFrame handling code here |
The code to display listB along with its button and text field is essentially the same as for listA, so I won't discuss it here. I also omitted some routine code that handles the outer JFrame object at this point. You can view it in the program listing later.
The next fragment is the key to this entire lesson. It is a class used to implement a custom list model that behaves the same as the DefaultListModel in all respects but one. This class extends the DefaultListModel class and therefore gains all the capability of that class. However, it overrides the method named removeElementAt(int index) to cause it to exhibit different behavior. The overridden behavior causes the element whose index value is immediately below the specified index value to be removed from the list, except in the case where the specified index is zero. In that case, it causes the element with the specified index to be removed.
As you can see, this behavior is implemented by subtracting one from the specified index value and passing the modified index to the same method in the superclass.
class CustomListModel extends DefaultListModel{ public void removeElementAt(int index){ if(index != 0){ super.removeElementAt(index-1); }else{ super.removeElementAt(index); }//end else }//end overridden removeElementAt() }//end class CustomListModel |
This is followed by the definition of a class which implements the ListSelectionListener interface. Listener objects of this type are registered on the lists to determine the index of selected list elements and to deposit that index value in the textfield discussed previously. Although the syntax of this code is rather messy, you shouldn't find any concepts here that are not already familiar to you. This is plain vanilla delegation event model material. Therefore, I won't provide any further discussion at this point. The listing of the program that follows later contains additional comments explaining why I did some of the things that I did in this class definition.
class MyListSelectionListener implements ListSelectionListener{ public void valueChanged(ListSelectionEvent e){ int selectedIndex = ((JList)e.getSource()).getSelectedIndex(); if(((Component)e.getSource()).getName(). compareTo("MyNameIsListA") == 0){ if(selectedIndex < 0) listA.setSelectedIndex(0); else fieldA.setText("" + selectedIndex); }else{ if(selectedIndex < 0) listB.setSelectedIndex(0); else fieldB.setText("" + selectedIndex); }//end if }//end valueChanged() }//end class MyListSelectionListener |
The code in the program that was not highlighted in the fragments above can be viewed in the complete listing of the program that follows in the next section.
/*File SwingList02.java Rev 12/16/98 The purpose of this program is to illustrate the use of a custom list model with a Swing JList component. It also illustrates the use of the ListSelectionListener interface to instantiate listener objects that monitor for the selection of an element in the list, retrieve the index value of the element, and display the value. This program should be discussed only after the student understands how to create and use JList objects as described in SwingList01.java. This program populates two JList objects with identical data. The data used to populate the two lists is obtained from the names of the files and directories in the root directory of the C-drive. One of the objects is based on the DefaultListModel. The other object is based on a CustomListModel. The two list models are identical except for their ability to remove elements from the list. They both have a method named removeElementAt(int index). (Both models will throw an exception if you attempt to remove an element from an empty list.) For the DefaultListModel, this method removes the element having the specified index from the list. However, for the CustomListModel, this method removes the element from the list having an index that is one less than the specified index (unless the index is 0, in which case it removes the element with the specified index). The purpose is not to provide a confusing way to remove elements from the list, but rather to illustrate that the management of the data for a list is accomplished by a list model object, and the list model object for one list can be radically different from the list model object for another list. The data to populate the two lists is automatically obtained from the root directory on the C-drive. When the program starts, the two populated lists, two buttons, and two text fields appear on the screen. The buttons and text fields should appear to the right of the list with which they are associated. To remove an element from a list, enter the index in the associated text field and click the associated button. To determine the index value of an element, simply select it with the mouse or an arrow key. The index number will be placed in the text field. Clicking the associated button at that point will cause removal to take place according to the rules described above. You can also manually enter an index value into the text field. (The program runs under JDK 1.1.6 whether compiled using the JDK or the Microsoft jvc. However, it won't run under my current version of Microsoft jview even when compiled using jvc. It throws an exception in the area of populating the Vector object.) Tested using JDK 1.1.6 and Swing 1.0.3 under Win95. It was also tested using JDK 1.2 under Win95. **********************************************************/ import java.io.*; import java.util.*; import java.awt.*; import java.awt.event.*; //import com.sun.java.swing.*;//jdk 1.1 version //import com.sun.java.swing.tree.*;//jdk 1.1 version //import com.sun.java.swing.event.*;//jdk 1.1 version import javax.swing.*;//jdk 1.2 version import javax.swing.tree.*;//jdk 1.2 version import javax.swing.event.*;//jdk 1.2 version //=======================================================// public class SwingList02 extends JFrame{ JList listA; JList listB; JScrollPane scrollPaneA; JScrollPane scrollPaneB; MyListSelectionListener listSelectionListenerA; MyListSelectionListener listSelectionListenerB; JPanel panelA = new JPanel(); JPanel panelB = new JPanel(); JButton buttonA; JButton buttonB; JTextField fieldA; JTextField fieldB; Dimension prefSize; //The following is a reference to the top-level JFrame // which contains everything else. SwingList02 thisObj; //-----------------------------------------------------// public static void main(String args[]) { //Instantiate the top-level JFrame object. new SwingList02(); }//end main //-----------------------------------------------------// public SwingList02 () {//constructor getContentPane().setLayout(new FlowLayout()); //Create a Vector object containing data for // populating two JList objects. Vector theFileNames = new Vector(); String dir = "c:/"; String[] fileList = new File(dir).list();//dir listing //Loop and process each file in the directory for(int fileCnt = 0;fileCnt<fileList.length;fileCnt++){ if(new File( dir + "/" + fileList[fileCnt]).isDirectory()){ theFileNames.addElement("dir: " + fileList[fileCnt]); }else{ theFileNames.addElement("file: " + fileList[fileCnt]); }//end else }//end for loop if(theFileNames != null){ //Create and populate two JList objects listA = new JList(new DefaultListModel()); listB = new JList(new CustomListModel()); //Give them names to be used in event handler listA.setName("MyNameIsListA"); listB.setName("MyNameIsListB"); //Populate the list Enumeration theEnum = theFileNames.elements(); while(theEnum.hasMoreElements()){ Object theObject = theEnum.nextElement(); ((DefaultListModel)listA.getModel()). addElement(theObject); ((CustomListModel)listB.getModel()). addElement(theObject); }//end while loop //Display listA, its button, and its textField scrollPaneA = new JScrollPane(listA); panelA.add(scrollPaneA); listSelectionListenerA = new MyListSelectionListener(); listA.addListSelectionListener( listSelectionListenerA); listA.setVisibleRowCount(6); //Allow selection of one element index at a time. listA.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); //Get size of list and adjust JPanel accordingly prefSize = listA.getPreferredScrollableViewportSize(); panelA.setBounds(0,0,prefSize.width,prefSize.height); getContentPane().add(panelA); buttonA = new JButton("A"); getContentPane().add(buttonA); fieldA = new JTextField("Field A"); getContentPane().add(fieldA); //Listener to remove elements from listA buttonA.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int theElement = Integer.parseInt(fieldA.getText()); ((DefaultListModel)listA.getModel()). removeElementAt(theElement);}}); //Display listB scrollPaneB = new JScrollPane(listB); panelB.add(scrollPaneB); listSelectionListenerB = new MyListSelectionListener(); listB.addListSelectionListener( listSelectionListenerB); listB.setVisibleRowCount(6); //Allow selection of one element index at a time. listB.setSelectionMode(ListSelectionModel. SINGLE_SELECTION); //Get size of list and adjust JPanel accordingly prefSize = listB.getPreferredScrollableViewportSize(); panelB.setBounds(0,0,prefSize.width,prefSize.height); getContentPane().add(panelB); buttonB = new JButton("B"); getContentPane().add(buttonB); fieldB = new JTextField("Field B"); getContentPane().add(fieldB); //Listener to remove elements from listA buttonB.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int theElement = Integer.parseInt(fieldB.getText()); ((CustomListModel)listB.getModel()). removeElementAt(theElement);}}); }//end if(theFileNames != null) //Save a reference to the top-level JFrame object // in an instance variable for later use. thisObj = this; setTitle("Copyright 1998, R.G.Baldwin"); setSize(400,350); setVisible(true); //An anonymous inner class to terminate the program // when the // user clicks the close button on the frame. this.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0);} }); }//end constructor //=====================================================// //Inner class to implement a custom list model that // behaves differently from the DefaultListModel. This // model responds to requests to remove an element on // the basis of the index value, but actually removes // the element below that one in index value unless the // specified index value is zero. class CustomListModel extends DefaultListModel{ //Override the method to remove element. Offset // specified index value by -1. public void removeElementAt(int index){ if(index != 0){ super.removeElementAt(index-1); }else{ super.removeElementAt(index); }//end else }//end overridden removeElementAt() }//end class CustomListModel //=====================================================// //Inner class to monitor for selection events on the // JList object and store the index of the selected // element in the JTextField associated with the JList. class MyListSelectionListener implements ListSelectionListener{ public void valueChanged(ListSelectionEvent e){ int selectedIndex = ((JList)e.getSource()).getSelectedIndex(); //Don't allow the list to exist without a selected // element giving a selected index of -1. This will // throw an exception if the user attempts to remove // an element from an empty list, but a negative // index will also throw an exception if the user // attempts to remove an element with a negative // index. if(((Component)e.getSource()).getName(). compareTo("MyNameIsListA") == 0){ if(selectedIndex < 0) listA.setSelectedIndex(0); else fieldA.setText("" + selectedIndex); }else{ if(selectedIndex < 0) listB.setSelectedIndex(0); else fieldB.setText("" + selectedIndex); }//end if }//end valueChanged() }//end class MyListSelectionListener //=====================================================// }//end class SwingList02 |
-end-