Skip to main content


Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Eclipse Projects » JFace » [databinding] exceptions in List-typed model property binding to Viewer multi selection
[databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629480] Tue, 28 September 2010 12:58 Go to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
This may be a bug in Eclipse databinding but I'm posting here to check if I did something wrong before posting a bug report.

I wish to bind the multi selection of a ListViewer to a model property of type List. The elements in the model are (simulated to be) fetched from a database and should be compared by an Id field.

I have an example based on Snippet000HelloWorld.java with a ViewModel object that is a JavaBean with a "person property" of type Person. This object has a JavaBean property of type List<Job>. Each Job has a database id and each element of the list must be inside an array of all possible Job objects.

When I override the equals method to compare on Id everything works fine. My binding code looks like this:
			IObservableValue master = BeansObservables.observeValue(viewModel,
					"person");
			IObservableList jobsObservableList = BeansObservables
					.observeDetailList(master, "jobs", null);
			IViewerObservableList observeMultiSelection = ViewersObservables
					.observeMultiSelection(jobsViewer);
			final Binding binding = bindingContext.bindList(
					observeMultiSelection, jobsObservableList);


I then simulate reloading data from the database by setting a new Person object to the ViewModel which has a new List of Jobs that are equals but not the same to the previous Person object, because we get a different copy from the database.

The SnippetModelToViewMultiSelectionIdEquals is a complete runnable example which shows that this binding to the ListViewer works correctly.

However I don't want to override equals because this may give other problems in databinding especially with master/detail because a change may go unnoticed if data changes but id stays the same. (See e.g. my post here).

The usual way to solve this "I don't want to override equals" problem is by setting an IElementComparer on the Viewer that compares by id. But here things go wrong when I simulate reloading the model from the database and automatically updating the ListViewer through the binding. The reloaded model has a list with elements that are equal to elements in the ListViewer according to the IElementComparer but not according to the equals method.

Now Eclipse databinding throws java.lang.IndexOutOfBoundsExceptions!
See SnippetModelToViewMultiSelectionIElementComparerBug!

Finally, I thought I had the solution: use a Converter on the modelToTarget binding that directly converts a list element from the model to a reference to the element in the viewer that has the same Id! Somehow this doesn't work and I get the same Exception.

I can post this snippet on request as well.

[Updated on: Tue, 28 September 2010 12:59]

Report message to a moderator

Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629481 is a reply to message #629480] Tue, 28 September 2010 13:03 Go to previous messageGo to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.conversion.Converter;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.viewers.IViewerObservableList;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * Adaptation of
 * <code>org.eclipse.jface.examples.databinding.snippets.Snippet000HelloWorld.java</code>
 * .
 * 
 * <p>
 * Together with {@link SnippetModelToViewMultiSelectionIElementComparerBug} and
 * {@link SnippetModelToViewMultiSelectionIElementComparerAndConverter} this
 * illustrates:
 * 
 * <ul>
 * <li>You can allow reloading an entire model object by using master-detail
 * binding. We simulate this by setting a new business object that is logically
 * equal to the original (both it's data and database id's) but not by Java
 * object identity.</li>
 * <li>Binding a model list to multi selection in a {@link ListViewer} works
 * fine if you override the equals method in your business entity to compare on
 * database id.</li>
 * <li>Multi selection binding does <b>not work fine</b> this way when you do
 * not override equals but use an {@link IElementComparer} in the viewer! Model
 * to view binding fails.</li>
 * <li>This bug is not solvable without delving into Eclipse databinding
 * internals. Unfortunately I can't even solve it using a {@link Converter} that
 * always returns an element of the list in the Viewer.</li>
 * </ul>
 * 
 * For the third and fourth point: on Eclipse 3.5 you get an
 * {@link IndexOutOfBoundsException} by playing around with selection and
 * pushing the reload button. (I already get it by only pushing the button
 * twice).
 * 
 * <p>
 * Note that overriding equals in the business entity is not always allowed but
 * even worse, it can unfortunately give other issues with data binding, see <a
 * href="http://www.eclipse.org/forums/index.php?t=msg&goto=546571&S=c7cf4c80f6fcf74768d1f36d163ca525"
 * >my forum post here</a>.
 * 
 * @author Henno Vermeulen
 */
public class SnippetModelToViewMultiSelectionIdEquals {

