Search code examples
javaswingpopupjbutton

How to get rid of repeating code in function?


I want to get rid of the repeating code from an application. I tried to do it in several ways, but when I did it, the application did not work as I expected. Only putting the same code in a function had an effect.

In a nutshell, in the application I can delete records in 2 ways, either by pressing a button or by pointing to a record and deleting it with the right mouse button. Button deletion works, but I do not know how to make the mouse deletion have the same effect.

Button that deletes a record in a table.

 deleteButton.addActionListener(event -> {
    
                String name;
                name = Name.getText();
    
                try {
                    removeSelectedRow(table1);
    
                    pst = con.prepareStatement("delete from recipe where recipe_name = ?");
                    pst.setString(1, name);
                    pst.executeUpdate();
                    JOptionPane.showMessageDialog(null, "Record deleted");
                    Name.setText("");
                    Time.setText("");
                    Difficulty.setSelectedItem("");
                    Name.requestFocus();
    
                } catch (SQLException e) {
    
                    e.printStackTrace();
                }
    
    
            });

here is a function that is supposed to delete with the right mouse button, as you can see it works but the code is almost identical to the previous example.

 public void setDeleteButton(ActionEvent event) {


        JMenuItem menu = (JMenuItem) event.getSource();
        if (menu == menuItemRemove) {
            removeSelectedRow(table1);

        }
        String name;
        name = Name.getText();

        try {
            removeSelectedRow(table1);

            pst = con.prepareStatement("delete from recipe where recipe_name = ?");
            pst.setString(1, name);
            pst.executeUpdate();
            JOptionPane.showMessageDialog(null, "Record deleted");
            Name.setText("");
            Time.setText("");
            Difficulty.setSelectedItem("");
            Name.requestFocus();

        } catch (SQLException e) {

            e.printStackTrace();
        }
    }

function that points to a specific record

 public void removeSelectedRow(JTable table) {

        DefaultTableModel model = (DefaultTableModel) table1.getModel();
        if (table.getSelectedRow() != -1) {
            model.removeRow(table.getSelectedRow());

        }
    }

