About six months ago I designed and built a very lightweight framework for a Swing application. On reading through the Spring RCP User Documentation, it seems to share some ideas, although the emphases is quite different. My framework provided the application with a state machine rather than help with coding views, menus or controls.
Commands take the machine from one state to another. The state defines the views shown and the commands that are enabled. The commands were loosely based on Struts actions, with an execute method that returns the name of the next state rather than a reference to the next JSP or action.
The components
Command Factory
The command factory that manufactures command beans that the developer codes the execute method to do a particular piece of logic and return the name of the next state. Commands can also provide an isEnabled method that can disable the command even if the machine is in a state that declares that command. Controls and KeyStrokes that have disabled commands attached are disabled and controls are greyed out. The configuration wires up the keystroke and the button top text for each command. The application nominates one command as the boot command. This takes the state machine to the initial state and can be used to perform intialisation.
The State Factory
The state factory returns descriptions of states. A state bean holds:
1. the set of panels,
2. the InputMap/ActionMap for the state's commands
3. the default button and default focus component.
When a state becomes active, each panel will be attached to a corresponding containing panel. The application was designed with a static set of containers. For more complex applications I would have a view container factory with the state nominating a set of containers
The Panel Factory
The panels are coded by a developer. Panels (views) can optionally implement an interface whose methods are called when:
1. the panel is being displayed because of a shift in state.
2. a command is executed while a panel is shown.
I would liked to have introduced the idea of forms being passed to panels.
SwingAction
An implementation of javax.swing.Action that holds a command bean as a property, and passes it to a frame's request processor when the performAction is executed. The developer gets these from the comand factory to attach commands to buttons or other controls, and the state factory uses them to attach commands to ActionMaps.
The Request Processor
This manages:
1. The execution of the command
2. The detaching and attaching of panels to containers for the new state.
3. The attaching of the correct InputMap/ActionMap to the frame for the state.
4. The disabling and enabling of SwingActions depending on wheither their commands are associated with the current state.
5. The setting of the focus and the default button for the state.
This arrangement relieves the developer of the burden of managing the display of views and coding InputMaps/ActionMaps. I'm pretty pleased with the result. It's a bit simplistic, and probably only suits certain types of application, but then it was only intended for use in a point of sale system. I understand
The Model
However, one thing I really feel that I got wrong was the way I dealt with the model. I went with the bound JavaBeans approach to the model. Changes to a bean's properties fire change events to any registered listeners. The listeners then re-read the property and if they are controls re-draw themselves. This sounds great, but it doesn't work well when your data model is really an object graph rather than a single bean. When your Employee bean refers to an Address bean, and you want to swap your Employee bean for another, you end up with a load of plumbing to cope with re-bind the controls that were bound to the Address bean properties. In fact what really happens (if you are as dense as me) is that you write code to get the values out of your beans and put them into controls (and visa versa). When you look at indexed properties (or the List/Map/Set properties you need for Hibernate) it just gets worse.
I've been pondering this for a few months, and I now think I have a neat way of doing this. I have even partly coded it. It is just a shame it is all too late for the above project. However, I thought I'd share the idea. You might like it, you may think it is really old hat, or you may just laugh derisively, I don't mind.
Here goes:
I still using bound JavaBeans. They are generated from Hibernate mapping files, but I guess AOP could be used to fire the change events.
Then, rather than have controls register property change listeners with the beans directly, they register with a proxy, and the proxy registers a single listener with the bean. The proxy forwards the property change events from the bean to all its listeners, who read and update the property via the proxy. Once you have a set of control classes that bind to proxies in this way, creating swing panels with bound controls becomes easy:
When a new bean arrives from the server, it is put into the proxy by the workflow logic.Code:public class EmployeePanel extends JPanel { private BeanProxy employeeBeanProxy; public EmployeePanel (BeanProxy employeeBeanProxy){ this.employeeBeanProxy = employeeBeanProxy; this.add(new TextControl(employeeBeanProxy, "forename")); this.add(new TextControl(employeeBeanProxy, "surname")); } }
The proxy re-binds to the new bean and fires property change events to all the listeners to indicate that all the properties have changed.Code:employeeBeanProxy.setBean(employeeBean);
Here's the clever bit: to cope with a graph of beans, further proxies register listeners with the first proxy. The initial proxy becomes the root of a graph of proxies that reflects the structure of the graph of beans. When the first bean is swapped or a watched property changes, the chained proxies take the new property value (a JavaBean of course) and bind to it.
Still when a new employee bean arrives from the server, all the workflow logic needs to do is put it into the root bean proxy.Code:public class EmployeePanel extends JPanel { private BeanProxy employeeBeanProxy; private BeanProxy addressBeanProxy; public EmployeePanel (BeanProxy employeeBeanProxy){ this.employeeBeanProxy = employeeBeanProxy; this.add(new TextControl(employeeBeanProxy, "forename")); this.add(new TextControl(employeeBeanProxy, "surname")); this.addressBeanProxy = new BeanProxy(employeeBeanProxy, "address"); this.add(new TextControl(addressBeanProxy, "line1")); this.add(new TextControl(addressBeanProxy, "line2")); ... } }
This gives the controls something stable to bind to. As you can see they can be bound to proxies before any beans are created. Controls can just register with a proxy when they are created, update themselves when they get a property change event, and write any user changes back via the proxy.Code:employeeBeanProxy.setBean(employeeBean);
Indexed properties and Lists properties are slightly more complex, but I'm working on it. List properties require an implementation of List that fires property change events (perhaps I really should get around looking at AOP). In fact I am planning to use extend an abstract implementation of List that exposes the items in the list as an indexed property. Ultimately I want to be able to extend JTable so that it can bind to a List property like this:
This table will display the properties from all the beans in the list, I should have then only have to do presentation coding, but nothing to do with keeping the data upto date, other that set the company bean into the proxy.Code:public class CompanyPanel extends JPanel { private BeanProxy companyBeanProxy; public CompanyPanel (BeanProxy companyBeanProxy){ this.companyBeanProxy = companyBeanProxy; this.add(new BeanTable(companyBeanProxy, "employeeList", new String[]{"forename", "surname"})); ... } }
There are further ideas circling to do with controls that look at the property type and edit it accordingly, or even including BeanInfo attributes for properties to provide validation and formatting info, but that's probably not going to fit in with RCP.
I hope you found something interesting in this diatribe.
Reg.


Reply With Quote
It would probably be less effort to adapt Spring WebFlow than develop something from scratch, but the main benefit is that we could leverage all the development and attention WebFlow gets.