	private static final Job[] SELECTABLE_JOBS = new Job[] {
			new Job(1l, "DBA"), new Job(2l, "System architect"),
			new Job(3l, "UI designer") };

	public static void main(String[] args) {
		Display display = new Display();
		final ViewModel viewModel = new ViewModel();

		Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() {
			public void run() {
				final Shell shell = new View(viewModel, SELECTABLE_JOBS)
						.createShell();
				// The SWT event loop
				Display display = Display.getCurrent();
				while (!shell.isDisposed()) {
					if (!display.readAndDispatch()) {
						display.sleep();
					}
				}
			}
		});
		// Print the results
		System.out.println("person.getJobs() = "
				+ viewModel.getPerson().getJobs());
	}

	// Minimal JavaBeans support
	public static abstract class AbstractModelObject {
		private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
				this);

		public void addPropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(listener);
		}

		public void addPropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(propertyName,
					listener);
		}

		public void removePropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(listener);
		}

		public void removePropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(propertyName,
					listener);
		}

		protected void firePropertyChange(String propertyName, Object oldValue,
				Object newValue) {
			propertyChangeSupport.firePropertyChange(propertyName, oldValue,
					newValue);
		}
	}

	// The data model class. This is normally a persistent class of some sort.
	static class Person extends AbstractModelObject {
		// A property...
		private List<Job> jobs = new ArrayList<Job>();

		public List<Job> getJobs() {
			return jobs;
		}

		public void setJobs(List<Job> newJobs) {
			System.out.println("person.setJobs: " + newJobs);
			List<Job> oldList = jobs;
			this.jobs = newJobs;
			firePropertyChange("jobs", oldList, jobs);
		}

	}

	/**
	 * This is actually what I would want but gives other issues as explained <a
	 * href="http://www.eclipse.org/forums/index.php?t=msg&goto=546571&S=c7cf4c80f6fcf74768d1f36d163ca525"
	 * >my forum post here</a>.
	 */
	static class Job extends AbstractModelObject {

		// database id
		private final Long id;
		private String name;

		public Job(Long id, String name) {
			this.id = id;
			this.name = name;
		}

		@Override
		public String toString() {
			return getName();
		}

		public Long getId() {
			return id;
		}

		public void setName(String name) {
			String oldName = name;
			this.name = name;
			firePropertyChange("name", oldName, name);
		}

		public String getName() {
			return name;
		}

		public Job getCopy() {
			return new Job(id, name);
		}

		// hashCode() and equals(Object) generated by Eclipse using the id
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((id == null) ? 0 : id.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Job other = (Job) obj;
			if (id == null) {
				if (other.id != null)
					return false;
			} else if (!id.equals(other.id))
				return false;
			return true;
		}

	}

	// The View's model--the root of our Model graph for this particular GUI.
	//
	// Typically each View class has a corresponding ViewModel class.
	// The ViewModel is responsible for getting the objects to edit from the
	// DAO. We simulate this by simply instantiating a model object to edit.
	// We also simulate the fact that we will probably get other instances of
	// the same model objects (the jobs) that are logically equal (same id)
	// but not equal with the equals method so that we must use an
	// IElementComparer in our ListViewer.
	static class ViewModel extends AbstractModelObject {
		// The model to bind
		private Person person;

		public ViewModel() {
			simulateLoadPersonFromDao();
		}

		public Person getPerson() {
			return person;
		}

		private void simulateLoadPersonFromDao() {
			Person p = new Person();

			List<Job> jobs = new ArrayList<Job>();
			// Note the getCopy()!!!
			jobs.add(SELECTABLE_JOBS[0].getCopy());
			jobs.add(SELECTABLE_JOBS[2].getCopy());
			p.setJobs(jobs);

			setPerson(p);
		}

		public void setPerson(Person newPerson) {
			Person oldPerson = person;
			this.person = newPerson;
			firePropertyChange("person", oldPerson, person);
		}
	}

	// The GUI view
	static class View {
		private ViewModel viewModel;
		private ListViewer jobsViewer;
		private Job[] selectableJobs;

		public View(ViewModel viewModel, Job[] selectableJobs) {
			this.viewModel = viewModel;
			this.selectableJobs = selectableJobs;
		}

		public Shell createShell() {
			// Build a UI
			Display display = Display.getDefault();
			Shell shell = new Shell(display);
			shell.setLayout(new RowLayout(SWT.VERTICAL));
			Button replace = new Button(shell, SWT.BORDER);
			replace.setText("Reload Person From Dao");
			replace.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					viewModel.simulateLoadPersonFromDao();
				}
			});

			jobsViewer = new ListViewer(shell, SWT.BORDER | SWT.MULTI);
			jobsViewer.setContentProvider(new ArrayContentProvider());
			jobsViewer.setInput(selectableJobs);

			createBindings();

			// Open and return the Shell
			shell.pack();
			shell.open();
			return shell;
		}

		private void createBindings() {
			// Bind it
			DataBindingContext bindingContext = new DataBindingContext();
			// We use master-detail to simulate the possibility of replacing
			// the entire Person model object (e.g. when refreshing data from a
			// database).
			IObservableValue master = BeansObservables.observeValue(viewModel,
					"person");

			IObservableList jobsObservableList = BeansObservables
					.observeDetailList(master, "jobs", null);
			IViewerObservableList observeMultiSelection = ViewersObservables
					.observeMultiSelection(jobsViewer);
			observeMultiSelection
					.addListChangeListener(new IListChangeListener() {
						@Override
						public void handleListChange(ListChangeEvent event) {
							System.out.println("Viewer selection changed to "
									+ jobsViewer.getSelection());
						}
					});
			final Binding binding = bindingContext.bindList(
					observeMultiSelection, jobsObservableList);

			jobsObservableList.addChangeListener(new IChangeListener() {
				@Override
				public void handleChange(ChangeEvent event) {
					checkBindingStatusForExceptions(binding);
				}
			});
		}
	}

	private static void checkBindingStatusForExceptions(Binding binding) {
		IStatus status = (IStatus) binding.getValidationStatus().getValue();
		if (status.getException() != null) {
			System.err.println("Exception in databinding:");
			status.getException().printStackTrace();
		}
	}

}
Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629482 is a reply to message #629480] Tue, 28 September 2010 13:03 Go to previous messageGo to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.viewers.IViewerObservableList;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * @author Henno Vermeulen
 */
