ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

BlackMamba: A Swing Case Study
Pages: 1, 2, 3, 4, 5

Code Reuse

One of the fundamental benefits of OOP is code reuse.



While developing BlackMamba, I faced a situation where three different kinds of data had to be displayed/edited from the UI: address book, list of spammers, and list of Spam Subjects. All of them required a JList to display the list and on selecting any one of them the details had to be displayed for editing/deletion on the right-hand side with buttons to create, save, and delete the entry on the extreme right. Only the panel that displayed the details would change. This arrangement is shown in Figure 6.

Figure 6
Figure 6. Subclass of ListConfig Panel, where the portion highlighted in red is the variation.

To avoid rewriting the code three times, I made a single widget class that held all the common widgets and three different JPanel extensions to display the details. This new JPanel extension is added to a predetermined place in the ListConfig panel by the controller class. There is a saying, "Good programmers know what to write. Great ones know what to rewrite (and reuse)."

public class Configure
{
    public void init(MambaFrm mambaFrm)
    {
        configureDlg = new ConfigureDlg(mambaFrm, 
                                            true);
        configureDlg.setLocationRelativeTo(null);
        configureDlg.setDefaultCloseOperation(
               javax.swing.WindowConstants \
                            .DO_NOTHING_ON_CLOSE);
        configureDlg.addWindowListener(
        new java.awt.event.WindowAdapter()
        {
            public void windowClosing(
                   java.awt.event.WindowEvent evt)
            {
                dispose();
            }
        });

        prepareAddressesTab();
        prepareSpammersTab();
        prepareSubjectsTab();

        configureDlg.pack();
        configureDlg.show();
    }
   
    protected void prepareAddressesTab()
    {
        ListConfig addressListConfig = 
                          new AddressListConfig();
        ...
        addressListConfig.init(
                configureDlg.addressListConfigPnl,    
                addressesListModel);
    }

    protected void prepareSpammersTab()
    {
        ListConfig spammerListConfig = 
                          new SpammerListConfig();
        ...
        spammerListConfig.init(
                configureDlg.spammerListConfigPnl, 
                spammerListModel);
    }

    protected void prepareSubjectsTab()
    {
        ListConfig subjectsListConfig = 
                         new SubjectsListConfig();
        ...
        subjectsListConfig.init(
                configureDlg.subjectListConfigPnl, 
                subjectsListModel);
    }  
}

Because blackmamba.ui.view.ListConfig panel is used for blackmamba.ui.view.AddressListConfig, blackmamba.ui.view.SpammerListConfig, and blackmamba.ui.view.SubjectsListConfig, the common logic in the controller classes for these three can be refactored into blackmamba.ui.control.ListConfig class.

public abstract class ListConfig
{
    protected CustomListModel listModel;
    protected ListConfigPnl listConfigPnl;

    public void init(ListConfigPnl lstConfigPnl, 
                         CustomListModel lstModel)
    {
        listConfigPnl = lstConfigPnl;
        listModel = lstModel;

        listConfigPnl.aliasesLst.setModel(
                                       listModel);

        ListSelectionListener selectionListener = 
        new ListSelectionListener()
        {
            public void valueChanged(
                             ListSelectionEvent e)
            {
                ...
            }
        };

        listConfigPnl.aliasesLst \
                 .addListSelectionListener( \
                               selectionListener);

        //Setup Buttons
        listConfigPnl.newBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                newBtnClicked();
            }
        });

        //Save
        listConfigPnl.saveBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                Object newObj = saveBtnClicked();
                ...
            }
        });

        //Delete
        listConfigPnl.deleteBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                deleteBtnClicked();
                ...
            }
        });

        //Close
        listConfigPnl.closeBtn.addActionListener(
        new ActionListener()
        {
            public void actionPerformed(
                                    ActionEvent e)
            {
                closeBtnClicked();
            }
        });
    }

    protected abstract void closeBtnClicked();

    protected abstract void deleteBtnClicked();

    /**
     * @return null to cancel Save. Or, the Object
     * to add.
     */
    protected abstract Object saveBtnClicked();

    protected abstract void newBtnClicked();

    protected abstract void listSelected(
                                       int index);
} 

class AddressListConfig extends ListConfig
{
    ...
    ...
}

Model

BlackMamba uses JTables to display the mails downloaded from the server. The number of rows can change dynamically. I did not use the javax.swing.table.DefaultTableModel because it uses a Vector of Vectors. Since Vectors are synchronized, the performance degrades if there are a lot of rows. And because the DefaultTableModel expects the column data to be separate objects, the granularity becomes too fine and unmanageable. A better design is to have a custom TableModel implementation: blackmamba.helpers.ui.CustomTableModel which uses the java.util.List interface as the backing datastore. The List elements or rows are instances of MailDetails. Also, by using the List interface, the implementation can be any one of ArrayList, LinkedList, or Vector. Since the mails are frequently added and deleted, LinkedList is an ideal choice of data-structure.

