Saturday, August 29, 2009

Simple Swing App - The View

In order to understand the view portion of the code, you must first understand the MVC framework class from which it extends. Let me list the whole class here,and then go over the important pieces and parts. Here is FormInputPanel.java:

public abstract class FormInputPanel
extends JPanel implements PropertyChangeListener {

private Model _model;
private boolean _processingData;
private Map<String, WeakReference<Method>> _methodMap;

/** Creates a new instance of FormInputPanel */
public FormInputPanel() {
super();
init();
}

public FormInputPanel(Model model) {
this();
setModel(model);
}

/**
* Return a title associated with this panel. This title can be used for
* text such as, a window title, tab text, or titled border text around
* this panel.
*/
public abstract String getTitle();

public void modelToView() {}

public void viewToModel() {}

public void updateModel(String methodName, Class[] params, Object[] values) {
if (isProcessingData()) {
return;
}

setProcessingData(true);
try {
Method method = getMethod(methodName, params);
method.invoke(_model, values);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} finally {
setProcessingData(false);
}
}

public void propertyChange(PropertyChangeEvent evt) {
modelToView();
}

public void setModel(Model model) {
if (_model != null) {
_model.removePropertyChangeListener(this);
}
_model = model;
registerModel(_model);
modelToView();
}

/**
* Create form components to be displayed in this panel. The order
* of Components returned will be the same order added to the panel.
*/
protected abstract Component[] createForms();

/**
* Allow subclasses to register with model as PropertyChangeListeners
* for specific properties.
*/
protected abstract void registerModel(Model model);

protected Model getModel() {
return _model;
}

protected boolean isProcessingData() {
return _processingData;
}

protected void setProcessingData(boolean b) {
_processingData = b;
}

protected void installBorder(JComponent comp, Border border) {
comp.setBorder(border);
}

private void init() {
_methodMap = Collections.synchronizedMap(new HashMap());

setLayout(new BorderLayout());
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
Box inputs = Box.createVerticalBox();
Component[] comps = createForms();
for (int i=0; i<comps.length; i++) {
inputs.add(comps[i]);
}

add(inputs, BorderLayout.NORTH);
}

private Method getMethod(String methodName, Class[] params)
throws NoSuchMethodException {

WeakReference<Method> wr = _methodMap.get(methodName);
Object obj = null;
if (wr != null && (obj = wr.get()) != null) {
return (Method)obj;
}

Method method = _model.getClass().getMethod(methodName, params);
_methodMap.put(methodName, new WeakReference<Method>(method));

return method;
}
}
The important pieces are the following methods:
  • protected abstract Component[] createForms(); This method builds the actual UI components. This array of components returned are added to the panel in top-down fashion via a vertical box.
  • public void modelToView() {} This method takes the values from the associated Model and populates the view components.
  • public void viewToModel() {} This method takes the user-entered values in the view components and populates the Model.
  • protected abstract void registerModel(Model model); This method gives the view the opportunity to register itself (or anything else) as a PropertyChangeListener on the Model.

Just as important is knowing the order that these methods get called. When you call the constructor with the Model parameter, the methods get called in the following order:

  1. createForms() gets called and the components are added to the panel.
  2. registerModel(Model model) gets called.
  3. modelToView() is finally called so that the components can be back-filled with the Model values.
This order is important because the values from the Model will not be available when the UI components are first constructed. You will only be able to get the Model values upon the calling of modelToView. This will all probably make more sense when we see the application's code.

So, here is the code for MyContactsEditor.java - at least most of it; namely the part that shows those methods implemented from FormInputPanel.

