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

Control

The control classes control the entire lifecycle of the view classes. It is the control class' responsibility to instantiate the UI, hookup listeners, attach models, and finally dispose them after the operation. It is very easy to have bloated control classes with too much logic forced upon them. A good rule of thumb is to display and handle only those screens (view/noun) that are involved in completing the action indicated by the control/verb, and delegate the rest to the control class that is next in line.



For example, in BlackMamba:

Controller/Verb View/Noun
(Modified Hungarian Notation)
Description
Start AboutDlg, AboutWin, MambaFrm Launch main frame. Setup menu action listeners and delegate Login action.
Login MambaFrm, LoginDlg Launch login dialog. Setup button action listeners and delegate Connect and Accounts actions.
Connect SplashPnl Launch connect dialog. Connect to the server, update progress, and setup cancel button action listener
Fetch MailsPnl, MambaFrm, SplashPnl Launch fetch dialog. Connect to the server, fetch mails, update progress, submit mails to MailProcessor, and setup cancel button action listener.

As a general practice, try to split the logic across controllers. Invent more verbs to which the bloated controller can delegate control to. A controller with more than 500-600 lines is definitely bloated. Smaller classes also improve readability.

Responsive UI

If the UI performs operations that require communication across a network like a database call or remote server invocation, or any operation that requires a lot of time to complete, it is best to have a Cancel option. The network can fail, or the remote server can crash. When performing such lengthy operations, it must be done in a separate Thread. Swing has a dedicated Thread to perform UI operations like adding/removing components to the UI, painting the screen, listening for buttons clicks, and dispatching events to listeners.

An important thing to keep in mind is that the non-GUI Thread must not make any direct changes to the UI. A UI operation that requires execution by it must be submitted to the EventQueue using the SwingUtilities.invokeLater(runnable) or SwingUtilities.invokeAndWait(runnable). A way to find out if the Thread invoking your method is that dedicated UI Thread or not is by invoking javax.swing.SwingUtilities.isEventDispatchThread().

The following code is a control class to display a Thread-safe "Connection progress" view. The classes Setup, LiteThreadPool, and PoolThread are described later.

public class Connect
{
    //Note the "volatile" keyword.
    private volatile int progressStep;
    ...
    final Runnable target = new Runnable()
            {
                public void run()
                {
                    connect();
                }
            };
    ...
    ...
    dialog.setDefaultCloseOperation(
        javax.swing.WindowConstants \
                            .DO_NOTHING_ON_CLOSE);
    
    dialog.addWindowListener(
        new java.awt.event.WindowAdapter()
        {
            public void windowClosing(
                   java.awt.event.WindowEvent evt)
            {
                cancel();
            }
        });
    ...
    ... 
    LiteThreadPool threadPool = 
                        Setup.getLiteThreadPool();
    PoolThread thread1 = threadPool.getThread1();
    thread1.execute(target);
    ...
    ...
    protected void connect()
    {
        final Runnable runnable = new Runnable()
        {
            public void run()
            {
                //Update UI
                splashPnl.connectingPrg.setValue(
                                    progressStep);
            }
        };
    
        try
        {
            mailHelper.connect(accountDetails);
            progressStep = 1;
            SwingUtilities.invokeLater(runnable);
    
            mailHelper.getMessageCount();
            progressStep = 2;
            SwingUtilities.invokeLater(runnable);
    
            mailHelper.disconnect();
            progressStep = STEP_END;
            SwingUtilities.invokeLater(runnable);
        }
        catch (Exception e)
        {
            ...
        }
    }
    
    protected void cancel()
    {
        boolean success = true;

        try
        {
            if (progressStep != STEP_END)
            {
                mailHelper.immediateDisconnect();
                success = false;
            }
        }
        catch (Exception e)
        {
        ...
        }
    
        dispose(success);
    }
   ...
   ...
}

Never perform lengthy computations inside a listener like ActionListener's actionPerformed() method. Spawn a new Thread and do the operation in the new non-GUI Thread. If it is not done this way, then the button will stay clicked until the lengthy operation completes. All the windows will turn gray because they cannot be painted/refreshed by the AWT-Thread, which is stuck doing your time-consuming operation. Even if you have a cancel button, it will not respond to mouse-clicks because the AWT-Thread is the one that is supposed to listen for such events and then dispatch the event to the listeners. This deadlock freezes the UI.

If the UI does such multi-threaded operations frequently, then a Thread Pool is a good option to improve performance. Spawning a new Thread every time is a very expensive operation. It can be accomplished easily using just two classes. The Thread Pool starts the required number of Threads beforehand and keeps them ready for consumers. After using a Thread, it is returned to the pool for reuse.

public class LiteThreadPool
{
    private PoolThread thread1;
    private PoolThread thread2;
    
    public LiteThreadPool()
    {
         thread1 = new PoolThread();
         thread1.setDaemon(true);
         thread1.start();
    
         thread2 = new PoolThread();
         thread2.setDaemon(true);
         thread2.start();
    }
    
    //Thread1 used by MailProcessor.
    public PoolThread getThread1()
    {
         return thread1;
    }
    
    //Thread2 used by control classes.
    public PoolThread getThread2()
    {
         return thread2;
    }
} 

public class PoolThread extends Thread
{
    private Object lock = new Object();
    private Runnable target = null;

    public void run()
    {
    while (true)
    {
    
        synchronized (lock)
        {
            while (target == null)
            {
                try
                {
                    lock.wait();
                }
                catch (InterruptedException e)
                {
                    if (Setup.DEBUG) 
                    {
                        e.printStackTrace();
                    }
                }
            } //end inner while

            try
            {
                target.run();
            }
            //Note that Exception is being caught.
            //If not, RuntimeExceptions can get 
            //through and kill the PoolThread. 
            catch (Exception e)
            {
                if (Setup.DEBUG)
                {
                    e.printStackTrace();
                }
            }
            
            target = null;
            
        } //end sync
        
    } //end while
    }

    public void execute(Runnable r)
    {
        synchronized (lock)
        {
            target = r;
            lock.notifyAll();
        }
    }
}

If you read the LiteThreadPool and PoolThread classes closely, you will realize it is a very rudimentary form of Thread pooling. In fact, it is just reusing Thread1 for the MailProcessor and Thread2 for the control classes, which is sufficient for a small application like BlackMamba. There is actually no pool from which Threads are picked up for processing Runnable targets. If a caller wants its target to be executed by a Thread from the pool and the only Thread is already executing another Runnable, it has to wait for it to finish. A real Thread pool will maintain a data structure like a Synchronized Queue that will hold several pre-initialized PoolThreads. Requests will be serviced by removing PoolThreads from the Queue in FIFO order. Once the PoolThread completes executing the target, it is smart enough to add itself back to the Queue.

Pages: 1, 2, 3, 4, 5

Next Pagearrow