public class CustomTableModel 
    extends AbstractTableModel implements MailList
{
    private List mails;
    ...
   
    //------- start AbstractTableModel methods ---
    public int getRowCount()
    {
        return mails.size();
    }

    public int getColumnCount()
    {
        return 5;
    }

    public Object getValueAt(int rowIndex, 
                                  int columnIndex)
    {
        Object obj = null;

        MailDetails mailDetails = 
                (MailDetails) mails.get(rowIndex);
        if (mailDetails == null)
        {
            return obj;
        }

        switch (columnIndex)
        {
            case 0 :
                obj = getSmileyValue(mailDetails);
                break;

            case 1 :
                obj = new Boolean(
                 mailDetails.isMarkedForDelete());
                break;

            case 2 :
                obj = getSenderValue(mailDetails);
                break;

            case 3 :
                obj = getSizeValue(mailDetails);
                break;

            case 4 :
               obj = getSubjectValue(mailDetails);
               break;

            default :
                break;
        }

        return obj;
    }
   ...
    public String getColumnName(int column)
    {
        String ret = null;

        switch (column)
        {
            case 0 :
                ret = " ";
                break;
            case 1 :
                ret = "[Delete]";
                break;
            case 2 :
                ret = "From";
                break;
            case 3 :
                ret = "Size";
                break;
            case 4 :
                ret = "Subject";
                break;
            default :
                break;
        }

        return ret;
    }

    public boolean isCellEditable(int rowIndex, 
                                  int columnIndex)
    {
        boolean ret = false;

        switch (columnIndex)
        {
            case 1 :
                ret = true;
                break;
            default :
                break;
        }

        return ret;
    }

    public Class getColumnClass(int columnIndex)
    {
        Class clazz = String.class;

        switch (columnIndex)
        {
            case 0 :
                clazz = Smiley.class;
                break;
            case 1 :
                clazz = Boolean.class;
                break;
            default :
                break;
        }

        return clazz;
    }

    ...
    ...
    public void setValueAt(Object aValue, 
                    int rowIndex, int columnIndex)
    {
        if (columnIndex == 1)
        {
            MailDetails mailDetails = 
                (MailDetails) mails.get(rowIndex);
            Boolean b = (Boolean) aValue;
            mailDetails.setMarkedForDelete(
                                b.booleanValue());

            fireTableRowsUpdated(rowIndex, 
                                        rowIndex);
        }
    }
    //------- end AbstractTableModel methods -----
    
    public Object[] getPrototypeValues()
    {
        return new Object[] {
            Smiley.SMILEY_UGLY,
            Boolean.TRUE,
            "XXXXXXXXXXXXXXXXXXXXXXXXXX",
            "XXXXX",
            "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" };
    }
   
    //------- start MailList methods -------------
    public void setMails(List list)
    {
        mails = list;
        fireTableDataChanged();
    }
    
    public void addMail(MailDetails mailDetails)
    {
        mails.add(mailDetails);
        int index = mails.indexOf(mailDetails);
        fireTableRowsInserted(index, index);

        onMailAdd.call();
    }

    public void addAll(List list)
    {
        mails.add(list);
        fireTableDataChanged();
    }

    public void update(MailDetails mailDetails)
    {
        int index = mails.indexOf(mailDetails);
        fireTableRowsUpdated(index, index);
    }

   public void removeMail(MailDetails mailDetails)
   {
        int index = mails.indexOf(mailDetails);
        mails.remove(mailDetails);
        fireTableRowsDeleted(index, index);
   }

    public void removeAll()
    {
        mails.clear();
        fireTableDataChanged();
    }
    //------- end MailList methods ---------------
}

Only the second column is editable. Most of the columns render the data as Strings. The second editable column renders the Boolean data as a JCheckbox. The blackmamba.ui.control.mail.Mails class is responsible for creating, maintaining, and finally disposing of the tables. It rigs up the default renderer for the first two columns, which do not use the default renderers. The default column sizes are not very pleasing to the eye. So it also resizes the columns to make them look better by using prototype values for each column.

public class Mails
{
...
protected void customizeTable(JTable table, 
                           CustomTableModel model)
{
    JCheckBox checkBox = new JCheckBox();
    checkBox.setHorizontalAlignment(
                           SwingConstants.CENTER);
    table.setDefaultEditor(Boolean.class, 
                 new DefaultCellEditor(checkBox));

    DefaultTableCellRenderer smileyRenderer = 
                   new DefaultTableCellRenderer();
    smileyRenderer.setHorizontalAlignment(
                           SwingConstants.CENTER);
    table.setDefaultRenderer(Smiley.class, 
                                  smileyRenderer);

    TableColumn column = null;
    Component comp = null;
    int headerWidth = 0;
    int cellWidth = 0;
    Object[] longValues = 
                       model.getPrototypeValues();
    int cols = model.getColumnCount();
    TableColumnModel columnModel = 
                           table.getColumnModel();

    for (int i = 0; i < cols; i++)
    {
        column = columnModel.getColumn(i);

        comp =
            table.getTableHeader() \
                .getDefaultRenderer() \
                .getTableCellRendererComponent(
                null,
                column.getHeaderValue(),
                false,
                false,
                0,
                0);
        headerWidth = comp.getPreferredSize() \
                                           .width;

        comp =
            table.getDefaultRenderer(
                  model.getColumnClass(i)) \
                  .getTableCellRendererComponent(
                table,
                longValues[i],
                false,
                false,
                0,
                i);
        cellWidth = comp.getPreferredSize().width;

       int min = Math.min(headerWidth, cellWidth);
       int max = Math.max(headerWidth, cellWidth);

        switch (i)
        {
            case 0 :
            case 1 :
            case 3 :
                column.setPreferredWidth(min);
                break;
            default :
                column.setPreferredWidth(max);
                break;
        }
    }
}
...
}

Pages: 1, 2, 3, 4, 5

Next Pagearrow