public class MyContactsEditor extends FormInputPanel
implements ChangeListener {

private static final long serialVersionUID = -1459578361121044353L;
private MyUser currentUser;
private JButton saveBtn;
private JComboBox userList;
private JTextField firstFld;
private JTextField lastFld;
private JTextField ssnFld;
private JXDatePicker datePicker;
private JLabel contactImg;

/**
*
* @param model
*/
public MyContactsEditor(MyContactsModel model) {
super(model);
setBorder(BorderFactory.createEmptyBorder(3, 3, 0, 3));
List<MyUser> users = model.getContacts();
for (Iterator<MyUser> iter = users.iterator(); iter.hasNext();) {
userList.addItem(iter.next());
}
}

/**
*
* @return
*/
@Override
public String getTitle() {
return "Contact Editor";
}

/**
*
*/
@Override
public void modelToView() {
super.modelToView();
if (currentUser == null) {
firstFld.setText("");
lastFld.setText("");
ssnFld.setText("");
datePicker.setDate(null);
contactImg.setIcon(null);
} else {
firstFld.setText(currentUser.getFirstName());
lastFld.setText(currentUser.getLastName());
ssnFld.setText(currentUser.getSocialSecurityNumber());
datePicker.setDate(currentUser.getBirthday());
contactImg.setIcon(ImageTools.imageToIcon(
currentUser.getImage(),
contactImg.getPreferredSize(), true));
}
}

/**
*
* @return
*/
@Override
protected Component[] createForms() {
JStatusPanel sp = new JStatusPanel();
sp.addRightComponent(new TimeLabel());

Component[] forms = new Component[]{
buildUserImage(),
buildUserList(),
Box.createVerticalStrut(5),
new JSeparator(),
buildContactInputs(),
new JSeparator(),
buildButtons(),
sp
};

return forms;
}

/**
*
* @param model
*/
@Override
protected void registerModel(Model model) {
MyContactsModel cm = (MyContactsModel) model;
model.addPropertyChangeListener(this);

currentUser = cm.getContacts().get(0);
}
So follow the methods in the order that they are called. The building of the UI components is split into their own methods. The following image shows what areas each method builds:


First, let me apologize for the amount of code in this huge post. I think the code can sometimes illustrate more than what I discuss here. In any event, if anyone has any questions/comments/suggestions, feel free to add your input and I will respond to any questions.

Now, from the above code, you can see how the model values are used for populating the view. Let me now show some of the methods that build the UI. I did not choose to implement the viewToModel method. Instead, the event handlers for the buttons do all the work - I will cover that method last. For now, here is the UI construction:

private Component buildUserList() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));

userList = new JComboBox(new DefaultComboBoxModel());
userList.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
currentUser = (MyUser) userList.getSelectedItem();
modelToView();
}
});
pnl.add(userList);

return pnl;
}

private Component buildUserImage() {
JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));

contactImg = new JLabel() {
@Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}

@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}

@Override
public Dimension getMaximumSize() {
return getPreferredSize();
}

@Override
public int getIconTextGap() {
return 0;
}

@Override
public int getHorizontalAlignment() {
return SwingConstants.CENTER;
}
};
pnl.add(contactImg);

return pnl;
}

/**
*
* @return
*/
private Component buildContactInputs() {
Box box = Box.createVerticalBox();
firstFld = new JTextField(25);
firstFld.setName("First Name");

lastFld = new JTextField(25);
lastFld.setName("Last Name");

try {
ssnFld = new JFormattedTextField(new MaskFormatter("###-##-####"));
ssnFld.setName("Soc. Sec. Num.");
ssnFld.setColumns(25);
} catch (ParseException ex) {
ssnFld = new JTextField(25);
Logger.getLogger(MyContactsEditor.class.getName()).log(Level.SEVERE, null, ex);
}

datePicker = new JXDatePicker();
datePicker.setFormats(new SimpleDateFormat("MMMMM dd, yyyy"));
datePicker.getEditor().setName("Birth Date");
datePicker.getEditor().setEditable(false);

JPanel p1 = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel l1 = new JLabel("First Name:", SwingConstants.RIGHT);
p1.add(l1);
p1.add(firstFld);
box.add(p1);

JPanel p2 = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel l2 = new JLabel("Last Name:", SwingConstants.RIGHT);
p2.add(l2);
p2.add(lastFld);
box.add(p2);

JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel l3 = new JLabel("Soc Sec Num:", SwingConstants.RIGHT);
p3.add(l3);
p3.add(ssnFld);
box.add(p3);

JPanel p4 = new JPanel(new FlowLayout(FlowLayout.LEFT));
JLabel l4 = new JLabel("Birth Date:", SwingConstants.RIGHT);
p4.add(l4);
p4.add(datePicker);
box.add(p4);

UITools.equalizeSizes(new JComponent[]{l1, l2, l3, l4});

ValidationPanel vp = new ValidationPanel();
vp.setInnerComponent(box);
vp.addChangeListener(this);
ValidationGroup grp = vp.getValidationGroup();
grp.add(firstFld, Validators.REQUIRE_NON_EMPTY_STRING);
grp.add(ssnFld, Validators.regexp("[0-9]{3}-[0-9]{2}-[0-9]{4}", "Invalid Social Security Number", false));

return vp;
}

So this basically shows the construction of the UI inputs. Again, the thing to remember here is that they do not get back-filled with data until after they are constructed and modelToView gets called.

Lastly, let's see the construction of the buttons. This also shows the event handlers that sync the UI data with the model:

