FIXED - bug 305367: [DataBinding] Detail value observables for observable (list|set|map)s
https://bugs.eclipse.org/bugs/show_bug.cgi?id=305367
diff --git a/bundles/org.eclipse.core.databinding.observable/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.databinding.observable/META-INF/MANIFEST.MF
index a99b010..1bdf4a1 100644
--- a/bundles/org.eclipse.core.databinding.observable/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.core.databinding.observable/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.core.databinding.observable
-Bundle-Version: 1.3.0.qualifier
+Bundle-Version: 1.4.0.qualifier
 Bundle-ClassPath: .
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
diff --git a/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/databinding/observable/masterdetail/MasterDetailObservables.java b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/databinding/observable/masterdetail/MasterDetailObservables.java
index e75dc53..203a1cf 100644
--- a/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/databinding/observable/masterdetail/MasterDetailObservables.java
+++ b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/databinding/observable/masterdetail/MasterDetailObservables.java
@@ -9,6 +9,7 @@
  *     IBM Corporation - initial API and implementation
  *     Brad Reynolds - bug 147515
  *     Matthew Hall - bug 221704, 226289
+ *     Ovidio Mallo - bugs 305367
  *******************************************************************************/
 
 package org.eclipse.core.databinding.observable.masterdetail;
@@ -21,6 +22,9 @@
 import org.eclipse.core.internal.databinding.observable.masterdetail.DetailObservableMap;
 import org.eclipse.core.internal.databinding.observable.masterdetail.DetailObservableSet;
 import org.eclipse.core.internal.databinding.observable.masterdetail.DetailObservableValue;
+import org.eclipse.core.internal.databinding.observable.masterdetail.ListDetailValueObservableList;
+import org.eclipse.core.internal.databinding.observable.masterdetail.MapDetailValueObservableMap;
+import org.eclipse.core.internal.databinding.observable.masterdetail.SetDetailValueObservableMap;
 
 /**
  * Allows for the observation of an attribute, the detail, of an observable
@@ -29,7 +33,7 @@
  * @since 1.0
  */
 public class MasterDetailObservables {
-	
+
 	/**
 	 * Creates a detail observable value from a master observable value and a
 	 * factory. This can be used to create observable values that represent a
@@ -145,4 +149,119 @@
 		return new DetailObservableMap(detailFactory, master, detailKeyType,
 				detailValueType);
 	}
+
+	/**
+	 * Returns a detail observable list where each element is the detail value
+	 * of the element in the master observable list. The provided factory is
+	 * used to create the detail observable values for every master element
+	 * which then define the elements of the detail list. The detail list
+	 * resides in the same realm as the given master list.
+	 * 
+	 * <p>
+	 * Note that since the values of the returned list are detail values of the
+	 * elements of the master list, the only modifications supported are through
+	 * the {@link IObservableList#set(int, Object)} method. Modifications made
+	 * through the returned list are made through the detail observables created
+	 * by the specified observable factory.
+	 * </p>
+	 * 
+	 * @param masterList
+	 *            The master observable list.
+	 * @param detailFactory
+	 *            The factory for creating {@link IObservableValue} instances
+	 *            for the elements of the master list which then define the
+	 *            elements of the new detail list.
+	 * @param detailType
+	 *            The value type of the detail values, typically of type
+	 *            <code>java.lang.Class</code>. May be <code>null</code>.
+	 * @return A detail observable list with elements which correspond to the
+	 *         detail values of the elements of the master list.
+	 * 
+	 * @since 1.4
+	 */
+	public static IObservableList detailValues(IObservableList masterList,
+			IObservableFactory detailFactory, Object detailType) {
+		return new ListDetailValueObservableList(masterList, detailFactory,
+				detailType);
+	}
+
+	/**
+	 * Returns a detail observable map where the map's key set is the same as
+	 * the given observable set, and where each value is the detail value of the
+	 * element in the master observable set. The provided factory is used to
+	 * create the detail observable values for every master key which then
+	 * define the values of the detail map. The detail map resides in the same
+	 * realm as the given master set.
+	 * 
+	 * <p>
+	 * Note that since the values of the returned map are detail values of the
+	 * elements of the master set, the only modifications supported are through
+	 * the {@link IObservableMap#put(Object, Object)} and
+	 * {@link IObservableMap#putAll(java.util.Map)} methods. Therefore, the
+	 * returned map does not add entries for elements not already contained in
+	 * the master set. Modifications made through the returned detail map are
+	 * made through the detail observables created by the specified observable
+	 * factory.
+	 * </p>
+	 * 
+	 * @param masterSet
+	 *            The master observable set.
+	 * @param detailFactory
+	 *            The factory for creating {@link IObservableValue} instances
+	 *            for the elements of the master set which then define the
+	 *            values of the new detail map.
+	 * @param detailType
+	 *            The value type of the detail values, typically of type
+	 *            <code>java.lang.Class</code>. May be <code>null</code>.
+	 * @return A detail observable map with the given master set as key set and
+	 *         with values which correspond to the detail values of the elements
+	 *         of the master set.
+	 * 
+	 * @since 1.4
+	 */
+	public static IObservableMap detailValues(IObservableSet masterSet,
+			IObservableFactory detailFactory, Object detailType) {
+		return new SetDetailValueObservableMap(masterSet, detailFactory,
+				detailType);
+	}
+
+	/**
+	 * Returns a detail observable map where the map's key set is the same as
+	 * the one of the given master observable map, and where each value is the
+	 * detail value of the corresponding value in the master observable map. The
+	 * provided factory is used to create the detail observable values for every
+	 * master value which then define the values of the detail map. The detail
+	 * map resides in the same realm as the given master map.
+	 * 
+	 * <p>
+	 * Note that since the values of the returned map are detail values of the
+	 * values of the master map, the only modifications supported are through
+	 * the {@link IObservableMap#put(Object, Object)} and
+	 * {@link IObservableMap#putAll(java.util.Map)} methods. Therefore, the
+	 * returned map does not add entries for keys not already contained in the
+	 * master map's key set. Modifications made through the returned detail map
+	 * are made through the detail observables created by the specified
+	 * observable factory.
+	 * </p>
+	 * 
+	 * @param masterMap
+	 *            The master observable map.
+	 * @param detailFactory
+	 *            The factory for creating {@link IObservableValue} instances
+	 *            for the values of the master map which then define the values
+	 *            of the new detail map.
+	 * @param detailType
+	 *            The value type of the detail values, typically of type
+	 *            <code>java.lang.Class</code>. May be <code>null</code>.
+	 * @return A detail observable map with the same key set as the given master
+	 *         observable map and with values which correspond to the detail
+	 *         values of the values of the master map.
+	 * 
+	 * @since 1.4
+	 */
+	public static IObservableMap detailValues(IObservableMap masterMap,
+			IObservableFactory detailFactory, Object detailType) {
+		return new MapDetailValueObservableMap(masterMap, detailFactory,
+				detailType);
+	}
 }
