I’ve been creating a lot of Eclipse Wizards lately, and it’s quite a challenge to keep the code clean, understandable and testable.  Eventually, I came up with a pattern that seems to work reasonably well.  I document it here for my and everybody else’s benefit.

Here are the key ideas:

  • Eclipse Facade:I am not a big fan of Eclipse Plug-In tests (vs. regular unit tests).   Therefore, everything that’s Eclipse-related should be as simple as possible, which we do by building a Facade.
  • Data Binding: We use data binding in the Wizard pages to keep them as simple as possible
  • Per-page Settings: Each page has a corresponding Settings object that manages the page’s data.  Specifically, it also validates the values for that page. There are no Eclipse- or GUI-dependencies in the Settings, so they can easily be tested.
  • Central settings store: The per-page settings are managed in a central settings store that contains all data  This allows us to persist the wizard settings (if desired), and also contains the finish() method that completes the execution of the wizard. This store has no GUI or Eclipse-dependencies either.

Here is a class diagram, for one page, but of course there could be multiple:

Here is a brief description of each element:

Wizard

The Wizard instantiates the Facade and the Store (which takes the Facade as an argument), and then creates the pages (which take the store as an argument).  It also calls performFinish() on the Store, which does all the hard work.

public class MyWizard extends Wizard implements IImportWizard {  	private ExchangeStore store;  	@Override 	public void init(IWorkbench workbench, IStructuredSelection selection) { 		IEclipseFacade facade = new EclipseFacade(selection); 		store = new ExchangeStore(facade);  		addPage(new SelectProjectPage(store)); 		addPage(new ImporterPage2SelectFile(store)); 		addPage(new ImporterPage3SelectAttributes(store)); 	}  	@Override 	public boolean performFinish() { 		return store.performFinish(); 	} } 

AbstractSettings

The AbstractSettings provide the Bean functionality, as described by Lars Vogel. It manages the registration of PropertyListeners and provides firePropertyChange() to announce changes.

Pages

I use an abstract base class for setting up the way I like to build GUIs.  But important is updateStatus(), which propagates the validation result.

 1  2  3  4  5  6  7  8  9 10 11 12
	protected void updateStatus(IStatus status) { 		setErrorMessage(null);  // Clear previous error messages - also braindead. 		setMessage(status.getMessage(), status.getSeverity()); 		if (status.getSeverity() == Status.OK) {  // Braindead: The message is not empty on ok! 			setMessage(null, Status.OK); 			setPageComplete(true); 		} else if (status.getSeverity() == Status.INFO) { 			setPageComplete(true); 		} else { 			setPageComplete(false); 		} 	} 

The pages themselves build the GUI and bind it to the Settings.  Key here is the DataBindingContext:

 1  2  3  4  5  6  7  8  9 10
		DataBindingContext ctx = new DataBindingContext();  		final Combo combo = new Combo(form.getBody(), SWT.READ_ONLY); 		combo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));  		ctx.bindList(WidgetProperties.items().observe(combo), BeanProperties 				.list("projectNames").observe(settings));  		ctx.bindValue(WidgetProperties.text().observe(combo), BeanProperties 				.value("projectName").observe(settings)); 

Note that we bind a List and a Value: The first is populating the combo with values, the second reacts to selections.

PageSettings

The PageSettings need getters for all the bound Widgets, and optionally setters, if they can change. Setters must fire, if they change a value.  Many values are just propagated from the Store, which may have an impact on the PageSettings.  If they have an impact, the PageSettings must register a listener on the Store to react to changes.  For example, Page 1 may allow the user to select a project, which would be set on the Store, which in turn would fire an event.  Page 2 may show the list of files.  Thus, Page 2 would register a listener on the Store that reacts to changes in the Project. Upon a change the list of files would be updated, which triggers firing of the corresponding property.  As an example, here the Settings for selecting a Project:

 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
public class SelectProjectSettings extends ExchangeSettings {  	private ExchangeStore store;  	public SelectProjectSettings(ExchangeStore store) { 		if (store == null) throw new NullPointerException(); 		this.store = store; 	} 	 	public List<String> getProjectNames() { 		return store.getProjectNames(); 	}  	public String getProjectName() { 		return store.getProjectName(); 	}  	public void setProjectName(String projectName) { 		String oldProjectName = store.getProjectName(); 		store.setProjectName(projectName); 		firePropertyChange("projectName", oldProjectName, getProjectName()); 	}  	public IStatus validatePage() { 		if (getProjectNames().contains(getProjectName())) { 			return ValidationStatus.ok(); 		} else { 			return ValidationStatus.warning("Please select a Project"); 		} 	} } 

 ExchangeStore

 The ExchangeStore is obviously very project specific.  But I want to demonstrate how it delegate Eclipse-specific information.  For instance, it contains the names of all Projects in the Workspace, which are used to populate the combo box in the wizard.  As this is Eclipse-specific, it is delegated to the Facade:

 1  2  3  4  5  6  7  8  9 10 11 12
public class ExchangeStore extends ExchangeSettings {  	private final IEclipseFacade facade;  	public ExchangeStore(IEclipseFacade facade) { 		this.facade = facade; 	} 	 	public List<String> getProjectNames() { 		return facade.getProjectNames(); 	}          ... much more stuff ... } 

Facade

And last, the facade abstracts Eclipse-specific, project-specific functionality. To pick up the example again, this is how we get the project names in the actual implementation:

1 2 3 4 5 6 7 8 9
	@Override 	public List<String> getProjectNames() { 		List<String> list = new ArrayList<String>(); 		IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); 		for (IProject project : projects) { 			list.add(project.getName()); 		} 		return list; 	}