A Simple View on Complex Stuff
Canoo’s CTO Bruno Schäffer is speaking at SD West 08 on “Design Patterns for Rich Internet Applications”.
The following blog post outlines some of the concepts that he will cover in his talk. This is part three of a series of blog posts:
So far we discussed the deficiencies of plain MVC for complex user interfaces and how the presentation model approach can remedy this situation. The presentation model assumes most of the former responsibilities of the view/controller pair. What remains for the view/controller pair? Frankly spoken, not much:
- The view is responsible to create the user interface components and arrange them in a layout.
- All relevant events triggered by the user interface components are redirected to the appropriate methods of the presentation model
- The view is an observer of the presentation model. It has to react to the relevant changes to the presentation model and update the user interface accordingly.
Creating the user interface (besides design and ergonomical aspects) is trivial but cumbersome to code. However, we have the opportunity to employ some markup language for specifiying the UI. Markup language are not really suited to specify presentation logic, but since this is coded in the presentation model, views can easily be built this way. Examples for UI markup languages are SwiXml , ULC XML or XUL.
Redirecting events to the presentation model and reacting to changes of the presentation model is also pretty straightforward in most cases. The standard scenarios can be covered by generic infrastructure known as binding. I will discuss some aspects of binding in a later blog post.
Now let us have a look at a simple example to illustrate the concepts discussed so far.The example is based on the ULC component library, but the basic concepts can easily be transfered to most common component libraries. Here is a screenshot of the application to show what it looks like:

