Java GUI – Dynamically Changing UI on List Box Value Changes

design-patternsguijava

I have a list box with several elements, let it be web servers (tomcat, iis etc). For each list box value, UI must have different views. For example, if we choose IIS, user name and password fields appear. If we choose tomcat, some additional fields appear depending on user OS – IP and port for linux and path for windows.

My current approach is the following:
– I created two enums. The first one for different OS (windows, linux, mac os x. The second one for servers: iis, nginx, tomcat etc.
– I have a method that returns the OS enum value.
– When user makes a choice, I determine it and basing on this choice I check user's OS. And after it I determine what UI elements to hide/add.

Here's some sort of pseudo-code for what I have:

//OS enum
public enum OS {
    WINDOWS, LINUX, MAC_OS_X;
}

//Servers enum
public enum SERVERS {
    TOMCAT, IIS, NGINX;
}

//Server Listbox Handler
private void chooseServer() {
        //...get value from list box
        switch(server) {
            case TOMCAT:
                changeUI(getOS()); //getOS() returns OS enum value
                break;
            case IIS:
                //IIS is for windows only
                changeUI(OS.WINDOWS); 
                break;
            case NGNIX:
                //Temporary support only linux
                changeUI(OS.LINUX);
                break;
        }
    }

//UI changes here
private void changeUI(OS os) {
        switch (os) {
            case WINDOWS:
                userNameField.setEnabled(true);
                userPasswordField.setEnabled(true);
                ipField.setVisible(false);
                portField.setVisible(false);
                serverPathField.setVisible(true);
                break;
            case LINUX:
                userNameField.setEnabled(true);
                userPasswordField.setEnabled(true);
                ipField.setVisible(true);
                portField.setVisible(true);
                serverPathField.setVisible(false);
                break;
            case MAC_OS_X:
                userNameField.setEnabled(false);
                userPasswordField.setEnabled(false);
                ipField.setVisible(false);
                portField.setVisible(false);
                serverPathField.setVisible(false);
                break;
        }
    }

I don't like the moment when other OS or new servers will be added. These switch operators will grow more and become more complicated, I don't speak about maintainability of this code. Are there any standard approaches (patterns) for such tasks that help to avoid switch/if…else grow and to make code more generic, expandable, readable and supportable?


I tried to implement Adapter pattern suggested by Neil and ran at some obscure moment. Here's some pseudo-code:

public abstract class EditorAdapter {
    protected boolean firstInit = true;
    public abstract void adapt(Panel panel);
}

//--------

public class IISWindowsEditorAdapter extends EditorAdapter {
    @Override
    public void adapt(Panel panel) {
        if(firstInit) {
            firstInit = false;

            panel.add(userNameField);
            //add other elements
        }
    }
}

//--------

public class TomcatEditorAdapter extends EditorAdapter {
    private OS os = getOS();

    @Override
    public void adapt(Panel panel) {
        if(firstInit) {
            firstInit = false;

            if(OS == OS.LINUX) {
                panel.add(userNameField);
                //add other elements
            } else if (OS == OS.WINDOWS) {
                //add elements
            } //... else if
        }
    }
}

//--------

public class Editor extends DialogEditor { //extends Dialog with protected Panel field (subPanel)
    public Editor() {
        initialize();
    }

    public void initialize() {
        //...
        main.add(createPanel()); //protected VerticalPanel main;
        //...
    }

    public void createPanel() {
        //listBox initialization
        commonPanel.add(listBox); //panel with common elements
        listBox.addClickHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                selectServer();
            });
        }
        //... add other common elements
    }

    private void selectServer() {
        //... get selection
        switch(server) {
            //Here something should be done in order to control different layouts.
            //At the moment, each choice adds more elements in addition to existing ones
            //As a variant, one can create class fields for each adapter and manipulate with them using lazy initialization
            case IIS:
                new WindowsEditorAdapter().adapt(subpanel);
                break;
            case TOMCAT:
                new TomcatEditorAdapter().adapt(subpanel);
                break;
        }
    }
}

