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.
2
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.
11
I guess what you are looking for is some kind of decision table. For each possible combination of OS, server and GUI element store a boolean flag indicating the visibility. You could simply utilize a 3 dimensional boolean array (bool[][][] uiVisibility
) for that task, indexed by enums indicating the OS, the server and the particular UI element. And you GUI initialization code then will just look like this:
userNameField.setEnabled(uiVisibility[osIndex][serverIndex][UIelem.userNameField.getValue()]);
userPasswordField.setEnabled(uiVisibility[osIndex][serverIndex][UIelem.userPasswordField.getValue()]);
As you see – no switch/case any more, and the visibility setting code is in just one place.
Note that it is much more likely that your list of GUI elements will grow than the list of OSs or servers – at least as long as you don’t have to distinguish between different OS versions or server versions.
EDIT: if this is the right tool for the job depends. First, a decision table is definitely much better maintainable then to duplicate the same setEnabled
code 12 times, with minor modifications between that blocks (note that Neil’s suggestion does not solve that problem, it just hides it because he does not show the // Things to do
part in his answer)
The key is how you initialize the table. You have different options for that, for example, using standard array initialization. If you have only minor differences between the 12 combinations, there is also the option to create a default initialization for all blocks in the decision table (or a default initilization for each OS) and add some small code writing only that modifications for specific combinations of server/OS.
3
If your configuration is composed of boolean values, I think what you could have is the following structure comprising a BitSet:
class OSConfiguration {
private BitSet configurationFields;
public OSConfiguration() {
configurationFields = new BitSet();
}
// you can allow checking if a given bitset index is set or not
public boolean isFieldEnabled(int index) { return configurationFields.get(index); }
// ... or create proper getter/setters
public boolean isUserNameFieldEnabled() { return configurationFields.get(3); }
public void setUserNameFieldEnabled(boolean value) { configurationFields.set(3, value); }
// all other fields could be done like that
}
And the following TreeMap:
TreeMap map = new TreeMap<SERVERS, Map<OS, OSConfiguration>>();
// to check if an OS configuration is available for a given server:
boolean isAvailable = map.get(SERVERS.TOMCAT).containsKey(OS.WINDOWS);
// to get an entire configuration:
OSConfiguration configuration = map.get(SERVERS.TOMCAT).get(OS.WINDOWS);
// to check if a given configuration is enabled:
boolean isEnabled = map.get(SERVERS.TOMCAT).get(OS.WINDOWS).isUserNameFieldEnabled();
Now, even if your OS configuration needs to have more than just boolean values, since you’ve hidden that in the OSConfiguration class, you can do whatever you want.
In any case, I suggest to keep it simple.