The application allows a user to load a record into the form, edit it and save it. There are two business objects involved, pertaining to the person and university. The presentation logic is simple: first name and last name must not be empty and the university id must relate to an existing university record. Validation is immediate, e.g. if the user enters the university id the university name field will be updated immediately. The presentation model captures the necessary state and logic. Below you can see the implementation stripped down to the properties personName and personFirstName (the observer infrastructure is left out too). It is fairly simple: since the presentation model is a Java Bean, we need to provide setters and getters which map to the person object referenced by the presentation model. The property personName has only a getter since it is a read only property. The setter validates the property and updates the errorList property accordingly.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class SimpleFormPresentationModel { private Person fPerson; private ErrorList fErrorList; public final static String PERSON_NAME = "personName"; public final static String PERSON_FIRST_NAME = "personFirstName"; public final static String ERROR_LIST = "errorList"; public SimpleFormPresentationModel() { fireAllPropertiesChanged(); } public String getPersonFirstName() { return getPerson().getFirstName(); } public void setPersonFirstName(String firstName) { getPerson().setFirstName(firstName); validate(PERSON_FIRST_NAME); firePropertyChanged(PERSON_NAME, getPersonName(getPerson())); } public String getPersonName(Person person) { return person != null ? person.getFirstName() + " " + person.getLastName() : ""; } } |
The view class is pretty straightforward. Basically, it creates the UI, hooks up the UI components to the presentation model and reacts to the changes of the presentation model. The implementation uses the UltraLightClient component library. The stripped down view class (incl. some convenience methods) looks like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
public class SimpleFormPane implements IPropertyChangeListener { private ULCBoxPane fFormPane; private ULCTextField fName; private ULCTextField fFirstName; private SimpleFormPresentationModel fFormPresentationModel; private Color fErrorColor = new Color(255, 0, 0, 192); private static final String[] UI_FIELDS = new String[]{"firstName"}; public SimpleFormPane(SimpleFormPresentationModel formPresentationModel) { fFormPresentationModel = formPresentationModel; } public ULCComponent getPane() { if (fFormPane == null) { fFormPane = createForm(); fFormPresentationModel.addPropertyChangeListener(PERSON_NAME, this); fFormPresentationModel.addPropertyChangeListener(PERSON_FIRST_NAME, this); addValueChangedListener(PERSON_FIRST_NAME, fFirstName); } return fFormPane; } private void addValueChangedListener(final String property, final ULCTextComponent textComponent) { textComponent.addValueChangedListener(new IValueChangedListener() { public void valueChanged(ValueChangedEvent event) { if (textComponent instanceof ULCTextField && ((ULCTextField)textComponent).getDataType() != null) { fFormPresentationModel.setProperty(property, ((ULCTextField)textComponent).getValue()); } else { fFormPresentationModel.setProperty(property, textComponent.getText()); } } }); } private ULCBoxPane createForm() { ULCBoxPane boxPane = new ULCBoxPane(2, 1, 5, 5); boxPane.add(2, "cc", new ULCLabel(HTMLUtilities.convertToHtml("<strong>Person Data</strong>"))); boxPane.add("rc", new ULCLabel("Name:")); fName = new ULCTextField(); fName.setEnabled(false); boxPane.add("ec", fName); boxPane.add("rc", new ULCLabel("First Name:")); fFirstName = new ULCTextField(); boxPane.add("ec", fFirstName); boxPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); return boxPane; } public void update(String property, Object oldValue, Object newValue) { if (property.equals(PERSON_NAME)) { fName.setText((String)newValue); } else if (property.equals(PERSON_FIRST_NAME)) { fFirstName.setText(((String)newValue)); } else if (property.equals(ERROR_LIST)) { ErrorList errorList = (ErrorList)newValue; for (at.diefaehre.argo.pm.util.Error error : errorList) { ULCComponent component = getComponent(error.getProperty()); if (component != null) { if (error.isError()) { component.setBackground(fErrorColor); } else { component.setBackground(Color.white); } } } setFocusToFirstError(errorList); } } private void setFocusToFirstError(ErrorList errorList) { for (String field : UI_FIELDS) { if (errorList.containsError(field)) { ULCComponent component = getComponent(field); if (component != null) { component.requestFocus(); break; } } } } private ULCComponent getComponent(String name) { ULCComponent component = null; try { component = (ULCComponent)PropertyUtils.getProperty(this, name); } catch (Exception e) { } return component; } public ULCTextField getName() { return fName; } public ULCTextField getFirstName() { return fFirstName; } } |
What would test cases for this simple scenario look like? The view contains no real logic and therefore we do not need to write any test cases. However, we should make sure that the presentation model updates its properties correctly (incl. validation) and triggers the appropriate update events. Tests for our presentation model could look like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Test public void testValidFirstName() { attachPropertyChangeRecorder(fPresentationModel); fPresentationModel.setPerson(Service.getService().getPersonById(1)); fPresentationModel.setProperty(PERSON_FIRST_NAME, "Bruno"); assertFalse(fErrorList.hasErrors()); testPropertyChangeMap(PERSON_NAME, PERSON_FIRST_NAME, ERROR_LIST); } @Test public void testInvalidFirstName() { attachPropertyChangeRecorder(fPresentationModel); fPresentationModel.setPerson(Service.getService().getPersonById(1)); fPresentationModel.setProperty(PERSON_FIRST_NAME, ""); assertTrue(fErrorList.hasErrors()); assertEquals("firstName", fErrorList.getError().getProperty()); assertEquals(1, fErrorList.getError().getErrorNr()); testPropertyChangeMap(PERSON_NAME, PERSON_FIRST_NAME, ERROR_LIST); } |
The first test case starts with attaching a person to the presentation model and then sets the personFirstName property. There are two checks: one whether no errors have been produced and the other checks whether all expected update events have been triggered. For this an event recorder is attached (attachPropertyChangeRecorder()) and testPropertyChangeMap verifies the expected update events.
As shown above, this blog post looked at simple views and how they are implemented with a presentation model running underneath. The above example demonstrated the concepts introduced so far. Obviously, there are still some drawbacks to this simple approach. It will not easily scale to more complex user interfaces (e.g. a master detail view with tabbed forms) and contains quite a lot of repetitive code. In the next blog post, we will investigate how more complex user interfaces can be tackled.
Part 1: MVC and the Brave New World of RIA
Part 2: The World Needs More Models
Part 3: A Simple View on Complex Stuff
Part 4: Hierarchies
Part 5: Presentation Model Framework
Part 6: What’s Needed Besides the Presentation Model