The problem is that I have several tables/panels in my Editor that at the end are added to main panel. In general, it looks like:

  • we open a dialog

  • choose web-server

  • some elements change inside the dialog according to our choice (and some choices are affected with OS user is using).

That's why first UI initialization with adapter doesn't work here. At least I didn't catch how to implement this adapter for logic above.

Best Answer

Note: Edited to reflect Dragon's modified requirement to make it work upon selection.

A common pattern to generalize this sort of thing is an adapter. Have an abstract class called GUIAdapter which can tinker with the individual gui aspects of your program according to the settings:

public abstract class GUIAdapter {
    public abstract void adapt(GUIFrame frame);
    public abstract void remove(GUIFrame frame);
}

Then override GUIAdapter with adapters suitable for each type of situation:

public class WindowsGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when windows operating system
    }
    public void remove(GUIFrame frame) {
        // Remove trace of WindowsGUIAdapter
    }
}

public class LinuxGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when linux operating system
    }
    public void remove(GUIFrame frame) {
        // Remove trace of LinuxGUIAdapter 
    }
}

public class TomcatGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when using tomcat server engine
    }
    public void remove(GUIFrame frame) {
        // Remove trace of TomcatGUIAdapter 
    }
}

// et cetera ...

These adapters have as much or as little control as you let them have over your frame. When the operating system or server is known, you call "remove" on all existing adapters, add the appropriate adapter for the current selection, and then call "adapt" to apply the configuration for current selection. I would recommend that you would add a panel to the frame and remove it when "remove" is called. Keep a reference to JPanel as a private member of your extended class of GUIAdapter in order to be able to remove it easily afterwards.

Notice that I didn't make a WindowsTomcatGUIAdapter. The idea is that your GUIFrame class (I'll assume that's what it's called) houses a list of adapters which get called to modify the state of the frame according to the selection without knowing how they work.

public class GUIFrame extends JFrame {
    // Other protected variables here
    private List<GUIAdapter> adapters = new ArrayList<GUIAdapter>();

    private JList serverList, osList;

    private SERVERS selectedServer = null;
    private OS selectedOS = null;


    public GUIFrame() {
        initialize();
    }

    private void initialize() {
        // Do things common to all here

        // Operating system select event
        ListSelectionListener refreshOptionsListener = new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent listSelectionEvent) {
                selectedOS = (OS)osList.getSelectedValue();
                selectedServer = (SERVERS)serverList.getSelectedValue();

                refreshAdapters();
            }
        };

        // Instantiate osList
        osList = new JList();
        // ...
        osList.addListSelectionListener(refreshOptionsListener);

        // Instantiate serverList
        serverList = new JList();
        // ...
        serverList.addListSelectionListener(refreshOptionsListener);
    }

    private void refreshAdapters() {
        for(GUIAdapter adapter : adapters) {
            adapter.remove(this);
        }
        adapters.clear();

        if(os == OS.WINDOWS) {
            adapters.add(new WindowsGUIAdapter());
        } else if (os == OS.LINUX) {
            adapters.add(new LinuxGUIAdapter());
        } // else if () ...

        if(server == SERVERS.TOMCAT) {
            adapters.add(new TomcatGUIAdapter());
        } // else if () ...

        for(GUIAdapter adapter : adapters) {
            adapter.adapt(this);
        }    
    }

    // Other class logic here
}

Now you've decoupled all logic pertaining to operating system or engine with the classes which perform this logic. Whenever a new operating system or a new server is selected, current adapters are removed and new adapters are added. If no operating system or server is selected, then no adapter is added (no condition is met in if/else chain since selectedOS or selectedServer would be null), which is fine. The frame is not adapted since no adaptation is required as you would expect.

This pattern works well only if server adapters do not really need to know what operating system is being used or inversely, if operating system adapters do not really need to know what server is being used. However, if you find yourself in that situation, you should create a WindowsTomcatGUIAdapter which deals with just that particular case rather than attempt to make WindowsGUIAdapter and TomcatGUIAdapter work well together (as that would create coupling once again). Future additions are literally as simple as adding a new class and plugging them in on initialization.

Presumably, you could extend this logic to include validation or whatever other type of logic particular to that particular adapter.

Related Topic