private Component buildButtons() {
Box box = Box.createHorizontalBox();
box.setBorder(BorderFactory.createEmptyBorder(5, 3, 5, 3));

saveBtn = new JButton("Save");
final JButton addBtn = new JButton("Add");
final JButton deleteBtn = new JButton("Delete");
final JButton resetBtn = new JButton("Reset");

saveBtn.setMnemonic('S');
saveBtn.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
try {
MyUser modifiedUser = new MyUser(firstFld.getText(),
lastFld.getText(),
ssnFld.getText(),
datePicker.getDate(),
currentUser.getImage());
MyContactsModel cm = (MyContactsModel) getModel();
if (cm.modifyUser(currentUser, modifiedUser)) {
DefaultComboBoxModel cbm = (DefaultComboBoxModel) userList.getModel();
rebuildUserList();
currentUser = modifiedUser;
cbm.setSelectedItem(modifiedUser);
EventBus.publish(new StatusEvent("User Modified: " + currentUser));
} else {
EventBus.publish(new StatusEvent("Unable to modify user: " + currentUser));
}
} catch (Exception ex) {
Logger.getLogger(MyContactsEditor.class.getName()).log(Level.SEVERE, null, ex);
}
}
});

addBtn.setMnemonic('A');
addBtn.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
try {
MyContactsModel cm = (MyContactsModel) getModel();
MyUser newUser = new MyUser(
firstFld.getText(),
lastFld.getText(),
ssnFld.getText(),
datePicker.getDate());
if (cm.addUser(newUser)) {
DefaultComboBoxModel cbm = (DefaultComboBoxModel) userList.getModel();
rebuildUserList();
currentUser = newUser;
cbm.setSelectedItem(newUser);
saveBtn.setEnabled(cbm.getSize() > 0);
deleteBtn.setEnabled(cbm.getSize() > 0);
EventBus.publish(new StatusEvent("User Added: " + newUser));
} else {
EventBus.publish(new StatusEvent("User Already Exists: " + newUser));
}
} catch (Exception ex) {
Logger.getLogger(MyContactsEditor.class.getName()).log(Level.SEVERE, null, ex);
}
}
});

deleteBtn.setMnemonic('D');
deleteBtn.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
int answer = JOptionPane.showConfirmDialog(
MyContactsEditor.this,
"Are you sure you want to delete \"" + currentUser + "\"?",
"Delete Contact",
JOptionPane.YES_NO_OPTION);
if (JOptionPane.YES_OPTION != answer) {
return;
}

MyContactsModel cm = (MyContactsModel) getModel();
DefaultComboBoxModel cbm = (DefaultComboBoxModel) userList.getModel();
int idx = cbm.getIndexOf(currentUser);
if (cm.removeUser(currentUser)) {
String deletedUser = currentUser.toString();
cbm.removeElementAt(idx);
saveBtn.setEnabled(cbm.getSize() > 0);
deleteBtn.setEnabled(cbm.getSize() > 0);
EventBus.publish(new StatusEvent("User Deleted: " + deletedUser));
} else {
EventBus.publish(new StatusEvent("Delete Failed for: " + currentUser));
}
}
});

resetBtn.setMnemonic('R');
resetBtn.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent e) {
modelToView();
EventBus.publish(new StatusEvent("User data reset: " + currentUser));
}
});

UITools.equalizeSizes(new JComponent[]{saveBtn, addBtn, deleteBtn, resetBtn});

box.add(saveBtn);
box.add(Box.createHorizontalStrut(3));
box.add(resetBtn);
box.add(Box.createHorizontalGlue());
box.add(Box.createHorizontalStrut(3));
box.add(deleteBtn);

return box;
}

/**
*
*/
private void rebuildUserList() {
MyContactsModel cm = (MyContactsModel) getModel();
DefaultComboBoxModel cbm = (DefaultComboBoxModel) userList.getModel();

List<MyUser> users = cm.getContacts();
cbm.removeAllElements();
for (Iterator<MyUser> iter = users.iterator(); iter.hasNext();) {
userList.addItem(iter.next());
}
}
That's really it for the view portion. Next time we will just point out the other framework libraries that are being used. By looking at all the code above, you can already see some of the frameworks being used such as EventBus notifications to the JStatusPanel and Simple Validation added to the form inputs.

Just to wrap up the application as a whole, I will end this post showing the code used to create and launch the app:

public class ContactsApp {

public static void main(final String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
creatAndShowUI(args);
}
});
}

private static void creatAndShowUI(String[] args) {
ContactService<MyUser> svc = new MyPeanutsService();//MyBradyService();
MyContactsModel model = new MyContactsModel(svc.getContacts());
MyContactsEditor view = new MyContactsEditor(model);

JFrame frame = new JFrame(view.getTitle());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);

frame.add(view, BorderLayout.NORTH);
UITools.centerAndShow(frame);
}
}
Until next time....

1 comment:

  1. I am also illustrating how difficult it is to follow code without comments. I hope we have all learned something from this; and by "we", I mean "me"

    ReplyDelete