I’ve been quietly hacking away at the Eclipse Business Expense Reporting Tool (EBERT) from the Examples project for some time. My goal is to turn this application into a shining example of how you can go about making a real (albeit relatively simple) application based on Eclipse RCP, eRCP, and RAP.
It’s taken me an embarrassing while to sort out coordination of the views. The application is composed of three views. In the RCP and RAP versions, the views are composed as shown below with the BinderView on the left, the ExpenseReportView on the top-right and the LineItemView on the bottom-right. In the eRCP version, the views are stacked and selectively exposed.
At first, I used the selection service to coordinate the views. The BinderView’s ListViewer is, for example, configured to be a selection provider and the ExpenseReportView is configured to be a selection consumer: when an ExpenseReport is selected, the ExpenseReportView updates itself. One nice thing about this mechanism is that it allows the views to be totally decoupled from one another and further allows other views to act as selection providers. Another very nice thing about using the selection service for this is that it works well in the multiple-user RAP environment since each user gets their own copy of the workbench (and, by extension, their own copy of the selection service). On the downside managing things like the case when, for example, a selected ExpenseReport is removed requires some work (when an ExpenseReport is deleted from the BinderView, you should expect the ExpenseReportView and LineItemView to become empty). It’s not particularly difficult to manage this, but it feels a little cludgy. That, and the selection service isn’t actually implemented in eRCP (because, it’s really not useful in that environment).
I opted instead to introduce the ViewModel class. ViewModel provides, as the name suggests, a model of my view. For this application, the view model is pretty simple: it keeps track of the “current” ExpensesBinder, ExpenseReport, and LineItem. The various views all tell the view model about the user’s activities. The view model is updated in response to those activities, and notifies the various views about the change.
When, for example, an ExpenseReport is selected in the BinderView, the view model is informed of the change:
// TODO Consider using addPostSelectionChangedListener instead expenseReportViewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = (IStructuredSelection)event.getSelection(); getViewModel().setReport((ExpenseReport) selection.getFirstElement()); } });
The view model responds by setting the “current” ExpenseReport is set to the newly selected one and the “current” LineItem is to null
. Then all the listeners are informed of the change. The code is a pretty straightforward implementation of the observer pattern:
public void setReport(ExpenseReport report) { this.report = report; this.lineItem = null; Object[] listeners = listenerList.getListeners(); for(int index=0;index<listeners.length;index++) { IViewModelListener listener = (IViewModelListener) listeners[index]; listener.reportChanged(this.report); listener.lineItemChanged(this.lineItem); } }
Each of the views listen to the view model. When the observer notifies them of the change, they react appropriately. The LineItemView, for example, responds to the lineItemChanged(LineItem)
method by updating itself to reflect the change:
IViewModelListener viewModelListener = new IViewModelListener() { public void binderChanged(ExpensesBinder binder) { } public void lineItemChanged(final LineItem item) { syncExec(new Runnable() { public void run() { setLineItem(item); }; }); } public void reportChanged(ExpenseReport report) {} };
The other types and views are handled in a similar way.
The big challenge with this approach is, of course, the multiple-user environment of RAP. To manage this, I needed to come up with a notion of user state. The view model is just part of the user state.
I’ll talk about this next time…
Please poke holes in this if there’s something that can be done better.