public class SnippetModelToViewMultiSelectionIElementComparerBug {

	private static final Job[] SELECTABLE_JOBS = new Job[] {
			new Job(1l, "DBA"), new Job(2l, "System architect"),
			new Job(3l, "UI designer") };

	private static class JobComparer implements IElementComparer {

		@Override
		public boolean equals(Object a, Object b) {
			return ((Job) a).getId().equals(((Job) b).getId());
		}

		@Override
		public int hashCode(Object element) {
			return ((Job) element).getId().hashCode();
		}

	}

	public static void main(String[] args) {
		Display display = new Display();
		final ViewModel viewModel = new ViewModel();

		Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() {
			public void run() {
				final Shell shell = new View(viewModel, SELECTABLE_JOBS)
						.createShell();
				// The SWT event loop
				Display display = Display.getCurrent();
				while (!shell.isDisposed()) {
					if (!display.readAndDispatch()) {
						display.sleep();
					}
				}
			}
		});
		// Print the results
		System.out.println("person.getJobs() = "
				+ viewModel.getPerson().getJobs());
	}

	// Minimal JavaBeans support
	public static abstract class AbstractModelObject {
		private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
				this);

		public void addPropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(listener);
		}

		public void addPropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(propertyName,
					listener);
		}

		public void removePropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(listener);
		}

		public void removePropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(propertyName,
					listener);
		}

		protected void firePropertyChange(String propertyName, Object oldValue,
				Object newValue) {
			propertyChangeSupport.firePropertyChange(propertyName, oldValue,
					newValue);
		}
	}

	// The data model class. This is normally a persistent class of some sort.
	static class Person extends AbstractModelObject {
		// A property...
		private List<Job> jobs = new ArrayList<Job>();

		public List<Job> getJobs() {
			return jobs;
		}

		public void setJobs(List<Job> newJobs) {
			System.out.println("person.setJobs: " + newJobs);
			List<Job> oldList = jobs;
			this.jobs = newJobs;
			firePropertyChange("jobs", oldList, jobs);
		}

	}

	/**
	 * This is actually what I would want but gives other issues as explained <a
	 * href="http://www.eclipse.org/forums/index.php?t=msg&goto=546571&S=c7cf4c80f6fcf74768d1f36d163ca525"
	 * >my forum post here</a>.
	 */
	static class Job extends AbstractModelObject {

		// database id
		private final Long id;
		private String name;

		public Job(Long id, String name) {
			this.id = id;
			this.name = name;
		}

		@Override
		public String toString() {
			return getName();
		}

		public Long getId() {
			return id;
		}

		public void setName(String name) {
			String oldName = name;
			this.name = name;
			firePropertyChange("name", oldName, name);
		}

		public String getName() {
			return name;
		}

		public Job getCopy() {
			return new Job(id, name);
		}

		// hashCode() and equals(Object) not overridden

	}

	// The View's model--the root of our Model graph for this particular GUI.
	//
	// Typically each View class has a corresponding ViewModel class.
	// The ViewModel is responsible for getting the objects to edit from the
	// DAO. We simulate this by simply instantiating a model object to edit.
	// We also simulate the fact that we will probably get other instances of
	// the same model objects (the jobs) that are logically equal (same id)
	// but not equal with the equals method so that we must use an
	// IElementComparer in our ListViewer.
	static class ViewModel extends AbstractModelObject {
		// The model to bind
		private Person person;

		public ViewModel() {
			simulateLoadPersonFromDao();
		}

		public Person getPerson() {
			return person;
		}

		private void simulateLoadPersonFromDao() {
			Person p = new Person();

			List<Job> jobs = new ArrayList<Job>();
			// Note the getCopy()!!!
			jobs.add(SELECTABLE_JOBS[0].getCopy());
			jobs.add(SELECTABLE_JOBS[2].getCopy());
			p.setJobs(jobs);

			setPerson(p);
		}

		public void setPerson(Person newPerson) {
			Person oldPerson = person;
			this.person = newPerson;
			firePropertyChange("person", oldPerson, person);
		}
	}

	// The GUI view
	static class View {
		private ViewModel viewModel;
		private ListViewer jobsViewer;
		private Job[] selectableJobs;

		public View(ViewModel viewModel, Job[] selectableJobs) {
			this.viewModel = viewModel;
			this.selectableJobs = selectableJobs;
		}

		public Shell createShell() {
			// Build a UI
			Display display = Display.getDefault();
			Shell shell = new Shell(display);
			shell.setLayout(new RowLayout(SWT.VERTICAL));
			Button replace = new Button(shell, SWT.BORDER);
			replace.setText("Reload Person From Dao");
			replace.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					viewModel.simulateLoadPersonFromDao();
				}
			});

			jobsViewer = new ListViewer(shell, SWT.BORDER | SWT.MULTI);
			jobsViewer.setContentProvider(new ArrayContentProvider());
			jobsViewer.setInput(selectableJobs);
			jobsViewer.setComparer(new JobComparer());

			createBindings();

			// Open and return the Shell
			shell.pack();
			shell.open();
			return shell;
		}

		private void createBindings() {
			// Bind it
			DataBindingContext bindingContext = new DataBindingContext();
			// We use master-detail to simulate the possibility of replacing
			// the entire Person model object (e.g. when refreshing data from a
			// database).
			IObservableValue master = BeansObservables.observeValue(viewModel,
					"person");

			IObservableList jobsObservableList = BeansObservables
					.observeDetailList(master, "jobs", null);
			IViewerObservableList observeMultiSelection = ViewersObservables
					.observeMultiSelection(jobsViewer);
			observeMultiSelection
					.addListChangeListener(new IListChangeListener() {
						@Override
						public void handleListChange(ListChangeEvent event) {
							System.out.println("Viewer selection changed to "
									+ jobsViewer.getSelection());
						}
					});
			final Binding binding = bindingContext.bindList(
					observeMultiSelection, jobsObservableList);

			jobsObservableList.addChangeListener(new IChangeListener() {
				@Override
				public void handleChange(ChangeEvent event) {
					checkBindingStatusForExceptions(binding);
				}
			});
		}
	}

	private static void checkBindingStatusForExceptions(Binding binding) {
		IStatus status = (IStatus) binding.getValidationStatus().getValue();
		if (status.getException() != null) {
			System.err.println("Exception in databinding:");
			status.getException().printStackTrace();
		}
	}

}
Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629531 is a reply to message #629480] Tue, 28 September 2010 15:51 Go to previous messageGo to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
It is so annoying that a little thing like this (at the moment we only need one list binding...) can take so much time to solve...