diff --git a/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/ListDetailValueObservableList.java b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/ListDetailValueObservableList.java
new file mode 100644
index 0000000..df985d8
--- /dev/null
+++ b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/ListDetailValueObservableList.java
@@ -0,0 +1,351 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.internal.databinding.observable.masterdetail;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.RandomAccess;
+
+import org.eclipse.core.databinding.observable.Diffs;
+import org.eclipse.core.databinding.observable.DisposeEvent;
+import org.eclipse.core.databinding.observable.IDisposeListener;
+import org.eclipse.core.databinding.observable.IObserving;
+import org.eclipse.core.databinding.observable.IStaleListener;
+import org.eclipse.core.databinding.observable.ObservableTracker;
+import org.eclipse.core.databinding.observable.StaleEvent;
+import org.eclipse.core.databinding.observable.list.AbstractObservableList;
+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.list.ListDiff;
+import org.eclipse.core.databinding.observable.list.ListDiffEntry;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.value.IObservableValue;
+import org.eclipse.core.databinding.observable.value.IValueChangeListener;
+import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
+import org.eclipse.core.internal.databinding.identity.IdentityMap;
+import org.eclipse.core.internal.databinding.identity.IdentitySet;
+
+/**
+ * @since 1.4
+ */
+public class ListDetailValueObservableList extends AbstractObservableList
+		implements IObserving, RandomAccess {
+
+	private IObservableList masterList;
+
+	private IObservableFactory detailFactory;
+
+	private Object detailType;
+
+	// The list of detail observables.
+	private ArrayList detailList;
+
+	// Maps every master to a DetailEntry containing the detail observable. This
+	// map is used to avoid that multiple detail observables are created for the
+	// same master.
+	private IdentityMap masterDetailMap = new IdentityMap();
+
+	private IdentitySet staleDetailObservables = new IdentitySet();
+
+	private IListChangeListener masterListListener = new IListChangeListener() {
+		public void handleListChange(ListChangeEvent event) {
+			handleMasterListChange(event.diff);
+		}
+	};
+
+	private IValueChangeListener detailValueListener = new IValueChangeListener() {
+		public void handleValueChange(ValueChangeEvent event) {
+			if (!event.getObservable().isStale()) {
+				staleDetailObservables.remove(event.getObservable());
+			}
+			handleDetailValueChange(event);
+		}
+	};
+
+	private IStaleListener masterStaleListener = new IStaleListener() {
+		public void handleStale(StaleEvent staleEvent) {
+			fireStale();
+		}
+	};
+
+	private IStaleListener detailStaleListener = new IStaleListener() {
+		public void handleStale(StaleEvent staleEvent) {
+			boolean wasStale = isStale();
+			staleDetailObservables.add((staleEvent.getObservable()));
+			if (!wasStale) {
+				fireStale();
+			}
+		}
+	};
+
+	/**
+	 * 
+	 * @param masterList
+	 * @param detailFactory
+	 * @param detailType
+	 */
+	public ListDetailValueObservableList(IObservableList masterList,
+			IObservableFactory detailFactory, Object detailType) {
+		super(masterList.getRealm());
+		this.masterList = masterList;
+		this.detailFactory = detailFactory;
+		this.detailType = detailType;
+		this.detailList = new ArrayList();
+
+		// Add change/stale/dispose listeners on the master list.
+		masterList.addListChangeListener(masterListListener);
+		masterList.addStaleListener(masterStaleListener);
+		masterList.addDisposeListener(new IDisposeListener() {
+			public void handleDispose(DisposeEvent event) {
+				ListDetailValueObservableList.this.dispose();
+			}
+		});
+
+		ListDiff initMasterDiff = Diffs.computeListDiff(Collections.EMPTY_LIST,
+				masterList);
+		handleMasterListChange(initMasterDiff);
+	}
+
+	protected synchronized void firstListenerAdded() {
+		for (int i = 0; i < detailList.size(); i++) {
+			IObservableValue detail = (IObservableValue) detailList.get(i);
+			detail.addValueChangeListener(detailValueListener);
+			detail.addStaleListener(detailStaleListener);
+			if (detail.isStale()) {
+				staleDetailObservables.add(detail);
+			}
+		}
+	}
+
+	protected synchronized void lastListenerRemoved() {
+		if (isDisposed()) {
+			return;
+		}
+
+		for (int i = 0; i < detailList.size(); i++) {
+			IObservableValue detail = (IObservableValue) detailList.get(i);
+			detail.removeValueChangeListener(detailValueListener);
+			detail.removeStaleListener(detailStaleListener);
+		}
+		staleDetailObservables.clear();
+	}
+
+	private void handleMasterListChange(ListDiff masterListDiff) {
+		boolean wasStale = isStale();
+
+		boolean hasListeners = hasListeners();
+		ListDiffEntry[] masterEntries = masterListDiff.getDifferences();
+		ListDiffEntry[] detailEntries = new ListDiffEntry[masterEntries.length];
+		for (int i = 0; i < masterEntries.length; i++) {
+			ListDiffEntry masterEntry = masterEntries[i];
+			int index = masterEntry.getPosition();
+
+			Object masterElement = masterEntry.getElement();
+			Object detailValue;
+			if (masterEntry.isAddition()) {
+				detailValue = addDetailObservable(masterElement, index);
+			} else {
+				detailValue = removeDetailObservable(masterElement, index);
+			}
+
+			if (hasListeners) {
+				// Create the corresponding diff for the detail list.
+				detailEntries[i] = Diffs.createListDiffEntry(index,
+						masterEntry.isAddition(), detailValue);
+			}
+		}
+
+		if (hasListeners) {
+			if (!wasStale && isStale()) {
+				fireStale();
+			}
+
+			// Fire a list change event with the adapted diff.
+			fireListChange(Diffs.createListDiff(detailEntries));
+		}
+	}
+
+	private Object addDetailObservable(Object masterElement, int index) {
+		DetailEntry detailEntry = (DetailEntry) masterDetailMap
+				.get(masterElement);
+		if (detailEntry != null) {
+			// If we already have a detail observable for the given
+			// masterElement, we increment the reference count.
+			detailEntry.masterReferenceCount++;
+			detailList.add(index, detailEntry.detailObservable);
+			return detailEntry.detailObservable.getValue();
+		}
+
+		IObservableValue detail = createDetailObservable(masterElement);
+		masterDetailMap.put(masterElement, new DetailEntry(detail));
+
+		detailList.add(index, detail);
+
+		if (hasListeners()) {
+			detail.addValueChangeListener(detailValueListener);
+			detail.addStaleListener(detailStaleListener);
+			if (detail.isStale()) {
+				staleDetailObservables.add(detail);
+			}
+		}
+
+		return detail.getValue();
+	}
+
+	private Object removeDetailObservable(Object masterElement, int index) {
+		IObservableValue detail = (IObservableValue) detailList.remove(index);
+		Object detailValue = detail.getValue();
+
+		DetailEntry detailEntry = (DetailEntry) masterDetailMap
+				.get(masterElement);
+
+		// We may only dispose the detail observable ASA there are no more
+		// masters referencing it.
+		detailEntry.masterReferenceCount--;
+		if (detailEntry.masterReferenceCount == 0) {
+			masterDetailMap.remove(masterElement);
+			staleDetailObservables.remove(detail);
+			detail.dispose();
+		}
+
+		return detailValue;
+	}
+
+	private void handleDetailValueChange(ValueChangeEvent event) {
+		IObservableValue detail = event.getObservableValue();
+
+		// When we get a change event on a detail observable, we must find its
+		// position while there may also be duplicate entries.
+		BitSet detailIndexes = new BitSet();
+		for (int i = 0; i < detailList.size(); i++) {
+			if (detailList.get(i) == detail) {
+				detailIndexes.set(i);
+			}
+		}
+
+		// Create the diff for every found position.
+		Object oldValue = event.diff.getOldValue();
+		Object newValue = event.diff.getNewValue();
+		ListDiffEntry[] diffEntries = new ListDiffEntry[2 * detailIndexes
+				.cardinality()];
+		int diffIndex = 0;
+		for (int b = detailIndexes.nextSetBit(0); b != -1; b = detailIndexes
+				.nextSetBit(b + 1)) {
+			diffEntries[diffIndex++] = Diffs.createListDiffEntry(b, false,
+					oldValue);
+			diffEntries[diffIndex++] = Diffs.createListDiffEntry(b, true,
+					newValue);
+		}
+		fireListChange(Diffs.createListDiff(diffEntries));
+	}
+
+	private IObservableValue createDetailObservable(Object masterElement) {
+		ObservableTracker.setIgnore(true);
+		try {
+			return (IObservableValue) detailFactory
+					.createObservable(masterElement);
+		} finally {
+			ObservableTracker.setIgnore(false);
+		}
+	}
+
+	protected int doGetSize() {
+		return detailList.size();
+	}
+
+	public Object get(int index) {
+		ObservableTracker.getterCalled(this);
+		return ((IObservableValue) detailList.get(index)).getValue();
+	}
+
+	public Object set(int index, Object element) {
+		IObservableValue detail = (IObservableValue) detailList.get(index);
+		Object oldElement = detail.getValue();
+		detail.setValue(element);
+		return oldElement;
+	}
+
+	public Object move(int oldIndex, int newIndex) {
+		throw new UnsupportedOperationException();
+	}
+
+	public boolean remove(Object o) {
+		throw new UnsupportedOperationException();
+	}
+
+	public boolean removeAll(Collection c) {
+		throw new UnsupportedOperationException();
+	}
+
+	public boolean retainAll(Collection c) {
+		throw new UnsupportedOperationException();
+	}
+
+	public void clear() {
+		throw new UnsupportedOperationException();
+	}
+
+	public Object getElementType() {
+		return detailType;
+	}
+
+	public boolean isStale() {
+		return super.isStale()
+				|| (masterList != null && masterList.isStale())
+				|| (staleDetailObservables != null && !staleDetailObservables
+						.isEmpty());
+	}
+
+	public Object getObserved() {
+		return masterList;
+	}
+
+	public synchronized void dispose() {
+		if (masterList != null) {
+			masterList.removeListChangeListener(masterListListener);
+			masterList.removeStaleListener(masterStaleListener);
+		}
+
+		if (detailList != null) {
+			for (Iterator iter = detailList.iterator(); iter.hasNext();) {
+				IObservableValue detailValue = (IObservableValue) iter.next();
+				detailValue.dispose();
+			}
+			detailList.clear();
+		}
+
+		masterList = null;
+		detailFactory = null;
+		detailType = null;
+		masterListListener = null;
+		detailValueListener = null;
+		masterDetailMap = null;
+		staleDetailObservables = null;
+
+		super.dispose();
+	}
+
+	private static final class DetailEntry {
+
+		private final IObservableValue detailObservable;
+
+		private int masterReferenceCount = 1;
+
+		public DetailEntry(IObservableValue detailObservable) {
+			this.detailObservable = detailObservable;
+		}
+	}
+}
diff --git a/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/MapDetailValueObservableMap.java b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/MapDetailValueObservableMap.java
new file mode 100644
index 0000000..fc3cec1
--- /dev/null
+++ b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/MapDetailValueObservableMap.java
@@ -0,0 +1,405 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.internal.databinding.observable.masterdetail;
+
+import java.util.AbstractSet;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.core.databinding.observable.Diffs;
+import org.eclipse.core.databinding.observable.DisposeEvent;
+import org.eclipse.core.databinding.observable.IDisposeListener;
+import org.eclipse.core.databinding.observable.IObserving;
+import org.eclipse.core.databinding.observable.IStaleListener;
+import org.eclipse.core.databinding.observable.ObservableTracker;
+import org.eclipse.core.databinding.observable.StaleEvent;
+import org.eclipse.core.databinding.observable.map.AbstractObservableMap;
+import org.eclipse.core.databinding.observable.map.IMapChangeListener;
+import org.eclipse.core.databinding.observable.map.IObservableMap;
+import org.eclipse.core.databinding.observable.map.MapChangeEvent;
+import org.eclipse.core.databinding.observable.map.MapDiff;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.value.IObservableValue;
+import org.eclipse.core.databinding.observable.value.IValueChangeListener;
+import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
+import org.eclipse.core.internal.databinding.identity.IdentityMap;
+import org.eclipse.core.internal.databinding.identity.IdentitySet;
+import org.eclipse.core.internal.databinding.observable.Util;
+
+/**
+ * @since 1.4
+ */
+public class MapDetailValueObservableMap extends AbstractObservableMap
+		implements IObserving {
+
+	private IObservableMap masterMap;
+
+	private IObservableFactory observableValueFactory;
+
+	private Object detailValueType;
+
+	private Set entrySet;
+
+	private IdentityHashMap keyDetailMap = new IdentityHashMap();
+
+	private IdentitySet staleDetailObservables = new IdentitySet();
+
+	private IMapChangeListener masterMapListener = new IMapChangeListener() {
+		public void handleMapChange(MapChangeEvent event) {
+			handleMasterMapChange(event.diff);
+		}
+	};
+
+	private IStaleListener masterStaleListener = new IStaleListener() {
+		public void handleStale(StaleEvent staleEvent) {
+			fireStale();
+		}
+	};
+
+	private IStaleListener detailStaleListener = new IStaleListener() {
+		public void handleStale(StaleEvent staleEvent) {
+			addStaleDetailObservable((IObservableValue) staleEvent
+					.getObservable());
+		}
+	};
+
+	/**
+	 * @param masterMap
+	 * @param observableValueFactory
+	 * @param detailValueType
+	 */
+	public MapDetailValueObservableMap(IObservableMap masterMap,
+			IObservableFactory observableValueFactory, Object detailValueType) {
+		super(masterMap.getRealm());
+		this.masterMap = masterMap;
+		this.observableValueFactory = observableValueFactory;
+		this.detailValueType = detailValueType;
+
+		// Add change/stale/dispose listeners on the master map.
+		masterMap.addMapChangeListener(masterMapListener);
+		masterMap.addStaleListener(masterStaleListener);
+		masterMap.addDisposeListener(new IDisposeListener() {
+			public void handleDispose(DisposeEvent event) {
+				MapDetailValueObservableMap.this.dispose();
+			}
+		});
+
+		// Initialize the map with the current state of the master map.
+		MapDiff initMasterDiff = Diffs.computeMapDiff(Collections.EMPTY_MAP,
+				masterMap);
+		handleMasterMapChange(initMasterDiff);
+	}
+
+	private void handleMasterMapChange(MapDiff diff) {
+		// Collect the detail values for the master values in the input diff.
+		IdentityMap oldValues = new IdentityMap();
+		IdentityMap newValues = new IdentityMap();
+
+		// Handle added master values.
+		Set addedKeys = diff.getAddedKeys();
+		for (Iterator iter = addedKeys.iterator(); iter.hasNext();) {
+			Object addedKey = iter.next();
+
+			// For added master values, we set up a new detail observable.
+			addDetailObservable(addedKey);
+
+			// Get the value of the created detail observable for the new diff.
+			IObservableValue detailValue = getDetailObservableValue(addedKey);
+			newValues.put(addedKey, detailValue.getValue());
+		}
+
+		// Handle removed master values.
+		Set removedKeys = diff.getRemovedKeys();
+		for (Iterator iter = removedKeys.iterator(); iter.hasNext();) {
+			Object removedKey = iter.next();
+
+			// First of all, get the current detail value and add it to the set
+			// of old values of the new diff.
+			IObservableValue detailValue = getDetailObservableValue(removedKey);
+			oldValues.put(removedKey, detailValue.getValue());
+
+			// For removed master values, we dispose the detail observable.
+			removeDetailObservable(removedKey);
+		}
+
+		// Handle changed master values.
+		Set changedKeys = diff.getChangedKeys();
+		for (Iterator iter = changedKeys.iterator(); iter.hasNext();) {
+			Object changedKey = iter.next();
+
+			// Get the detail value prior to the change and add it to the set of
+			// old values of the new diff.
+			IObservableValue oldDetailValue = getDetailObservableValue(changedKey);
+			oldValues.put(changedKey, oldDetailValue.getValue());
+
+			// Remove the old detail value for the old master value and add it
+			// again for the new master value.
+			removeDetailObservable(changedKey);
+			addDetailObservable(changedKey);
+
+			// Get the new detail value and add it to the set of new values.
+			IObservableValue newDetailValue = getDetailObservableValue(changedKey);
+			newValues.put(changedKey, newDetailValue.getValue());
+		}
+
+		// The different key sets are the same, only the values change.
+		fireMapChange(Diffs.createMapDiff(addedKeys, removedKeys, changedKeys,
+				oldValues, newValues));
+	}
+
+	private void addDetailObservable(final Object addedKey) {
+		Object masterElement = masterMap.get(addedKey);
+
+		IObservableValue detailValue = (IObservableValue) keyDetailMap
+				.get(addedKey);
+
+		if (detailValue == null) {
+			detailValue = createDetailObservable(masterElement);
+
+			keyDetailMap.put(addedKey, detailValue);
+
+			detailValue.addValueChangeListener(new IValueChangeListener() {
+				public void handleValueChange(ValueChangeEvent event) {
+					if (!event.getObservableValue().isStale()) {
+						staleDetailObservables.remove(event.getSource());
+					}
+
+					fireMapChange(Diffs.createMapDiffSingleChange(addedKey,
+							event.diff.getOldValue(), event.diff.getNewValue()));
+				}
+			});
+
+			if (detailValue.isStale()) {
+				addStaleDetailObservable(detailValue);
+			}
+		}
+
+		detailValue.addStaleListener(detailStaleListener);
+	}
+
+	private IObservableValue createDetailObservable(Object masterElement) {
+		ObservableTracker.setIgnore(true);
+		try {
+			return (IObservableValue) observableValueFactory
+					.createObservable(masterElement);
+		} finally {
+			ObservableTracker.setIgnore(false);
+		}
+	}
+
+	private void removeDetailObservable(Object removedKey) {
+		if (isDisposed()) {
+			return;
+		}
+
+		IObservableValue detailValue = (IObservableValue) keyDetailMap
+				.remove(removedKey);
+		staleDetailObservables.remove(detailValue);
+		detailValue.dispose();
+	}
+
+	private IObservableValue getDetailObservableValue(Object masterKey) {
+		return (IObservableValue) keyDetailMap.get(masterKey);
+	}
+
+	private void addStaleDetailObservable(IObservableValue detailObservable) {
+		boolean wasStale = isStale();
+		staleDetailObservables.add(detailObservable);
+		if (!wasStale) {
+			fireStale();
+		}
+	}
+
+	public Set keySet() {
+		getterCalled();
+
+		return masterMap.keySet();
+	}
+
+	public Object get(Object key) {
+		getterCalled();
+
+		if (!containsKey(key)) {
+			return null;
+		}
+
+		IObservableValue detailValue = getDetailObservableValue(key);
+		return detailValue.getValue();
+	}
+
+	public Object put(Object key, Object value) {
+		if (!containsKey(key)) {
+			return null;
+		}
+
+		IObservableValue detailValue = getDetailObservableValue(key);
+		Object oldValue = detailValue.getValue();
+		detailValue.setValue(value);
+		return oldValue;
+	}
+
+	public boolean containsKey(Object key) {
+		getterCalled();
+
+		return masterMap.containsKey(key);
+	}
+
+	public Object remove(Object key) {
+		checkRealm();
+
+		if (!containsKey(key)) {
+			return null;
+		}
+
+		IObservableValue detailValue = getDetailObservableValue(key);
+		Object oldValue = detailValue.getValue();
+
+		masterMap.remove(key);
+
+		return oldValue;
+	}
+
+	public int size() {
+		getterCalled();
+
+		return masterMap.size();
+	}
+
+	public boolean isStale() {
+		return super.isStale()
+				|| (masterMap != null && masterMap.isStale())
+				|| (staleDetailObservables != null && !staleDetailObservables
+						.isEmpty());
+	}
+
+	public Object getKeyType() {
+		return masterMap.getKeyType();
+	}
+
+	public Object getValueType() {
+		return detailValueType;
+	}
+
+	public Object getObserved() {
+		return masterMap;
+	}
+
+	public synchronized void dispose() {
+		if (masterMap != null) {
+			masterMap.removeMapChangeListener(masterMapListener);
+			masterMap.removeStaleListener(masterStaleListener);
+		}
+
+		if (keyDetailMap != null) {
+			for (Iterator iter = keyDetailMap.values().iterator(); iter
+					.hasNext();) {
+				IObservableValue detailValue = (IObservableValue) iter.next();
+				detailValue.dispose();
+			}
+			keyDetailMap.clear();
+		}
+
+		masterMap = null;
+		observableValueFactory = null;
+		detailValueType = null;
+		keyDetailMap = null;
+		masterStaleListener = null;
+		detailStaleListener = null;
+		staleDetailObservables = null;
+
+		super.dispose();
+	}
+
+	public Set entrySet() {
+		getterCalled();
+
+		if (entrySet == null) {
+			entrySet = new EntrySet();
+		}
+		return entrySet;
+	}
+
+	private void getterCalled() {
+		ObservableTracker.getterCalled(this);
+	}
+
+	private class EntrySet extends AbstractSet {
+
+		public Iterator iterator() {
+			final Iterator keyIterator = keySet().iterator();
+			return new Iterator() {
+
+				public boolean hasNext() {
+					return keyIterator.hasNext();
+				}
+
+				public Object next() {
+					Object key = keyIterator.next();
+					return new MapEntry(key);
+				}
+
+				public void remove() {
+					keyIterator.remove();
+				}
+			};
+		}
+
+		public int size() {
+			return MapDetailValueObservableMap.this.size();
+		}
+	}
+
+	private final class MapEntry implements Map.Entry {
+
+		private final Object key;
+
+		private MapEntry(Object key) {
+			this.key = key;
+		}
+
+		public Object getKey() {
+			MapDetailValueObservableMap.this.getterCalled();
+			return key;
+		}
+
+		public Object getValue() {
+			return MapDetailValueObservableMap.this.get(getKey());
+		}
+
+		public Object setValue(Object value) {
+			return MapDetailValueObservableMap.this.put(getKey(), value);
+		}
+
+		public boolean equals(Object o) {
+			MapDetailValueObservableMap.this.getterCalled();
+			if (o == this)
+				return true;
+			if (o == null)
+				return false;
+			if (!(o instanceof Map.Entry))
+				return false;
+			Map.Entry that = (Map.Entry) o;
+			return Util.equals(this.getKey(), that.getKey())
+					&& Util.equals(this.getValue(), that.getValue());
+		}
+
+		public int hashCode() {
+			MapDetailValueObservableMap.this.getterCalled();
+			Object value = getValue();
+			return (getKey() == null ? 0 : getKey().hashCode())
+					^ (value == null ? 0 : value.hashCode());
+		}
+	}
+}
diff --git a/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/SetDetailValueObservableMap.java b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/SetDetailValueObservableMap.java
new file mode 100644
index 0000000..f66a1c9
--- /dev/null
+++ b/bundles/org.eclipse.core.databinding.observable/src/org/eclipse/core/internal/databinding/observable/masterdetail/SetDetailValueObservableMap.java
@@ -0,0 +1,178 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.internal.databinding.observable.masterdetail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.core.databinding.observable.IObserving;
+import org.eclipse.core.databinding.observable.IStaleListener;
+import org.eclipse.core.databinding.observable.ObservableTracker;
+import org.eclipse.core.databinding.observable.StaleEvent;
+import org.eclipse.core.databinding.observable.map.ComputedObservableMap;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.set.IObservableSet;
+import org.eclipse.core.databinding.observable.value.IObservableValue;
+import org.eclipse.core.databinding.observable.value.IValueChangeListener;
+import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
+import org.eclipse.core.internal.databinding.identity.IdentitySet;
+
+/**
+ * @since 1.4
+ */
+public class SetDetailValueObservableMap extends ComputedObservableMap
+		implements IObserving {
+
+	private IObservableFactory observableValueFactory;
+
+	private Map detailObservableValueMap = new HashMap();
+
+	private IdentitySet staleDetailObservables = new IdentitySet();
+
+	private IStaleListener detailStaleListener = new IStaleListener() {
+		public void handleStale(StaleEvent staleEvent) {
+			addStaleDetailObservable((IObservableValue) staleEvent
+					.getObservable());
+		}
+	};
+
+	/**
+	 * @param masterKeySet
+	 * @param observableValueFactory
+	 * @param detailValueType
+	 */
+	public SetDetailValueObservableMap(IObservableSet masterKeySet,
+			IObservableFactory observableValueFactory, Object detailValueType) {
+		super(masterKeySet, detailValueType);
+		this.observableValueFactory = observableValueFactory;
+	}
+
+	protected void hookListener(final Object addedKey) {
+		final IObservableValue detailValue = getDetailObservableValue(addedKey);
+
+		detailValue.addValueChangeListener(new IValueChangeListener() {
+			public void handleValueChange(ValueChangeEvent event) {
+				if (!event.getObservableValue().isStale()) {
+					staleDetailObservables.remove(detailValue);
+				}
+
+				fireSingleChange(addedKey, event.diff.getOldValue(),
+						event.diff.getNewValue());
+			}
+		});
+
+		detailValue.addStaleListener(detailStaleListener);
+	}
+
+	protected void unhookListener(Object removedKey) {
+		if (isDisposed()) {
+			return;
+		}
+
+		IObservableValue detailValue = (IObservableValue) detailObservableValueMap
+				.remove(removedKey);
+		staleDetailObservables.remove(detailValue);
+		detailValue.dispose();
+	}
+
+	private IObservableValue getDetailObservableValue(Object masterKey) {
+		IObservableValue detailValue = (IObservableValue) detailObservableValueMap
+				.get(masterKey);
+
+		if (detailValue == null) {
+			ObservableTracker.setIgnore(true);
+			try {
+				detailValue = (IObservableValue) observableValueFactory
+						.createObservable(masterKey);
+			} finally {
+				ObservableTracker.setIgnore(false);
+			}
+
+			detailObservableValueMap.put(masterKey, detailValue);
+
+			if (detailValue.isStale()) {
+				addStaleDetailObservable(detailValue);
+			}
+		}
+
+		return detailValue;
+	}
+
+	private void addStaleDetailObservable(IObservableValue detailObservable) {
+		boolean wasStale = isStale();
+		staleDetailObservables.add(detailObservable);
+		if (!wasStale) {
+			fireStale();
+		}
+	}
+
+	protected Object doGet(Object key) {
+		IObservableValue detailValue = getDetailObservableValue(key);
+		return detailValue.getValue();
+	}
+
+	protected Object doPut(Object key, Object value) {
+		IObservableValue detailValue = getDetailObservableValue(key);
+		Object oldValue = detailValue.getValue();
+		detailValue.setValue(value);
+		return oldValue;
+	}
+
+	public boolean containsKey(Object key) {
+		getterCalled();
+
+		return keySet().contains(key);
+	}
+
+	public Object remove(Object key) {
+		checkRealm();
+
+		if (!containsKey(key)) {
+			return null;
+		}
+
+		IObservableValue detailValue = getDetailObservableValue(key);
+		Object oldValue = detailValue.getValue();
+
+		keySet().remove(key);
+
+		return oldValue;
+	}
+
+	public int size() {
+		getterCalled();
+
+		return keySet().size();
+	}
+
+	public boolean isStale() {
+		return super.isStale() || staleDetailObservables != null
+				&& !staleDetailObservables.isEmpty();
+	}
+
+	public Object getObserved() {
+		return keySet();
+	}
+
+	public synchronized void dispose() {
+		super.dispose();
+
+		observableValueFactory = null;
+		detailObservableValueMap = null;
+		detailStaleListener = null;
+		staleDetailObservables = null;
+	}
+
+	private void getterCalled() {
+		ObservableTracker.getterCalled(this);
+	}
+}
diff --git a/bundles/org.eclipse.core.databinding.property/META-INF/MANIFEST.MF b/bundles/org.eclipse.core.databinding.property/META-INF/MANIFEST.MF
index c2b26cc..11b5608 100644
--- a/bundles/org.eclipse.core.databinding.property/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.core.databinding.property/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.core.databinding.property
-Bundle-Version: 1.3.0.qualifier
+Bundle-Version: 1.4.0.qualifier
 Bundle-ClassPath: .
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
diff --git a/bundles/org.eclipse.core.databinding.property/src/org/eclipse/core/databinding/property/value/ValueProperty.java b/bundles/org.eclipse.core.databinding.property/src/org/eclipse/core/databinding/property/value/ValueProperty.java
index 8422e8e..5024b3b 100644
--- a/bundles/org.eclipse.core.databinding.property/src/org/eclipse/core/databinding/property/value/ValueProperty.java
+++ b/bundles/org.eclipse.core.databinding.property/src/org/eclipse/core/databinding/property/value/ValueProperty.java
@@ -8,15 +8,18 @@
  * Contributors:
  *     Matthew Hall - initial API and implementation (bug 194734)
  *     Matthew Hall - bug 195222