Solution

  • Okay, so this is going to require a slight shift in mindset. To make this truely flexible, you're going to need to support concepts such as "dependency inject" and "delegation".

    The reason for this is, you "operation" needs a lot of information, but, we should be driving towards having a low level of cohesion or coupling between our classes. Your "operation", for example, shouldn't care "how" the row is deleted, only that when requested, it should be done.

    So, lets start with some basic delegation...

    public interface MutableTableSupportable {
        public void addListSelectionListener(ListSelectionListener listener);
        public void removeListSelectionListener(ListSelectionListener listener);
    }
    
    public interface TableRowDeletable extends MutableTableSupportable {
        public int getSelectedRowCount();
        public void removeSelectedRow();
    }
    

    Now, obviously, I'm overly simplifying this for more my needs, but here I've provided a "basic" level delegate and more focused delegate. Why? Because what happens if you want to provide a "insert" action? Why should it have "delete" functionality? Instead, we deliberately isolate the functionality we want to expose.

    Next, we need to design our action...

    public class DeleteRowAction extends AbstractAction {
    
        private TableRowDeletable delgate;
    
        public DeleteRowAction(TableRowDeletable delgate) {
            putValue(SHORT_DESCRIPTION, "Delete the currently selected row");
            putValue(MNEMONIC_KEY, KeyEvent.VK_D);
            putValue(NAME, "Delete Row");
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK));
            this.delgate = delgate;
    
            delgate.addListSelectionListener(new ListSelectionListener() {
                @Override
                public void valueChanged(ListSelectionEvent e) {
                    selectionDidChange();
                }
            });
            selectionDidChange();
        }
    
        protected void selectionDidChange() {
            setEnabled(delgate.getSelectedRowCount() > 0);
        }
    
        @Override
        public void actionPerformed(ActionEvent e) {
            delgate.removeSelectedRow();
        }
    
    }
    

    Okay, nothing really special, which is kind of the point. It monitors the selection state so we can enable/disable the action and when triggered, we call our delegate to do the actual work. This decouples the action from the implementation, as the action doesn't need to know what type of TableModel is in use or what type of data source it might be using, it just wants to tell the delegate that it should carry out some kind of operation.

    Also note, we've set up a keyboard short cut, which can be used by the JMenuItem and mnemonic support (hold down the Alt or Option key)

    Okay, but that's really doing a lot for us, or is it...

    Let's have a look at what a delete action might look like...

    DeleteRowAction deleteRowAction = new DeleteRowAction(new TableRowDeletable() {
        @Override
        public int getSelectedRowCount() {
            return table.getSelectedRowCount();
        }
    
        @Override
        public void removeSelectedRow() {
            DefaultTableModel model = (DefaultTableModel) table.getModel();
            int visibleRowIndex = table.getSelectedRow();
            if (visibleRowIndex == -1) {
                return;
            }
    
            int modelIndex = table.convertRowIndexToModel(visibleRowIndex);
            // I'm guessing here, but if you're deleting a row, you should
            // use the row data
            String recordId = (String) model.getValueAt(modelIndex, 0);
    
            try (PreparedStatement pst = getConnection().prepareStatement("delete from recipe where recipe_name = ?")) {
                pst.setString(1, recordId);
                // You could check the number of rows effected by this change
                pst.executeUpdate();
    
                JOptionPane.showMessageDialog(TestPane.this, "Record deleted", "Success", JOptionPane.INFORMATION_MESSAGE);
    
                model.removeRow(modelIndex);
            } catch (SQLException ex) {
                ex.printStackTrace();
                JOptionPane.showMessageDialog(TestPane.this, "Failed to delete row from database", "Error", JOptionPane.ERROR_MESSAGE);
            }
        }
    
        @Override
        public void addListSelectionListener(ListSelectionListener listener) {
            table.getSelectionModel().addListSelectionListener(listener);
        }
    
        @Override
        public void removeListSelectionListener(ListSelectionListener listener) {
            table.getSelectionModel().removeListSelectionListener(listener);
        }
    });
    

    Now, this is just an example, but the basic idea is, we've provide implementation for both the MutableTableSupportable and TableRowDeletable interfaces (but the DeleteRowAction doesn't care about the "how") and we've implemented the removeSelectedRow functionality to delete the row from the TableModel and database.

    Again, DeleteRowAction doesn't care how this is implemented, it's just delegating that responsibility, so you could have multiple DeleteRowActions which work with different TableModels and data sources all at the same time 😱

    Delegation 💪

    Okay, but how would all that work together? Well, actually, really easily in fact

    Runnable example...

    import java.awt.BorderLayout;
    import java.awt.EventQueue;
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
    import javax.swing.AbstractAction;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JTable;
    import javax.swing.KeyStroke;
    import javax.swing.event.ListSelectionEvent;
    import javax.swing.event.ListSelectionListener;
    import javax.swing.table.DefaultTableModel;
    import java.sql.*;
    import javax.swing.ActionMap;
    import javax.swing.InputMap;
    import javax.swing.JButton;
    import javax.swing.JMenu;
    import javax.swing.JMenuBar;
    import javax.swing.JOptionPane;
    import javax.swing.JScrollPane;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JMenuBar menuBar = new JMenuBar();
                    JFrame frame = new JFrame();
                    frame.setJMenuBar(menuBar);
                    frame.add(new TestPane(menuBar));
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private JTable table;
    
            public TestPane(JMenuBar menuBar) {
                setLayout(new BorderLayout());
    
                DefaultTableModel model = new DefaultTableModel(new Object[][]{new Object[]{"Test"}}, new Object[]{"Test"});
                table = new JTable(model);
    
                add(new JScrollPane(table));
    
                DeleteRowAction deleteRowAction = new DeleteRowAction(new TableRowDeletable() {
                    @Override
                    public int getSelectedRowCount() {
                        return table.getSelectedRowCount();
                    }
    
                    @Override
                    public void removeSelectedRow() {
                        JOptionPane.showMessageDialog(TestPane.this, "Delete the row please", "Debug", JOptionPane.INFORMATION_MESSAGE);
                    }
    
                    @Override
                    public void addListSelectionListener(ListSelectionListener listener) {
                        table.getSelectionModel().addListSelectionListener(listener);
                    }
    
                    @Override
                    public void removeListSelectionListener(ListSelectionListener listener) {
                        table.getSelectionModel().removeListSelectionListener(listener);
                    }
                });
    
                JMenu actionsMenu = new JMenu("Actions");
                actionsMenu.add(deleteRowAction);
    
                menuBar.add(actionsMenu);
    
                JButton deleteButton = new JButton(deleteRowAction);
                add(deleteButton, BorderLayout.SOUTH);
            }
    
        }
    
        public interface MutableTableSupportable {
            public void addListSelectionListener(ListSelectionListener listener);
            public void removeListSelectionListener(ListSelectionListener listener);
        }
    
        public interface TableRowDeletable extends MutableTableSupportable {
            public int getSelectedRowCount();
            public void removeSelectedRow();
        }
    
        public class DeleteRowAction extends AbstractAction {
    
            private TableRowDeletable delgate;
    
            public DeleteRowAction(TableRowDeletable delgate) {
                putValue(SHORT_DESCRIPTION, "Delete the currently selected row");
                putValue(MNEMONIC_KEY, KeyEvent.VK_D);
                putValue(NAME, "Delete Row");
                putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK));
                this.delgate = delgate;
    
                delgate.addListSelectionListener(new ListSelectionListener() {
                    @Override
                    public void valueChanged(ListSelectionEvent e) {
                        selectionDidChange();
                    }
                });
                selectionDidChange();
            }
    
            protected void selectionDidChange() {
                setEnabled(delgate.getSelectedRowCount() > 0);
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                delgate.removeSelectedRow();
            }
    
        }
    }
    

    nb: This example removes the database support, as I don't have one and instead displays a message

    Okay, lets take a quick look at some of the interesting things here...

    Firstly...

    JMenuBar menuBar = new JMenuBar();
    JFrame frame = new JFrame();
    frame.setJMenuBar(menuBar);
    frame.add(new TestPane(menuBar));
    

    We inject the menu bar into the panel. This is done so that the panel can configure the menu bar as it needs. We could use a type of factory or another delegate here, but I'll leave that up to you to figure out.

    Next...

    JMenu actionsMenu = new JMenu("Actions");
    actionsMenu.add(deleteRowAction);
    
    menuBar.add(actionsMenu);
    
    JButton deleteButton = new JButton(deleteRowAction);
    add(deleteButton, BorderLayout.SOUTH);
    

    We build the JMenu and add our delete row action and create a JButton, using the same Action ... for five lines of code, we've actually done a lot. We've been able to set up the text displayed by each component, the tooltip text, the accelerator key and mnemonic ... try doing that manually, and then need to change something down the track 🙄 (want to support localisation - need to make changes in one location)

    But wait, we can do more!! 😱

    If we add...

    InputMap inputMap = table.getInputMap(WHEN_FOCUSED);
    ActionMap actionMap = table.getActionMap();
    
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "deleteRow");
    actionMap.put("deleteRow", deleteRowAction);
    

    to the end of the constructor, we can can provide a key binding to the user, so that when the JTable has keyboard focus and the hit they Delete/Backspace key, it will trigger the action as well!!!

    Now we have four ways to trigger the action:

    1. Press the button
    2. Open and trigger the menu items
    3. Use the menus keyboard "accelerator" key binding
    4. Hit the Delete key

    Actions 💪

    Key bindings runnable example...

    import java.awt.BorderLayout;
    import java.awt.EventQueue;
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
    import javax.swing.AbstractAction;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JTable;
    import javax.swing.KeyStroke;
    import javax.swing.event.ListSelectionEvent;
    import javax.swing.event.ListSelectionListener;
    import javax.swing.table.DefaultTableModel;
    import javax.swing.ActionMap;
    import javax.swing.InputMap;
    import javax.swing.JButton;
    import javax.swing.JMenu;
    import javax.swing.JMenuBar;
    import javax.swing.JOptionPane;
    import javax.swing.JScrollPane;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JMenuBar menuBar = new JMenuBar();
                    JFrame frame = new JFrame();
                    frame.setJMenuBar(menuBar);
                    frame.add(new TestPane(menuBar));
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private JTable table;
    
            public TestPane(JMenuBar menuBar) {
                setLayout(new BorderLayout());
    
                DefaultTableModel model = new DefaultTableModel(new Object[][]{new Object[]{"Test"}}, new Object[]{"Test"});
                table = new JTable(model);
    
                add(new JScrollPane(table));
    
                DeleteRowAction deleteRowAction = new DeleteRowAction(new TableRowDeletable() {
                    @Override
                    public int getSelectedRowCount() {
                        return table.getSelectedRowCount();
                    }
    
                    @Override
                    public void removeSelectedRow() {
                        JOptionPane.showMessageDialog(TestPane.this, "Delete the row please", "Debug", JOptionPane.INFORMATION_MESSAGE);
                    }
    
                    @Override
                    public void addListSelectionListener(ListSelectionListener listener) {
                        table.getSelectionModel().addListSelectionListener(listener);
                    }
    
                    @Override
                    public void removeListSelectionListener(ListSelectionListener listener) {
                        table.getSelectionModel().removeListSelectionListener(listener);
                    }
                });
    
                JMenu actionsMenu = new JMenu("Actions");
                actionsMenu.add(deleteRowAction);
    
                menuBar.add(actionsMenu);
    
                JButton deleteButton = new JButton(deleteRowAction);
                add(deleteButton, BorderLayout.SOUTH);
    
                InputMap inputMap = table.getInputMap(WHEN_FOCUSED);
                ActionMap actionMap = table.getActionMap();
    
                inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "deleteRow");
                actionMap.put("deleteRow", deleteRowAction);
            }
    
        }
    
        public interface MutableTableSupportable {
            public void addListSelectionListener(ListSelectionListener listener);
            public void removeListSelectionListener(ListSelectionListener listener);
        }
    
        public interface TableRowDeletable extends MutableTableSupportable {
            public int getSelectedRowCount();
            public void removeSelectedRow();
        }
    
        public class DeleteRowAction extends AbstractAction {
    
            private TableRowDeletable delgate;
    
            public DeleteRowAction(TableRowDeletable delgate) {
                putValue(SHORT_DESCRIPTION, "Delete the currently selected row");
                putValue(MNEMONIC_KEY, KeyEvent.VK_D);
                putValue(NAME, "Delete Row");
                putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK));
                this.delgate = delgate;
    
                delgate.addListSelectionListener(new ListSelectionListener() {
                    @Override
                    public void valueChanged(ListSelectionEvent e) {
                        selectionDidChange();
                    }
                });
                selectionDidChange();
            }
    
            protected void selectionDidChange() {
                setEnabled(delgate.getSelectedRowCount() > 0);
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                delgate.removeSelectedRow();
            }
    
        }
    }
    

    But that's not all! We could also add a button to a JToolBar, because, why not!?