I tried overloading equals only for this business object, but then the part of our program where we can edit these objects (our table data binding code) fails: I set an IObservableList as input for the table, but it does not detect certain changes if the id of an element stays the same!

Maybe I should create a wrapper object that has the right equals?

I really admire the cleverness that went into Eclipse databinding (for example master-detail) but unfortunately it is not the goose with the golden eggs, it can suck a lot of development time to get it right.

[Updated on: Tue, 28 September 2010 16:04]

Report message to a moderator

Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629646 is a reply to message #629531] Wed, 29 September 2010 05:46 Go to previous messageGo to next message
Matthew Hall is currently offline Matthew HallFriend
Messages: 368
Registered: July 2009
Senior Member
On 09/28/2010 09:51 AM, SlowStrider wrote:
> It is so annoying that a little thing like this (at the moment we only
> need one list binding...) can take so much time to solve...
>
> I tried overloading equals only for this business object, but then the
> part of our program where we can edit these objects (our table data
> binding code) fails...
>
> I really admire the cleverness that went into Eclipse databinding (for
> example master-detail) but unfortunately it is not the goose with the
> golden eggs, it can suck a lot of development time to get it right.

I agree, this is a serious problem. This issue has been discussed at
length:

https://bugs.eclipse.org/bugs/show_bug.cgi?id=278301
https://bugs.eclipse.org/bugs/show_bug.cgi?id=307585