- *     Ovidio Mallo - bug 331348
+ *     Ovidio Mallo - bugs 331348, 305367
  ******************************************************************************/
 
 package org.eclipse.core.databinding.property.value;
 
 import org.eclipse.core.databinding.observable.IObservable;
 import org.eclipse.core.databinding.observable.Realm;
+import org.eclipse.core.databinding.observable.list.IObservableList;
+import org.eclipse.core.databinding.observable.map.IObservableMap;
 import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
 import org.eclipse.core.databinding.observable.masterdetail.MasterDetailObservables;
+import org.eclipse.core.databinding.observable.set.IObservableSet;
 import org.eclipse.core.databinding.observable.value.IObservableValue;
 import org.eclipse.core.databinding.property.list.IListProperty;
 import org.eclipse.core.databinding.property.map.IMapProperty;
@@ -125,6 +128,30 @@
 				valueFactory(master.getRealm()), getValueType());
 	}
 
+	/**
+	 * @since 1.4
+	 */
+	public IObservableList observeDetail(IObservableList master) {
+		return MasterDetailObservables.detailValues(master,
+				valueFactory(master.getRealm()), getValueType());
+	}
+
+	/**
+	 * @since 1.4
+	 */
+	public IObservableMap observeDetail(IObservableSet master) {
+		return MasterDetailObservables.detailValues(master,
+				valueFactory(master.getRealm()), getValueType());
+	}
+
+	/**
+	 * @since 1.4
+	 */
+	public IObservableMap observeDetail(IObservableMap master) {
+		return MasterDetailObservables.detailValues(master,
+				valueFactory(master.getRealm()), getValueType());
+	}
+
 	public final IValueProperty value(IValueProperty detailValue) {
 		return new ValuePropertyDetailValue(this, detailValue);
 	}
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/ListDetailValueObservableListTest.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/ListDetailValueObservableListTest.java
new file mode 100644
index 0000000..eefa802
--- /dev/null
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/ListDetailValueObservableListTest.java
@@ -0,0 +1,391 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.tests.internal.databinding.observable.masterdetail;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import org.eclipse.core.databinding.beans.BeansObservables;
+import org.eclipse.core.databinding.observable.IObservable;
+import org.eclipse.core.databinding.observable.IObservableCollection;
+import org.eclipse.core.databinding.observable.Realm;
+import org.eclipse.core.databinding.observable.list.IObservableList;
+import org.eclipse.core.databinding.observable.list.ListDiff;
+import org.eclipse.core.databinding.observable.list.ListDiffEntry;
+import org.eclipse.core.databinding.observable.list.WritableList;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.core.internal.databinding.observable.masterdetail.ListDetailValueObservableList;
+import org.eclipse.jface.databinding.conformance.ObservableListContractTest;
+import org.eclipse.jface.databinding.conformance.delegate.AbstractObservableCollectionContractDelegate;
+import org.eclipse.jface.databinding.conformance.util.ListChangeEventTracker;
+import org.eclipse.jface.examples.databinding.model.SimplePerson;
+import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase;
+
+/**
+ * @since 1.3
+ */
+public class ListDetailValueObservableListTest extends
+		AbstractDefaultRealmTestCase {
+
+	public static Test suite() {
+		TestSuite suite = new TestSuite(
+				ListDetailValueObservableListTest.class.getName());
+		suite.addTestSuite(ListDetailValueObservableListTest.class);
+		suite.addTest(ObservableListContractTest.suite(new Delegate()));
+		return suite;
+	}
+
+	public void testUnmodifiability() {
+		WritableList masterObservableList = new WritableList();
+		masterObservableList.add(new SimplePerson());
+		masterObservableList.add(new SimplePerson());
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterObservableList, BeansObservables.valueFactory("name"),
+				null);
+
+		try {
+			ldol.add("name");
+			fail("ListDetailValueObservableList must not be modifiable.");
+		} catch (UnsupportedOperationException e) {
+			// expected exception
+		}
+
+		try {
+			ldol.remove(masterObservableList.get(0));
+			fail("ListDetailValueObservableList must not be modifiable.");
+		} catch (UnsupportedOperationException e) {
+			// expected exception
+		}
+
+		try {
+			ldol.removeAll(Collections.singleton(masterObservableList.get(0)));
+			fail("ListDetailValueObservableList must not be modifiable.");
+		} catch (UnsupportedOperationException e) {
+			// expected exception
+		}
+
+		try {
+			ldol.retainAll(Collections.EMPTY_LIST);
+			fail("ListDetailValueObservableList must not be modifiable.");
+		} catch (UnsupportedOperationException e) {
+			// expected exception
+		}
+
+		try {
+			ldol.move(0, 1);
+			fail("ListDetailValueObservableList must not be modifiable.");
+		} catch (UnsupportedOperationException e) {
+			// expected exception
+		}
+	}
+
+	public void testGetElementType() {
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				new WritableList(), BeansObservables.valueFactory("name"),
+				String.class);
+
+		assertSame(String.class, ldol.getElementType());
+	}
+
+	public void testGetObserved() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// The observed object is the master list.
+		assertSame(masterList, ldol.getObserved());
+	}
+
+	public void testMasterListInitiallyNotEmpty() {
+		WritableList masterList = new WritableList();
+		SimplePerson person = new SimplePerson();
+		person.setName("name");
+		masterList.add(person);
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// Make sure that a non-empty master list is initialized correctly.
+		assertEquals(masterList.size(), ldol.size());
+		assertEquals(person.getName(), ldol.get(0));
+	}
+
+	public void testAddRemove() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// Initially, the detail list is empty.
+		assertTrue(ldol.isEmpty());
+
+		// Add a first person and check that its name is in the detail list.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterList.add(p1);
+		assertEquals(masterList.size(), ldol.size());
+		assertEquals(p1.getName(), ldol.get(0));
+
+		// Add a second person and check that it's name is in the detail list.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name2");
+		masterList.add(p2);
+		assertEquals(masterList.size(), ldol.size());
+		assertEquals(p2.getName(), ldol.get(1));
+
+		// Remove the first person from the master list and check that we still
+		// have the name of the second person in the detail list.
+		masterList.remove(0);
+		assertEquals(masterList.size(), ldol.size());
+		assertEquals(p2.getName(), ldol.get(0));
+
+		// Remove the second person as well.
+		masterList.remove(0);
+		assertTrue(ldol.isEmpty());
+	}
+
+	public void testChangeDetail() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterList.add(p1);
+		assertEquals(p1.getName(), ldol.get(0));
+		p1.setName("name2");
+		assertEquals(p1.getName(), ldol.get(0));
+
+		// Change the detail attribute by changing the master.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name3");
+		masterList.set(0, p2);
+		assertEquals(p2.getName(), ldol.get(0));
+	}
+
+	public void testSet() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson person = new SimplePerson();
+		person.setName("name1");
+		masterList.add(person);
+		assertEquals(person.getName(), ldol.get(0));
+
+		// Set a new name on the detail list.
+		ldol.set(0, "name2");
+		// Check that the name has been propagated to the master.
+		assertEquals("name2", person.getName());
+		assertEquals(person.getName(), ldol.get(0));
+	}
+
+	public void testDuplicateMasterElements() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		SimplePerson master = new SimplePerson();
+		master.setName("name1");
+
+		// Add the same master twice.
+		masterList.add(master);
+		masterList.add(master);
+
+		// Attach the change listener to the detail list.
+		ListChangeEventTracker changeTracker = ListChangeEventTracker
+				.observe(ldol);
+
+		// Setting the name on master should trigger an event on both
+		// occurrences of in the master list.
+		master.setName("name2");
+
+		// We should have 2 replace diffs, i.e. 4 diff entries.
+		assertEquals(1, changeTracker.count);
+		assertEquals(4, changeTracker.event.diff.getDifferences().length);
+		assertReplaceDiffAt(changeTracker.event.diff, 0, 0, "name1", "name2");
+		assertReplaceDiffAt(changeTracker.event.diff, 2, 0, "name1", "name2");
+
+		// Remove one instance of the master (one will remain).
+		masterList.remove(master);
+
+		// It should still be possible to work on the remaining master instance.
+		ldol.set(0, "name3");
+		assertEquals("name3", master.getName());
+	}
+
+	public void testDetailObservableChangeEvent() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		ListChangeEventTracker changeTracker = ListChangeEventTracker
+				.observe(ldol);
+
+		SimplePerson person = new SimplePerson();
+		person.setName("old name");
+
+		// Initially, we should not have received any event.
+		assertEquals(0, changeTracker.count);
+
+		// Add the person and check that we receive an addition event on the
+		// correct index and with the correct value.
+		masterList.add(person);
+		assertEquals(1, changeTracker.count);
+		assertEquals(1, changeTracker.event.diff.getDifferences().length);
+		assertTrue(changeTracker.event.diff.getDifferences()[0].isAddition());
+		assertEquals(0,
+				changeTracker.event.diff.getDifferences()[0].getPosition());
+		assertEquals(person.getName(),
+				changeTracker.event.diff.getDifferences()[0].getElement());
+
+		// Change the detail property and check that we receive a replace event.
+		person.setName("new name");
+		assertEquals(2, changeTracker.count);
+		assertIsSingleReplaceDiff(changeTracker.event.diff, 0, "old name",
+				"new name");
+	}
+
+	private void assertIsSingleReplaceDiff(ListDiff diff, int index,
+			Object oldElement, Object newElement) {
+		// We should have 2 diff entries.
+		assertEquals(2, diff.getDifferences().length);
+
+		// Check that it indeed is a replace diff.
+		assertReplaceDiffAt(diff, 0, index, oldElement, newElement);
+	}
+
+	private void assertReplaceDiffAt(ListDiff diff, int diffOffset, int index,
+			Object oldElement, Object newElement) {
+		ListDiffEntry entry1 = diff.getDifferences()[0];
+		ListDiffEntry entry2 = diff.getDifferences()[1];
+
+		// One diff entry must be an addition, the other a removal.
+		assertTrue(entry1.isAddition() != entry2.isAddition());
+
+		// Check for the index on the diff entries.
+		assertEquals(index, entry1.getPosition());
+		assertEquals(index, entry2.getPosition());
+
+		// Check for the old/new element values on both diff entries.
+		if (entry1.isAddition()) {
+			assertEquals(oldElement, entry2.getElement());
+			assertEquals(newElement, entry1.getElement());
+		} else {
+			assertEquals(oldElement, entry1.getElement());
+			assertEquals(newElement, entry2.getElement());
+		}
+	}
+
+	public void testMasterNull() {
+		WritableList masterObservableList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterObservableList, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Make sure null values are handled gracefully.
+		masterObservableList.add(null);
+		assertEquals(1, ldol.size());
+		assertNull(ldol.get(0));
+	}
+
+	public void testDetailObservableValuesAreDisposed() {
+		final List detailObservables = new ArrayList();
+		IObservableFactory detailValueFactory = new IObservableFactory() {
+			public IObservable createObservable(Object target) {
+				WritableValue detailObservable = new WritableValue();
+				// Remember the created observables.
+				detailObservables.add(detailObservable);
+				return detailObservable;
+			}
+		};
+
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, detailValueFactory, null);
+
+		masterList.add(new Object());
+		masterList.add(new Object());
+
+		assertEquals(ldol.size(), detailObservables.size());
+
+		// No detail observables should be disposed yet.
+		assertFalse(((WritableValue) detailObservables.get(0)).isDisposed());
+		assertFalse(((WritableValue) detailObservables.get(1)).isDisposed());
+
+		// Only the detail observable for the removed master should be disposed.
+		masterList.remove(1);
+		assertFalse(((WritableValue) detailObservables.get(0)).isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(1)).isDisposed());
+
+		// After disposing the detail list, all detail observables should be
+		// disposed.
+		ldol.dispose();
+		assertTrue(((WritableValue) detailObservables.get(0)).isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(1)).isDisposed());
+	}
+
+	public void testDisposeOnMasterDisposed() {
+		WritableList masterList = new WritableList();
+		ListDetailValueObservableList ldol = new ListDetailValueObservableList(
+				masterList, BeansObservables.valueFactory("name"), String.class);
+
+		// Initially, nothing should be disposed.
+		assertFalse(masterList.isDisposed());
+		assertFalse(ldol.isDisposed());
+
+		// Upon disposing the master list, the detail list should be disposed as
+		// well.
+		masterList.dispose();
+		assertTrue(masterList.isDisposed());
+		assertTrue(ldol.isDisposed());
+	}
+
+	private static class Delegate extends
+			AbstractObservableCollectionContractDelegate {
+		public IObservableCollection createObservableCollection(Realm realm,
+				int elementCount) {
+			WritableList masterList = new WritableList(realm);
+			for (int i = 0; i < elementCount; i++) {
+				masterList.add(new SimplePerson());
+			}
+
+			return new TestListDetailValueObservableList(masterList,
+					BeansObservables.valueFactory(realm, "name"), String.class);
+		}
+
+		public void change(IObservable observable) {
+			TestListDetailValueObservableList ldol = (TestListDetailValueObservableList) observable;
+			ldol.masterList.add(new SimplePerson());
+		}
+
+		public Object getElementType(IObservableCollection collection) {
+			return String.class;
+		}
+	}
+
+	private static class TestListDetailValueObservableList extends
+			ListDetailValueObservableList {
+		final IObservableList masterList;
+
+		public TestListDetailValueObservableList(IObservableList masterList,
+				IObservableFactory detailValueFactory, Object detailType) {
+			super(masterList, detailValueFactory, detailType);
+			this.masterList = masterList;
+		}
+	}
+}
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/MapDetailValueObservableMapTest.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/MapDetailValueObservableMapTest.java
new file mode 100644
index 0000000..5146b89
--- /dev/null
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/MapDetailValueObservableMapTest.java
@@ -0,0 +1,309 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.tests.internal.databinding.observable.masterdetail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import org.eclipse.core.databinding.beans.BeansObservables;
+import org.eclipse.core.databinding.observable.IObservable;
+import org.eclipse.core.databinding.observable.map.WritableMap;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.core.internal.databinding.observable.masterdetail.MapDetailValueObservableMap;
+import org.eclipse.jface.databinding.conformance.util.MapChangeEventTracker;
+import org.eclipse.jface.examples.databinding.model.SimplePerson;
+import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase;
+
+/**
+ * @since 1.3
+ */
+public class MapDetailValueObservableMapTest extends
+		AbstractDefaultRealmTestCase {
+
+	public static Test suite() {
+		TestSuite suite = new TestSuite(
+				MapDetailValueObservableMapTest.class.getName());
+		suite.addTestSuite(MapDetailValueObservableMapTest.class);
+		return suite;
+	}
+
+	public void testGetKeyType() {
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				new WritableMap(SimplePerson.class, SimplePerson.class),
+				BeansObservables.valueFactory("name"), String.class);
+
+		assertSame(SimplePerson.class, mdom.getKeyType());
+	}
+
+	public void testGetValueType() {
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				new WritableMap(), BeansObservables.valueFactory("name"),
+				String.class);
+
+		assertSame(String.class, mdom.getValueType());
+	}
+
+	public void testGetObserved() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// The observed object is the master key set.
+		assertSame(masterMap, mdom.getObserved());
+	}
+
+	public void testMasterSetInitiallyNotEmpty() {
+		WritableMap masterMap = new WritableMap();
+		SimplePerson person = new SimplePerson();
+		person.setName("name");
+		masterMap.put(person, person);
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Make sure that a non-empty master key set is initialized correctly.
+		assertEquals(masterMap.size(), mdom.size());
+		assertEquals(person.getName(), mdom.get(person));
+	}
+
+	public void testAddRemove() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Initially, the detail map is empty.
+		assertTrue(mdom.isEmpty());
+
+		// Add a first person and check that its name is in the detail map.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterMap.put(p1, p1);
+		assertEquals(masterMap.size(), mdom.size());
+		assertEquals(p1.getName(), mdom.get(p1));
+
+		// Add a second person and check that it's name is in the detail map.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name2");
+		masterMap.put(p2, p2);
+		assertEquals(masterMap.size(), mdom.size());
+		assertEquals(p2.getName(), mdom.get(p2));
+
+		// Remove the first person from the master map and check that we still
+		// have the name of the second person in the detail map.
+		masterMap.remove(p1);
+		assertEquals(masterMap.size(), mdom.size());
+		assertEquals(p2.getName(), mdom.get(p2));
+
+		// Remove the second person as well.
+		masterMap.remove(p2);
+		assertTrue(mdom.isEmpty());
+	}
+
+	public void testChangeDetail() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterMap.put(p1, p1);
+		assertEquals(p1.getName(), mdom.get(p1));
+		p1.setName("name2");
+		assertEquals(p1.getName(), mdom.get(p1));
+
+		// Change the detail attribute by changing the master.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name3");
+		masterMap.put(p1, p2);
+		assertEquals(p2.getName(), mdom.get(p1));
+	}
+
+	public void testPut() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson person = new SimplePerson();
+		person.setName("name1");
+		masterMap.put(person, person);
+		assertEquals(person.getName(), mdom.get(person));
+
+		// Set a new name on the detail map.
+		mdom.put(person, "name2");
+		// Check that the name has been propagated to the master.
+		assertEquals("name2", person.getName());
+		assertEquals(person.getName(), mdom.get(person));
+	}
+
+	public void testContainsValue() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Add a person with a given name.
+		SimplePerson person = new SimplePerson();
+		person.setName("name");
+		masterMap.put(person, person);
+
+		// Make sure the name of the person is contained.
+		assertTrue(mdom.containsValue(person.getName()));
+
+		// Remove the person and make sure that it's name cannot be found
+		// anymore.
+		masterMap.remove(person);
+		assertFalse(mdom.containsValue(person.getName()));
+	}
+
+	public void testRemove() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Add two person objects to the map.
+		SimplePerson p1 = new SimplePerson();
+		SimplePerson p2 = new SimplePerson();
+		masterMap.put(p1, p1);
+		masterMap.put(p2, p2);
+
+		// Initially, both person objects should be contained in the detail map.
+		assertTrue(mdom.containsKey(p1));
+		assertTrue(mdom.containsKey(p2));
+
+		// Remove one person and check that it is not contained anymore.
+		mdom.remove(p1);
+		assertFalse(mdom.containsKey(p1));
+		assertTrue(mdom.containsKey(p2));
+
+		// Trying to remove a non-existent is allowed but has no effect.
+		mdom.remove(p1);
+		assertFalse(mdom.containsKey(p1));
+		assertTrue(mdom.containsKey(p2));
+	}
+
+	public void testDetailObservableChangeEvent() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		MapChangeEventTracker changeTracker = MapChangeEventTracker
+				.observe(mdom);
+
+		SimplePerson person = new SimplePerson();
+		person.setName("old name");
+
+		// Initially, we should not have received any event.
+		assertEquals(0, changeTracker.count);
+
+		// Add the person and check that we receive an addition event on the
+		// correct index and with the correct value.
+		masterMap.put(person, person);
+		assertEquals(1, changeTracker.count);
+		assertEquals(1, changeTracker.event.diff.getAddedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getRemovedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getChangedKeys().size());
+		assertSame(person, changeTracker.event.diff.getAddedKeys().iterator()
+				.next());
+		assertNull(changeTracker.event.diff.getOldValue(person));
+		assertEquals("old name", changeTracker.event.diff.getNewValue(person));
+
+		// Change the detail property and check that we receive a replace
+		person.setName("new name");
+		assertEquals(2, changeTracker.count);
+		assertEquals(0, changeTracker.event.diff.getAddedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getRemovedKeys().size());
+		assertEquals(1, changeTracker.event.diff.getChangedKeys().size());
+		assertSame(person, changeTracker.event.diff.getChangedKeys().iterator()
+				.next());
+		assertEquals("old name", changeTracker.event.diff.getOldValue(person));
+		assertEquals("new name", changeTracker.event.diff.getNewValue(person));
+	}
+
+	public void testMasterNull() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Make sure null values are handled gracefully.
+		masterMap.put(null, null);
+		assertEquals(1, mdom.size());
+		assertNull(mdom.get(null));
+	}
+
+	public void testDetailObservableValuesAreDisposed() {
+		final Map detailObservables = new HashMap();
+		IObservableFactory detailValueFactory = new IObservableFactory() {
+			public IObservable createObservable(Object target) {
+				WritableValue detailObservable = new WritableValue();
+				// Remember the created observables.
+				detailObservables.put(target, detailObservable);
+				return detailObservable;
+			}
+		};
+
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, detailValueFactory, null);
+
+		Object master1 = new Object();
+		Object master2 = new Object();
+		masterMap.put(master1, master1);
+		masterMap.put(master2, master2);
+
+		// Attach a listener in order to ensure that all detail observables are
+		// actually created.
+		MapChangeEventTracker.observe(mdom);
+
+		assertEquals(mdom.size(), detailObservables.size());
+
+		// No detail observables should be disposed yet.
+		assertFalse(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertFalse(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+
+		// Only the detail observable for the removed master should be disposed.
+		masterMap.remove(master2);
+		assertFalse(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+
+		// After disposing the detail map, all detail observables should be
+		// disposed.
+		mdom.dispose();
+		assertTrue(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+	}
+
+	public void testDisposeOnMasterDisposed() {
+		WritableMap masterMap = new WritableMap();
+		MapDetailValueObservableMap mdom = new MapDetailValueObservableMap(
+				masterMap, BeansObservables.valueFactory("name"), String.class);
+
+		// Initially, nothing should be disposed.
+		assertFalse(masterMap.isDisposed());
+		assertFalse(mdom.isDisposed());
+
+		// Upon disposing the master map, the detail map should be disposed as
+		// well.
+		masterMap.dispose();
+		assertTrue(masterMap.isDisposed());
+		assertTrue(mdom.isDisposed());
+	}
+}
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/SetDetailValueObservableMapTest.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/SetDetailValueObservableMapTest.java
new file mode 100644
index 0000000..26bcf1b
--- /dev/null
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/observable/masterdetail/SetDetailValueObservableMapTest.java
@@ -0,0 +1,311 @@
+/*******************************************************************************
+ * Copyright (c) 2010 Ovidio Mallo and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Ovidio Mallo - initial API and implementation (bug 305367)
+ ******************************************************************************/
+
+package org.eclipse.core.tests.internal.databinding.observable.masterdetail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import org.eclipse.core.databinding.beans.BeansObservables;
+import org.eclipse.core.databinding.observable.IObservable;
+import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
+import org.eclipse.core.databinding.observable.set.WritableSet;
+import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.core.internal.databinding.observable.masterdetail.SetDetailValueObservableMap;
+import org.eclipse.jface.databinding.conformance.util.MapChangeEventTracker;
+import org.eclipse.jface.examples.databinding.model.SimplePerson;
+import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase;
+
+/**
+ * @since 1.3
+ */
+public class SetDetailValueObservableMapTest extends
+		AbstractDefaultRealmTestCase {
+
+	public static Test suite() {
+		TestSuite suite = new TestSuite(SetDetailValueObservableMapTest.class
+				.getName());
+		suite.addTestSuite(SetDetailValueObservableMapTest.class);
+		return suite;
+	}
+
+	public void testGetValueType() {
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				new WritableSet(), BeansObservables.valueFactory("name"),
+				String.class);
+
+		assertSame(String.class, sdom.getValueType());
+	}
+
+	public void testGetObserved() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// The observed object is the master key set.
+		assertSame(masterKeySet, sdom.getObserved());
+	}
+
+	public void testMasterSetInitiallyNotEmpty() {
+		WritableSet masterKeySet = new WritableSet();
+		SimplePerson person = new SimplePerson();
+		person.setName("name");
+		masterKeySet.add(person);
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Make sure that a non-empty master key set is initialized correctly.
+		assertEquals(masterKeySet.size(), sdom.size());
+		assertEquals(person.getName(), sdom.get(person));
+	}
+
+	public void testAddRemove() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Initially, the detail map is empty.
+		assertTrue(sdom.isEmpty());
+
+		// Add a first person and check that its name is in the detail list.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterKeySet.add(p1);
+		assertEquals(masterKeySet.size(), sdom.size());
+		assertEquals(p1.getName(), sdom.get(p1));
+
+		// Add a second person and check that it's name is in the detail list.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name2");
+		masterKeySet.add(p2);
+		assertEquals(masterKeySet.size(), sdom.size());
+		assertEquals(p2.getName(), sdom.get(p2));
+
+		// Remove the first person from the master list and check that we still
+		// have the name of the second person in the detail list.
+		masterKeySet.remove(p1);
+		assertEquals(masterKeySet.size(), sdom.size());
+		assertEquals(p2.getName(), sdom.get(p2));
+
+		// Remove the second person as well.
+		masterKeySet.remove(p2);
+		assertTrue(sdom.isEmpty());
+	}
+
+	public void testChangeDetail() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson p1 = new SimplePerson();
+		p1.setName("name1");
+		masterKeySet.add(p1);
+		assertEquals(p1.getName(), sdom.get(p1));
+		p1.setName("name2");
+		assertEquals(p1.getName(), sdom.get(p1));
+
+		// Change the detail attribute by changing the master.
+		SimplePerson p2 = new SimplePerson();
+		p2.setName("name3");
+		masterKeySet.add(p2);
+		assertEquals(p2.getName(), sdom.get(p2));
+	}
+
+	public void testPut() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Change the detail attribute explicitly.
+		SimplePerson person = new SimplePerson();
+		person.setName("name1");
+		masterKeySet.add(person);
+		assertEquals(person.getName(), sdom.get(person));
+
+		// Set a new name on the detail map.
+		sdom.put(person, "name2");
+		// Check that the name has been propagated to the master.
+		assertEquals("name2", person.getName());
+		assertEquals(person.getName(), sdom.get(person));
+	}
+
+	public void testContainsValue() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Add a person with a given name.
+		SimplePerson person = new SimplePerson();
+		person.setName("name");
+		masterKeySet.add(person);
+
+		// Make sure the name of the person is contained.
+		assertTrue(sdom.containsValue(person.getName()));
+
+		// Remove the person and make sure that it's name cannot be found
+		// anymore.
+		masterKeySet.remove(person);
+		assertFalse(sdom.containsValue(person.getName()));
+	}
+
+	public void testRemove() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Add two person objects to the map.
+		SimplePerson p1 = new SimplePerson();
+		SimplePerson p2 = new SimplePerson();
+		masterKeySet.add(p1);
+		masterKeySet.add(p2);
+
+		// Initially, both person objects should be contained in the detail map.
+		assertTrue(sdom.containsKey(p1));
+		assertTrue(sdom.containsKey(p2));
+
+		// Remove one person and check that it is not contained anymore.
+		sdom.remove(p1);
+		assertFalse(sdom.containsKey(p1));
+		assertTrue(sdom.containsKey(p2));
+
+		// Trying to remove a non-existent is allowed but has no effect.
+		sdom.remove(p1);
+		assertFalse(sdom.containsKey(p1));
+		assertTrue(sdom.containsKey(p2));
+	}
+
+	public void testDetailObservableChangeEvent() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		MapChangeEventTracker changeTracker = MapChangeEventTracker
+				.observe(sdom);
+
+		SimplePerson person = new SimplePerson();
+		person.setName("old name");
+
+		// Initially, we should not have received any event.
+		assertEquals(0, changeTracker.count);
+
+		// Add the person and check that we receive an addition event on the
+		// correct index and with the correct value.
+		masterKeySet.add(person);
+		assertEquals(1, changeTracker.count);
+		assertEquals(1, changeTracker.event.diff.getAddedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getRemovedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getChangedKeys().size());
+		assertSame(person, changeTracker.event.diff.getAddedKeys().iterator()
+				.next());
+		assertNull(changeTracker.event.diff.getOldValue(person));
+		assertEquals("old name", changeTracker.event.diff.getNewValue(person));
+
+		// Change the detail property and check that we receive a replace
+		person.setName("new name");
+		assertEquals(2, changeTracker.count);
+		assertEquals(0, changeTracker.event.diff.getAddedKeys().size());
+		assertEquals(0, changeTracker.event.diff.getRemovedKeys().size());
+		assertEquals(1, changeTracker.event.diff.getChangedKeys().size());
+		assertSame(person, changeTracker.event.diff.getChangedKeys().iterator()
+				.next());
+		assertEquals("old name", changeTracker.event.diff.getOldValue(person));
+		assertEquals("new name", changeTracker.event.diff.getNewValue(person));
+	}
+
+	public void testMasterNull() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Make sure null values are handled gracefully.
+		masterKeySet.add(null);
+		assertEquals(1, sdom.size());
+		assertNull(sdom.get(null));
+	}
+
+	public void testDetailObservableValuesAreDisposed() {
+		final Map detailObservables = new HashMap();
+		IObservableFactory detailValueFactory = new IObservableFactory() {
+			public IObservable createObservable(Object target) {
+				WritableValue detailObservable = new WritableValue();
+				// Remember the created observables.
+				detailObservables.put(target, detailObservable);
+				return detailObservable;
+			}
+		};
+
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, detailValueFactory, null);
+
+		Object master1 = new Object();
+		Object master2 = new Object();
+		masterKeySet.add(master1);
+		masterKeySet.add(master2);
+
+		// Attach a listener in order to ensure that all detail observables are
+		// actually created.
+		MapChangeEventTracker.observe(sdom);
+
+		assertEquals(sdom.size(), detailObservables.size());
+
+		// No detail observables should be disposed yet.
+		assertFalse(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertFalse(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+
+		// Only the detail observable for the removed master should be disposed.
+		masterKeySet.remove(master2);
+		assertFalse(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+
+		// After disposing the detail map, all detail observables should be
+		// disposed.
+		sdom.dispose();
+		assertTrue(((WritableValue) detailObservables.get(master1))
+				.isDisposed());
+		assertTrue(((WritableValue) detailObservables.get(master2))
+				.isDisposed());
+	}
+
+	public void testDisposeOnMasterDisposed() {
+		WritableSet masterKeySet = new WritableSet();
+		SetDetailValueObservableMap sdom = new SetDetailValueObservableMap(
+				masterKeySet, BeansObservables.valueFactory("name"),
+				String.class);
+
+		// Initially, nothing should be disposed.
+		assertFalse(masterKeySet.isDisposed());
+		assertFalse(sdom.isDisposed());
+
+		// Upon disposing the master list, the detail list should be disposed as
+		// well.
+		masterKeySet.dispose();
+		assertTrue(masterKeySet.isDisposed());
+		assertTrue(sdom.isDisposed());
+	}
+}
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/BindingTestSuite.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/BindingTestSuite.java
index 3c26247..56f2d52 100644
--- a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/BindingTestSuite.java
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/BindingTestSuite.java
@@ -17,7 +17,7 @@
  *                    246103, 249992, 256150, 256543, 262269, 175735, 262946,
  *                    255734, 263693, 169876, 266038, 268336, 270461, 271720,
  *                    283204, 281723, 283428
- *     Ovidio Mallo - bugs 237163, 235195, 299619, 306611
+ *     Ovidio Mallo - bugs 237163, 235195, 299619, 306611, 305367
  *******************************************************************************/
 package org.eclipse.jface.tests.databinding;
 
@@ -143,6 +143,9 @@
 import org.eclipse.core.tests.internal.databinding.observable.masterdetail.DetailObservableMapTest;
 import org.eclipse.core.tests.internal.databinding.observable.masterdetail.DetailObservableSetTest;
 import org.eclipse.core.tests.internal.databinding.observable.masterdetail.DetailObservableValueTest;
+import org.eclipse.core.tests.internal.databinding.observable.masterdetail.ListDetailValueObservableListTest;
+import org.eclipse.core.tests.internal.databinding.observable.masterdetail.MapDetailValueObservableMapTest;
+import org.eclipse.core.tests.internal.databinding.observable.masterdetail.SetDetailValueObservableMapTest;
 import org.eclipse.core.tests.internal.databinding.property.value.ListSimpleValueObservableListTest;
 import org.eclipse.core.tests.internal.databinding.property.value.MapSimpleValueObservableMapTest;
 import org.eclipse.core.tests.internal.databinding.property.value.SetSimpleValueObservableMapTest;
@@ -386,6 +389,9 @@
 		addTestSuite(DetailObservableMapTest.class);
 		addTest(DetailObservableSetTest.suite());
 		addTest(DetailObservableValueTest.suite());
+		addTest(ListDetailValueObservableListTest.suite());
+		addTest(MapDetailValueObservableMapTest.suite());
+		addTest(SetDetailValueObservableMapTest.suite());
 
 		// org.eclipse.core.tests.internal.databinding.property.value
 		addTestSuite(MapSimpleValueObservableMapTest.class);