The holdup, as I see it, is that this would be a sweeping change in
behavior, and doing so might cause unforeseen regressions. It's
difficult to tell how such a change might affect existing code. It's a
tough nut to crack.

We could definitely use some community involvement in helping to solve
this problem.

Matthew
Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629662 is a reply to message #629646] Wed, 29 September 2010 07:56 Go to previous messageGo to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
Thank you, good to know I'm not the only one having problems with equals I added myself to the CC lists.

I already found out that most problems can be solved by not overriding equals (default VM identity comparison with ==) and using an IElementComparer on Viewers that compare using what you want equals to be (by database id).

Do you happen to have any idea for a solution for my current problem with multi-selection binding? It's already taken me one and a half day and the only thing I can think of trying now is using wrapper objects that have the equals method by Id, perhaps in combination with a Converter.

[Updated on: Wed, 29 September 2010 07:58]

Report message to a moderator

Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629699 is a reply to message #629646] Wed, 29 September 2010 10:11 Go to previous messageGo to next message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
Well it seems I actually found two bugs with DataBindingContext.bindList(...).

My Converter trick that converts a copy of a model object into a reference to an object that already exists in the viewer's list should actually work but there is also an issue with the Converter not always being used.

I created this bug report
https://bugs.eclipse.org/bugs/show_bug.cgi?id=326507
Re: [databinding] exceptions in List-typed model property binding to Viewer multi selection [message #629742 is a reply to message #629699] Wed, 29 September 2010 12:47 Go to previous message
Henno Vermeulen is currently offline Henno VermeulenFriend
Messages: 126
Registered: July 2009
Senior Member
A found a reasonable workaround for the problem by using a Value Binding and IElementComparer.

I convert the IObservableList of multi-selection to an IObservableValue and subsequently use a Value Binding to the model property. No converter is necessary by using the usual IElementComparer on the Viewer.

No change is required to the model and no converters are required.

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.observable.ChangeEvent;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.viewers.IViewerObservableList;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IElementComparer;
import org.eclipse.jface.viewers.ListViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * Adaptation of
 * <code>org.eclipse.jface.examples.databinding.snippets.Snippet000HelloWorld.java</code>
 * .
 * 
 * <p>
 * This illustrates that multi selection binding can be made to work right (see
 * <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=326507">this bug
 * report why it normally doesn't</a> by converting the {@link IObservableList}
 * to an {@link IObservableValue} and using a normal value Binding in
 * combination with using an {@link IElementComparer} on the Viewer.
 * 
 * @author Henno Vermeulen
 */
public class SnippetModelToViewMultiSelectionWorkaroundWithObservableValue {

	/**
	 * An {@link IObservableValue} which wraps {@link IObservableList} and
	 * represents a {@link List}-typed value that is synchronized with the
	 * contents of the wrapped observable list.
	 * 
	 * <p>
	 * Useful for implementing a list data binding as a value binding. This is a
	 * workaround for <a
	 * href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=326507">bugs</a> in
	 * {@link DataBindingContext#bindList(IObservableList, IObservableList)}.
	 * 
	 * <p>
	 * Disclaimer: not sure if this class is written 100% correctly but it does
	 * the job!
	 * 
	 * @author Henno Vermeulen
	 */
	public static class ObservableListValue extends WritableValue {

		private final IObservableList list;
		private boolean inSynchronizeValueToList;
		private IListChangeListener listChangeListener;

		/**
		 * Private constructor, use {@link #newInstance(IObservableList)}
		 * instead.
		 */
		private ObservableListValue(final IObservableList list) {
			this.list = list;
			listChangeListener = new IListChangeListener() {
				@Override
				public void handleListChange(ListChangeEvent event) {
					if (!inSynchronizeValueToList) {
						synchronizeObservableListToValue();
					}
				}
			};
		}

		/**
		 * @return a new {@link ObservableListValue} that is synchronized with
		 *         <code>listToWrap</code>.
		 */
		public static ObservableListValue newInstance(
				final IObservableList listToWrap) {
			ObservableListValue instance = new ObservableListValue(listToWrap);
			instance.synchronizeObservableListToValue();
			listToWrap.addListChangeListener(instance.listChangeListener);
			return instance;
		}

		private void synchronizeObservableListToValue() {
			// Simply setting the list itself does not fire a change event the
			// second time!
			super.doSetValue(new ArrayList(list));
		}

		private void synchronizeValueToObservableList() {
			inSynchronizeValueToList = true;
			// TODO optimization this throws two list change events
			list.clear();
			list.addAll((Collection) getValue());
			inSynchronizeValueToList = false;
		}

		@Override
		public void doSetValue(Object value) {
			super.doSetValue(value);
			synchronizeValueToObservableList();
		}

		@Override
		public synchronized void dispose() {
			list.removeListChangeListener(listChangeListener);
			super.dispose();
		}
	}

	private static final Job[] SELECTABLE_JOBS = new Job[] {
			new Job(1l, "DBA"), new Job(2l, "System architect"),
			new Job(3l, "UI designer") };

	private static class JobComparer implements IElementComparer {

		@Override
		public boolean equals(Object a, Object b) {
			return ((Job) a).getId().equals(((Job) b).getId());
		}

		@Override
		public int hashCode(Object element) {
			return ((Job) element).getId().hashCode();
		}

	}

	public static void main(String[] args) {
		Display display = new Display();
		final ViewModel viewModel = new ViewModel();

		Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() {
			public void run() {
				final Shell shell = new View(viewModel).createShell();
				// The SWT event loop
				Display display = Display.getCurrent();
				while (!shell.isDisposed()) {
					if (!display.readAndDispatch()) {
						display.sleep();
					}
				}
			}
		});
		// Print the results
		System.out.println("person.getJobs() = "
				+ viewModel.getPerson().getJobs());
	}

	// Minimal JavaBeans support
	public static abstract class AbstractModelObject {
		private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(
				this);

		public void addPropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(listener);
		}

		public void addPropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.addPropertyChangeListener(propertyName,
					listener);
		}

		public void removePropertyChangeListener(PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(listener);
		}

		public void removePropertyChangeListener(String propertyName,
				PropertyChangeListener listener) {
			propertyChangeSupport.removePropertyChangeListener(propertyName,
					listener);
		}

		protected void firePropertyChange(String propertyName, Object oldValue,
				Object newValue) {
			propertyChangeSupport.firePropertyChange(propertyName, oldValue,
					newValue);
		}
	}

	// The data model class. This is normally a persistent class of some sort.
	static class Person extends AbstractModelObject {
		// A property...
		private List<Job> jobs = new ArrayList<Job>();

		public List<Job> getJobs() {
			return jobs;
		}

		public void setJobs(List<Job> newJobs) {
			System.out.println("person.setJobs: " + newJobs);
			List<Job> oldList = jobs;
			this.jobs = newJobs;
			firePropertyChange("jobs", oldList, jobs);
		}

	}

	/**
	 * This is actually what I would want but gives other issues as explained <a
	 * href="http://www.eclipse.org/forums/index.php?t=msg&goto=546571&S=c7cf4c80f6fcf74768d1f36d163ca525"
	 * >my forum post here</a>.
	 */
	static class Job extends AbstractModelObject {

		// database id
		private final Long id;
		private String name;

		public Job(Long id, String name) {
			this.id = id;
			this.name = name;
		}

		@Override
		public String toString() {
			return getName();
		}

		public Long getId() {
			return id;
		}

		public void setName(String name) {
			String oldName = name;
			this.name = name;
			firePropertyChange("name", oldName, name);
		}

		public String getName() {
			return name;
		}

		public Job getCopy() {
			return new Job(id, name);
		}

		// hashCode() and equals(Object) not overridden

	}

	// The View's model--the root of our Model graph for this particular GUI.
	//
	// Typically each View class has a corresponding ViewModel class.
	// The ViewModel is responsible for getting the objects to edit from the
	// DAO. We simulate this by simply instantiating a model object to edit.
	// We also simulate the fact that we will probably get other instances of
	// the same model objects (the jobs) that are logically equal (same id)
	// but not equal with the equals method so that we must use an
	// IElementComparer in our ListViewer.
	static class ViewModel extends AbstractModelObject {
		// The model to bind
		private Person person;

		public ViewModel() {
			simulateLoadPersonFromDao();
		}

		public Person getPerson() {
			return person;
		}

		private void simulateLoadPersonFromDao() {
			Person p = new Person();

			List<Job> jobs = new ArrayList<Job>();
			// Note the getCopy()!!!
			jobs.add(SELECTABLE_JOBS[0].getCopy());
			jobs.add(SELECTABLE_JOBS[2].getCopy());
			p.setJobs(jobs);

			setPerson(p);
		}

		public void setPerson(Person newPerson) {
			Person oldPerson = person;
			this.person = newPerson;
			firePropertyChange("person", oldPerson, person);
		}
	}

	// The GUI view
	static class View {
		private ViewModel viewModel;
		private ListViewer jobsViewer;

		public View(ViewModel viewModel) {
			this.viewModel = viewModel;
		}

		public Shell createShell() {
			// Build a UI
			Display display = Display.getDefault();
			Shell shell = new Shell(display);
			shell.setLayout(new RowLayout(SWT.VERTICAL));
			Button replace = new Button(shell, SWT.BORDER);
			replace.setText("Reload Person From Dao");
			replace.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					viewModel.simulateLoadPersonFromDao();
				}
			});

			jobsViewer = new ListViewer(shell, SWT.BORDER | SWT.MULTI);
			jobsViewer.setContentProvider(new ArrayContentProvider());
			jobsViewer.setComparer(new JobComparer());
			jobsViewer.setInput(SELECTABLE_JOBS);

			createBindings();

			// Open and return the Shell
			shell.pack();
			shell.open();
			return shell;
		}

		private void createBindings() {
			// Bind it
			DataBindingContext bindingContext = new DataBindingContext();
			// We use master-detail to simulate the possibility of replacing
			// the entire Person model object (e.g. when refreshing data from a
			// database).
			IObservableValue master = BeansObservables.observeValue(viewModel,
					"person");

			IObservableValue jobsObservableListValue = BeansObservables
					.observeDetailValue(master, "jobs", null);
			IViewerObservableList observeMultiSelection = ViewersObservables
					.observeMultiSelection(jobsViewer);
			observeMultiSelection
					.addListChangeListener(new IListChangeListener() {
						@Override
						public void handleListChange(ListChangeEvent event) {
							System.out.println("Viewer selection changed to "
									+ jobsViewer.getSelection());
						}
					});
			IObservableValue observeMultiSelectionListValue = ObservableListValue
					.newInstance(observeMultiSelection);
			final Binding binding = bindingContext.bindValue(
					observeMultiSelectionListValue, jobsObservableListValue);

			jobsObservableListValue.addChangeListener(new IChangeListener() {
				@Override
				public void handleChange(ChangeEvent event) {
					checkBindingStatusForExceptions(binding);
				}
			});
		}
	}

	private static void checkBindingStatusForExceptions(Binding binding) {
		IStatus status = (IStatus) binding.getValidationStatus().getValue();
		if (status.getException() != null) {
			System.err.println("Exception in databinding:");
			status.getException().printStackTrace();
		}
	}

}
Previous Topic:FieldEditors
Next Topic:DelayedObservableValue with IObservableCollection?
Goto Forum:
  


Current Time: Fri Mar 29 13:04:15 GMT 2024

Powered by FUDForum. Page generated in 0.02872 seconds
.:: Contact :: Home ::.

Powered by: FUDforum 3.0.2.
Copyright ©2001-2010 FUDforum Bulletin Board Software

Back to the top