Merge remote-tracking branch 'origin/releases/5.2.x' into releases/6.0.x
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/SmartFieldTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/SmartFieldTest.java
index 518fb01..f72d676 100644
--- a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/SmartFieldTest.java
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/SmartFieldTest.java
@@ -16,6 +16,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -26,6 +28,8 @@
 import org.eclipse.scout.rt.client.testenvironment.TestEnvironmentClientSession;
 import org.eclipse.scout.rt.client.ui.action.menu.AbstractMenu;
 import org.eclipse.scout.rt.client.ui.action.menu.IMenu;
+import org.eclipse.scout.rt.client.ui.basic.tree.ITree;
+import org.eclipse.scout.rt.client.ui.basic.tree.ITreeNode;
 import org.eclipse.scout.rt.client.ui.form.AbstractForm;
 import org.eclipse.scout.rt.client.ui.form.fields.button.AbstractCloseButton;
 import org.eclipse.scout.rt.client.ui.form.fields.groupbox.AbstractGroupBox;
@@ -33,6 +37,9 @@
 import org.eclipse.scout.rt.platform.BeanMetaData;
 import org.eclipse.scout.rt.platform.IBean;
 import org.eclipse.scout.rt.platform.Order;
+import org.eclipse.scout.rt.platform.job.IBlockingCondition;
+import org.eclipse.scout.rt.platform.job.Jobs;
+import org.eclipse.scout.rt.platform.status.IStatus;
 import org.eclipse.scout.rt.platform.util.Assertions;
 import org.eclipse.scout.rt.shared.data.basic.FontSpec;
 import org.eclipse.scout.rt.shared.services.lookup.ILookupCall;
@@ -309,6 +316,99 @@
   }
 
   @Test
+  public void testHierarchical_UIFacade() {
+    StyleField f = m_styleField;
+    f.setBrowseHierarchy(true);
+    f.getUIFacade().acceptProposalFromUI("Red", false, false);
+    assertRedFieldStyle(f);
+    f.getUIFacade().acceptProposalFromUI("Empty", false, false);
+    assertDefaultFieldStyle(f);
+    f.getUIFacade().acceptProposalFromUI("Yellow", false, false);
+    assertYellowFieldStyle(f);
+    f.getUIFacade().acceptProposalFromUI(null, false, false);
+    assertDefaultFieldStyle(f);
+  }
+
+  @Test
+  public void testHierarchicalOpenProposal() {
+    StyleField f = m_styleField;
+    f.setBrowseHierarchy(true);
+    f.setValue(10L);
+    f.getUIFacade().openProposalChooserFromUI("Red", true);
+    waitForProposalResult(IProposalChooser.PROP_SEARCH_RESULT);
+
+    //single result
+    assertTrue(m_styleField.isProposalChooserRegistered());
+    assertEquals("Red", m_styleField.getDisplayText());
+
+    // verifies tree is loaded containing a single node
+    @SuppressWarnings("unchecked")
+    TreeProposalChooser<Long> treeProposalChooser = (TreeProposalChooser<Long>) f.getProposalChooser();
+    ITree tree = treeProposalChooser.getModel();
+    ITreeNode rootNode = tree.getRootNode();
+    assertEquals(5, rootNode.getChildNodes().size());
+    assertEquals("Red", rootNode.getChildNode(0).getCell().getText());
+    //current value should be selected
+    assertTrue(rootNode.getChildNode(0).isSelectedNode());
+  }
+
+  @Test
+  public void testHierarchicalBrowse() {
+    StyleField f = m_styleField;
+    f.setBrowseHierarchy(true);
+
+    m_styleField.getUIFacade().openProposalChooserFromUI("*", false);
+
+    waitForProposalResult(IProposalChooser.PROP_SEARCH_RESULT);
+    //single result
+    assertTrue(m_styleField.isProposalChooserRegistered());
+    assertEquals("*", m_styleField.getDisplayText());
+
+    // verifies tree is loaded containing a single node
+    @SuppressWarnings("unchecked")
+    TreeProposalChooser<Long> treeProposalChooser = (TreeProposalChooser<Long>) f.getProposalChooser();
+    ITree tree = treeProposalChooser.getModel();
+    ITreeNode rootNode = tree.getRootNode();
+    assertEquals(5, rootNode.getChildNodes().size());
+    assertEquals("Red", rootNode.getChildNode(0).getCell().getText());
+  }
+
+  @Test
+  public void testHierarchyNoResult() {
+    StyleField f = m_styleField;
+    f.setBrowseHierarchy(true);
+
+    m_styleField.getUIFacade().openProposalChooserFromUI("unknown", false);
+
+    waitForProposalResult(IProposalChooser.PROP_STATUS);
+    //single result
+    assertTrue(m_styleField.isProposalChooserRegistered());
+    assertEquals("unknown", m_styleField.getDisplayText());
+
+    // verifies tree is loaded containing a single node
+    @SuppressWarnings("unchecked")
+    TreeProposalChooser<Long> treeProposalChooser = (TreeProposalChooser<Long>) f.getProposalChooser();
+    ITree tree = treeProposalChooser.getModel();
+    ITreeNode rootNode = tree.getRootNode();
+    assertEquals(0, rootNode.getChildNodes().size());
+  }
+
+  @Test
+  public void testThrowingLookupCall() {
+    StyleField f = m_styleField;
+    f.setBrowseHierarchy(true);
+    ((StyleLookupCall) f.getLookupCall()).allowLookup(false);
+    m_styleField.getUIFacade().openProposalChooserFromUI("Red", false);
+    waitForProposalResult(IProposalChooser.PROP_STATUS);
+    @SuppressWarnings("unchecked")
+    TreeProposalChooser<Long> treeProposalChooser = ((TreeProposalChooser<Long>) f.getProposalChooser());
+    ITree tree = treeProposalChooser.getModel();
+    ITreeNode rootNode = tree.getRootNode();
+    assertEquals(0, rootNode.getChildNodes().size());
+    assertEquals(IStatus.ERROR, treeProposalChooser.getStatus().getSeverity());
+  }
+
+  @Test
   public void testStyle_AcceptProposal() throws Throwable {
     StyleField f = m_styleField;
     f.acceptProposal(createRedLookupRow());
@@ -451,4 +551,17 @@
     // Yield the current model job permit, so that the lookup rows can be written into the model.
     ModelJobs.yield();
   }
+
+  private void waitForProposalResult(String property) {
+    final IBlockingCondition bc = Jobs.newBlockingCondition(true);
+    IProposalChooser<?, ?> chooser = m_styleField.getProposalChooser();
+    chooser.addPropertyChangeListener(property, new PropertyChangeListener() {
+
+      @Override
+      public void propertyChange(PropertyChangeEvent evt) {
+        bc.setBlocking(false);
+      }
+    });
+    bc.waitFor();
+  }
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNodeBuilder.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNodeBuilder.java
index dfa19dd..16c96aa 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNodeBuilder.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNodeBuilder.java
@@ -11,7 +11,6 @@
 package org.eclipse.scout.rt.client.ui.basic.tree;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -33,7 +32,7 @@
     return createTreeNode(new LookupRow<LOOKUP_ROW_TYPE>(primaryKey, text), nodeStatus, markChildrenLoaded);
   }
 
-  public List<ITreeNode> createTreeNodes(Collection<? extends ILookupRow<LOOKUP_ROW_TYPE>> lookupRows, int nodeStatus, boolean markChildrenLoaded) {
+  public List<ITreeNode> createTreeNodes(List<? extends ILookupRow<LOOKUP_ROW_TYPE>> lookupRows, int nodeStatus, boolean markChildrenLoaded) {
     ArrayList<ITreeNode> rootNodes = new ArrayList<>();
     HashMap<Object, ITreeNode> nodeMap = new HashMap<>();
     HashMap<LOOKUP_ROW_TYPE, ArrayList<ITreeNode>> parentChildMap = new HashMap<>();
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistField.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistField.java
index 81d8fbf..6dab23c 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistField.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistField.java
@@ -1020,10 +1020,16 @@
 
   @Override
   public void doSearch(String text, boolean selectCurrentValue, boolean synchronous) {
+    IContentAssistSearchParam<LOOKUP_KEY> param = ContentAssistSearchParam.createTextParam(toSearchText(text), selectCurrentValue);
+    doSearch(param, synchronous);
+  }
+
+  @Override
+  public void doSearch(IContentAssistSearchParam<LOOKUP_KEY> param, boolean synchronous) {
     if (isProposalChooserRegistered()) {
       getProposalChooser().setStatus(new Status(ScoutTexts.get("searchingProposals"), IStatus.OK));
     }
-    getLookupRowFetcher().update(text, selectCurrentValue, synchronous);
+    getLookupRowFetcher().update(param, synchronous);
   }
 
   // blocking lookups
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistFieldLookupRowFetcher.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistFieldLookupRowFetcher.java
index 7c7cb4e..3a1051c 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistFieldLookupRowFetcher.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractContentAssistFieldLookupRowFetcher.java
@@ -18,7 +18,7 @@
 
 public abstract class AbstractContentAssistFieldLookupRowFetcher<LOOKUP_KEY> implements IContentAssistFieldLookupRowFetcher<LOOKUP_KEY> {
 
-  private BasicPropertySupport propertySupport;
+  private final BasicPropertySupport propertySupport;
   private final IContentAssistField<?, LOOKUP_KEY> m_contentAssistField;
 
   public AbstractContentAssistFieldLookupRowFetcher(IContentAssistField<?, LOOKUP_KEY> contentAssistField) {
@@ -78,7 +78,8 @@
 
   @Override
   public IContentAssistFieldDataFetchResult<LOOKUP_KEY> newResult(String searchText, boolean selectCurrentValue) {
-    return new ContentAssistFieldDataFetchResult<LOOKUP_KEY>(Collections.<ILookupRow<LOOKUP_KEY>> emptyList(), null, searchText, selectCurrentValue);
+    IContentAssistSearchParam<LOOKUP_KEY> param = ContentAssistSearchParam.createTextParam(searchText, selectCurrentValue);
+    return new ContentAssistFieldDataFetchResult<LOOKUP_KEY>(Collections.<ILookupRow<LOOKUP_KEY>> emptyList(), null, param);
   }
 
   protected void setResult(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
@@ -89,7 +90,7 @@
   @Override
   public String getLastSearchText() {
     if (getResult() != null) {
-      return getResult().getSearchText();
+      return getResult().getSearchParam().getSearchText();
     }
     return null;
   }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractMixedSmartField.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractMixedSmartField.java
index 01854bf..9c69149 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractMixedSmartField.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractMixedSmartField.java
@@ -135,7 +135,8 @@
     }
 
     String searchText = toSearchText(text);
-    getLookupRowFetcher().update(searchText, false, true);
+    IContentAssistSearchParam<LOOKUP_KEY> param = ContentAssistSearchParam.createTextParam(searchText, false);
+    getLookupRowFetcher().update(param, true);
     List<? extends ILookupRow<LOOKUP_KEY>> lookupRows = getLookupRowFetcher().getResult().getLookupRows();
     if (lookupRows == null || lookupRows.size() == 0) {
       setValidationError(text, TEXTS.get("SmartFieldCannotComplete", text), NO_RESULTS_ERROR_CODE);
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractProposalChooser.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractProposalChooser.java
index 8afcd49..61ef4c2 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractProposalChooser.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/AbstractProposalChooser.java
@@ -10,10 +10,17 @@
  ******************************************************************************/
 package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
 
+import java.util.List;
+
+import org.eclipse.scout.rt.platform.exception.ProcessingException;
 import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver;
 import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility;
 import org.eclipse.scout.rt.platform.status.IStatus;
+import org.eclipse.scout.rt.platform.status.Status;
+import org.eclipse.scout.rt.platform.util.CollectionUtility;
 import org.eclipse.scout.rt.platform.util.TriState;
+import org.eclipse.scout.rt.platform.util.concurrent.FutureCancelledException;
+import org.eclipse.scout.rt.shared.ScoutTexts;
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
 
 public abstract class AbstractProposalChooser<T, LOOKUP_KEY> extends AbstractPropertyObserver implements IProposalChooser<T, LOOKUP_KEY> {
@@ -103,7 +110,7 @@
   public String getSearchText() {
     IContentAssistFieldDataFetchResult<LOOKUP_KEY> searchResult = getSearchResult();
     if (searchResult != null) {
-      return searchResult.getSearchText();
+      return searchResult.getSearchParam().getSearchText();
     }
     return null;
   }
@@ -183,4 +190,52 @@
     return m_contentAssistField;
   }
 
+  protected void updateStatus(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
+    if (result != null && result.getException() instanceof FutureCancelledException) {
+      return;
+    }
+
+    List<? extends ILookupRow<LOOKUP_KEY>> rows = null;
+    Throwable exception = null;
+    String searchText = null;
+    if (result != null) {
+      rows = result.getLookupRows();
+      exception = result.getException();
+      searchText = result.getSearchParam().getSearchText();
+    }
+    if (rows == null) {
+      rows = CollectionUtility.emptyArrayList();
+    }
+    String statusText = null;
+    int severity = IStatus.INFO;
+    if (exception != null) {
+      if (exception instanceof ProcessingException) {
+        statusText = ((ProcessingException) exception).getStatus().getMessage();
+      }
+      else {
+        statusText = exception.getMessage();
+      }
+      severity = IStatus.ERROR;
+    }
+    else if (rows.size() <= 0) {
+      if (getContentAssistField().getWildcard().equals(searchText)) {
+        statusText = ScoutTexts.get("SmartFieldNoDataFound");
+      }
+      else {
+        statusText = ScoutTexts.get("SmartFieldCannotComplete", (searchText == null) ? ("") : (searchText));
+      }
+      severity = IStatus.WARNING;
+    }
+    else if (rows.size() > m_contentAssistField.getBrowseMaxRowCount()) {
+      statusText = ScoutTexts.get("SmartFieldMoreThanXRows", "" + m_contentAssistField.getBrowseMaxRowCount());
+      severity = IStatus.INFO;
+    }
+    if (statusText != null) {
+      setStatus(new Status(statusText, severity));
+    }
+    else {
+      setStatus(null);
+    }
+  }
+
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetchResult.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetchResult.java
index cb3e04f..6c013a6 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetchResult.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetchResult.java
@@ -12,50 +12,43 @@
 
 import java.util.List;
 
+import org.eclipse.scout.rt.platform.util.ToStringBuilder;
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
 
 public class ContentAssistFieldDataFetchResult<LOOKUP_KEY> implements IContentAssistFieldDataFetchResult<LOOKUP_KEY> {
 
-  private final List<? extends ILookupRow<LOOKUP_KEY>> m_lookupRows;
-  private final RuntimeException m_processingException;
-  private final String m_searchText;
-  private final boolean m_selectCurrentValue;
+  private final IContentAssistSearchParam<LOOKUP_KEY> m_searchParam;
+  private final List<ILookupRow<LOOKUP_KEY>> m_lookupRows;
+  private final Throwable m_processingException;
 
-  public ContentAssistFieldDataFetchResult(List<? extends ILookupRow<LOOKUP_KEY>> rows, RuntimeException failed, String searchText, boolean selectCurrentValue) {
+  public ContentAssistFieldDataFetchResult(List<ILookupRow<LOOKUP_KEY>> rows, Throwable failed, IContentAssistSearchParam<LOOKUP_KEY> searchParam) {
     m_lookupRows = rows;
     m_processingException = failed;
-    m_searchText = searchText;
-    m_selectCurrentValue = selectCurrentValue;
+    m_searchParam = searchParam;
   }
 
   @Override
-  public List<? extends ILookupRow<LOOKUP_KEY>> getLookupRows() {
+  public List<ILookupRow<LOOKUP_KEY>> getLookupRows() {
     return m_lookupRows;
   }
 
   @Override
-  public RuntimeException getException() {
+  public Throwable getException() {
     return m_processingException;
   }
 
   @Override
-  public String getSearchText() {
-    return m_searchText;
-  }
-
-  @Override
-  public boolean isSelectCurrentValue() {
-    return m_selectCurrentValue;
+  public IContentAssistSearchParam<LOOKUP_KEY> getSearchParam() {
+    return m_searchParam;
   }
 
   @Override
   public int hashCode() {
     final int prime = 31;
     int result = 1;
+    result = prime * result + ((m_searchParam == null) ? 0 : m_searchParam.hashCode());
     result = prime * result + ((m_lookupRows == null) ? 0 : m_lookupRows.hashCode());
     result = prime * result + ((m_processingException == null) ? 0 : m_processingException.hashCode());
-    result = prime * result + ((m_searchText == null) ? 0 : m_searchText.hashCode());
-    result = prime * result + (m_selectCurrentValue ? 1231 : 1237);
     return result;
   }
 
@@ -71,6 +64,14 @@
       return false;
     }
     ContentAssistFieldDataFetchResult other = (ContentAssistFieldDataFetchResult) obj;
+    if (m_searchParam == null) {
+      if (other.m_searchParam != null) {
+        return false;
+      }
+    }
+    else if (!m_searchParam.equals(other.m_searchParam)) {
+      return false;
+    }
     if (m_lookupRows == null) {
       if (other.m_lookupRows != null) {
         return false;
@@ -87,27 +88,16 @@
     else if (!m_processingException.equals(other.m_processingException)) {
       return false;
     }
-    if (m_searchText == null) {
-      if (other.m_searchText != null) {
-        return false;
-      }
-    }
-    else if (!m_searchText.equals(other.m_searchText)) {
-      return false;
-    }
-    if (m_selectCurrentValue != other.m_selectCurrentValue) {
-      return false;
-    }
     return true;
   }
 
   @Override
   public String toString() {
-    String s = getClass().getSimpleName() + " {";
-    s += "[searchText: " + (m_searchText == null ? "null" : "'" + m_searchText + "'") + "], ";
-    s += "[lookupRows: " + (getLookupRows() == null ? "null" : getLookupRows().size() + " rows") + "], ";
-    s += "[selectCurrentValue: " + isSelectCurrentValue() + "], ";
-    s += "[processingException: " + getException() + "]}";
-    return s;
+    return new ToStringBuilder(this)
+        .attr(m_searchParam)
+        .attr("lookupRows", m_lookupRows)
+        .attr("exception", m_processingException)
+        .toString();
   }
+
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetcher.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetcher.java
index 03dd6ee..c9ee934 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetcher.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldDataFetcher.java
@@ -10,6 +10,7 @@
  ******************************************************************************/
 package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
@@ -23,7 +24,8 @@
   }
 
   @Override
-  public void update(final String searchText, boolean selectCurrentValue, boolean synchronous) {
+  public void update(IContentAssistSearchParam<LOOKUP_KEY> query, boolean synchronous) {
+    String searchText = query.getSearchText();
     String text = searchText;
     if (text == null) {
       text = "";
@@ -31,7 +33,7 @@
 
     final String textNonNull = text;
     final int maxCount = getContentAssistField().getBrowseMaxRowCount();
-    final ILookupRowFetchedCallback<LOOKUP_KEY> callback = new P_LookupCallDataCallback(searchText, selectCurrentValue);
+    final ILookupRowFetchedCallback<LOOKUP_KEY> callback = new P_LookupCallDataCallback(query);
     final int maxRowCount = (maxCount > 0 ? maxCount + 1 : 0);
 
     if (synchronous) {
@@ -58,23 +60,22 @@
   }
 
   private class P_LookupCallDataCallback implements ILookupRowFetchedCallback<LOOKUP_KEY> {
-    private String m_searchText;
-    private boolean m_selectCurrentValue;
+    private final IContentAssistSearchParam<LOOKUP_KEY> m_param;
 
-    private P_LookupCallDataCallback(String searchText, boolean selectCurrentValue) {
-      m_searchText = searchText;
-      m_selectCurrentValue = selectCurrentValue;
+    private P_LookupCallDataCallback(IContentAssistSearchParam<LOOKUP_KEY> param) {
+      m_param = param;
       setResult(null);
     }
 
     @Override
     public void onSuccess(List<? extends ILookupRow<LOOKUP_KEY>> rows) {
-      setResult(new ContentAssistFieldDataFetchResult<>(rows, null, m_searchText, m_selectCurrentValue));
+      setResult(new ContentAssistFieldDataFetchResult<>(new ArrayList<ILookupRow<LOOKUP_KEY>>(rows), null, m_param));
     }
 
     @Override
     public void onFailure(RuntimeException exception) {
-      setResult(new ContentAssistFieldDataFetchResult<LOOKUP_KEY>(null, exception, m_searchText, m_selectCurrentValue));
+      setResult(new ContentAssistFieldDataFetchResult<LOOKUP_KEY>(null, exception, m_param));
     }
   }
+
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldUIFacade.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldUIFacade.java
index ecd8334..5123171 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldUIFacade.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistFieldUIFacade.java
@@ -43,7 +43,13 @@
     m_field.clearProposal();
     m_field.setDisplayText(text);
     if (!StringUtility.equalsIgnoreNewLines(m_field.getLookupRowFetcher().getLastSearchText(), toSearchText(text))) {
-      m_field.doSearch(text, false, false);
+      if (m_field.isBrowseLoadIncremental() && m_field.getWildcard().equals(toSearchText(text))) {
+        IContentAssistSearchParam<LOOKUP_KEY> searchParam = ContentAssistSearchParam.createParentParam(null, false);
+        m_field.doSearch(searchParam, false);
+      }
+      else {
+        m_field.doSearch(text, false, false);
+      }
     }
   }
 
@@ -57,9 +63,15 @@
     m_field.setDisplayText(text);
     String searchText = toSearchText(text);
     IProposalChooser<?, LOOKUP_KEY> proposalChooser = m_field.registerProposalChooserInternal();
-    IContentAssistFieldDataFetchResult<LOOKUP_KEY> newResult = m_field.getLookupRowFetcher().newResult(toSearchText(searchText), selectCurrentValue);
+    IContentAssistFieldDataFetchResult<LOOKUP_KEY> newResult = m_field.getLookupRowFetcher().newResult(toSearchText(searchText), false);
     proposalChooser.dataFetchedDelegate(newResult, m_field.getConfiguredBrowseMaxRowCount());
-    m_field.doSearch(text, selectCurrentValue, false);
+    if (m_field.isBrowseLoadIncremental()) {
+      IContentAssistSearchParam<LOOKUP_KEY> searchParam = ContentAssistSearchParam.createParentParam(null, selectCurrentValue);
+      m_field.doSearch(searchParam, false);
+    }
+    else {
+      m_field.doSearch(text, selectCurrentValue, false);
+    }
   }
 
   @Override
@@ -105,7 +117,7 @@
       boolean acceptByLookupRow = true;
       String searchText = toSearchText(text);
       String lastSearchText = m_field.getProposalChooser().getSearchText();
-      if (!lastSearchText.equals(m_field.getWildcard())) {
+      if (lastSearchText != null && !lastSearchText.equals(m_field.getWildcard())) {
         acceptByLookupRow = CompareUtility.equals(searchText, lastSearchText);
       }
 
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistSearchParam.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistSearchParam.java
new file mode 100644
index 0000000..06b6e9f
--- /dev/null
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/ContentAssistSearchParam.java
@@ -0,0 +1,104 @@
+package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
+
+import org.eclipse.scout.rt.platform.util.ToStringBuilder;
+
+public class ContentAssistSearchParam<LOOKUP_KEY> implements IContentAssistSearchParam<LOOKUP_KEY> {
+
+  private final String m_searchText;
+  private final LOOKUP_KEY m_parentKey;
+  private final boolean m_isByParentSearch;
+  private final boolean m_selectCurrentValue;
+
+  ContentAssistSearchParam(String searchText, LOOKUP_KEY parentKey, boolean isByParentSearch, boolean selectCurrentValue) {
+    m_parentKey = parentKey;
+    m_selectCurrentValue = selectCurrentValue;
+    m_searchText = searchText;
+    m_isByParentSearch = isByParentSearch;
+  }
+
+  @Override
+  public String getSearchText() {
+    return m_searchText;
+  }
+
+  @Override
+  public LOOKUP_KEY getParentKey() {
+    return m_parentKey;
+  }
+
+  @Override
+  public boolean isSelectCurrentValue() {
+    return m_selectCurrentValue;
+  }
+
+  @Override
+  public boolean isByParentSearch() {
+    return m_isByParentSearch;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + (m_isByParentSearch ? 1231 : 1237);
+    result = prime * result + ((m_parentKey == null) ? 0 : m_parentKey.hashCode());
+    result = prime * result + ((m_searchText == null) ? 0 : m_searchText.hashCode());
+    result = prime * result + (m_selectCurrentValue ? 1231 : 1237);
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null) {
+      return false;
+    }
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+    ContentAssistSearchParam other = (ContentAssistSearchParam) obj;
+    if (m_isByParentSearch != other.m_isByParentSearch) {
+      return false;
+    }
+    if (m_parentKey == null) {
+      if (other.m_parentKey != null) {
+        return false;
+      }
+    }
+    else if (!m_parentKey.equals(other.m_parentKey)) {
+      return false;
+    }
+    if (m_searchText == null) {
+      if (other.m_searchText != null) {
+        return false;
+      }
+    }
+    else if (!m_searchText.equals(other.m_searchText)) {
+      return false;
+    }
+    if (m_selectCurrentValue != other.m_selectCurrentValue) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return new ToStringBuilder(this)
+        .attr("searchText", m_searchText)
+        .attr("parentKey", m_parentKey)
+        .attr("selectCurrentValue", m_selectCurrentValue)
+        .toString();
+  }
+
+  public static <LOOKUP_KEY> IContentAssistSearchParam<LOOKUP_KEY> createTextParam(String searchText, boolean selectCurrentValue) {
+    return new ContentAssistSearchParam<LOOKUP_KEY>(searchText, null, false, selectCurrentValue);
+  }
+
+  public static <LOOKUP_KEY> IContentAssistSearchParam<LOOKUP_KEY> createParentParam(LOOKUP_KEY parentKey, boolean selectCurrentValue) {
+    return new ContentAssistSearchParam<LOOKUP_KEY>(null, parentKey, true, selectCurrentValue);
+  }
+
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/HierachycalContentAssistDataFetcher.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/HierachycalContentAssistDataFetcher.java
index 2bb615e..af3b4c6 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/HierachycalContentAssistDataFetcher.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/HierachycalContentAssistDataFetcher.java
@@ -10,19 +10,120 @@
  ******************************************************************************/
 package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
 
-public class HierachycalContentAssistDataFetcher<LOOKUP_KEY> extends AbstractContentAssistFieldLookupRowFetcher<LOOKUP_KEY> {
+import java.util.List;
 
-  /**
-   * @param contentAssistField
-   */
+import org.eclipse.scout.rt.client.context.ClientRunContexts;
+import org.eclipse.scout.rt.client.job.ModelJobs;
+import org.eclipse.scout.rt.platform.exception.IProcessingStatus;
+import org.eclipse.scout.rt.platform.exception.ProcessingException;
+import org.eclipse.scout.rt.platform.exception.VetoException;
+import org.eclipse.scout.rt.platform.job.IFuture;
+import org.eclipse.scout.rt.platform.job.JobInput;
+import org.eclipse.scout.rt.platform.job.Jobs;
+import org.eclipse.scout.rt.platform.util.StringUtility;
+import org.eclipse.scout.rt.platform.util.concurrent.IBiFunction;
+import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HierachycalContentAssistDataFetcher<LOOKUP_KEY> extends AbstractContentAssistFieldLookupRowFetcher<LOOKUP_KEY> {
+  private static final Logger LOG = LoggerFactory.getLogger(HierachycalContentAssistDataFetcher.class);
+
   public HierachycalContentAssistDataFetcher(IContentAssistField<?, LOOKUP_KEY> contentAssistField) {
     super(contentAssistField);
   }
 
   @Override
-  public void update(String searchText, boolean selectCurrentValue, boolean synchronous) {
-    // in case of hierarchical simply delegate the searchText
-    setResult(newResult(searchText, selectCurrentValue));
+  public void update(IContentAssistSearchParam<LOOKUP_KEY> query, boolean blocking) {
+    IFuture<Void> fRes =
+        scheduleLookup(query)
+            .whenDoneSchedule(createResult(query), newJobInput())
+            .whenDoneSchedule(updateResult(), newModelJobInput());
+    if (blocking) {
+      LookupJobHelper.awaitDone(fRes);
+    }
+  }
+
+  private IBiFunction<List<ILookupRow<LOOKUP_KEY>>, Throwable, ContentAssistFieldDataFetchResult<LOOKUP_KEY>> createResult(final IContentAssistSearchParam<LOOKUP_KEY> fetchInfo) {
+    return new IBiFunction<List<ILookupRow<LOOKUP_KEY>>, Throwable, ContentAssistFieldDataFetchResult<LOOKUP_KEY>>() {
+
+      @Override
+      public ContentAssistFieldDataFetchResult<LOOKUP_KEY> apply(List<ILookupRow<LOOKUP_KEY>> rows, Throwable error) {
+        return new ContentAssistFieldDataFetchResult<>(rows, error, fetchInfo);
+      }
+    };
+  }
+
+  private IBiFunction<ContentAssistFieldDataFetchResult<LOOKUP_KEY>, Throwable, Void> updateResult() {
+    return new IBiFunction<ContentAssistFieldDataFetchResult<LOOKUP_KEY>, Throwable, Void>() {
+
+      @Override
+      public Void apply(ContentAssistFieldDataFetchResult<LOOKUP_KEY> result, Throwable error) {
+        if (result.getException() != null) {
+          logException(result.getException());
+        }
+        setResult(result);
+        return null;
+      }
+    };
+  }
+
+  /**
+   * Exceptions are handled differently in smartfields (make sure it is logged)
+   */
+  protected void logException(Throwable e) {
+    if (e instanceof VetoException) {
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("", e);
+      }
+      else {
+        LOG.info("{}: {}", e.getClass().getSimpleName(), e.getMessage());
+      }
+    }
+    else if (e instanceof ProcessingException) {
+      switch (((ProcessingException) e).getStatus().getSeverity()) {
+        case IProcessingStatus.INFO:
+        case IProcessingStatus.OK:
+          LOG.info("", e);
+          break;
+        case IProcessingStatus.WARNING:
+          LOG.warn("", e);
+          break;
+        default:
+          LOG.error("", e);
+          break;
+      }
+    }
+    else {
+      LOG.error("", e);
+    }
+  }
+
+  private JobInput newModelJobInput() {
+    return ModelJobs.newInput(ClientRunContexts.copyCurrent())
+        .withExceptionHandling(null, true);
+  }
+
+  private JobInput newJobInput() {
+    return Jobs.newInput()
+        .withRunContext(ClientRunContexts.copyCurrent())
+        .withExceptionHandling(null, true);
+  }
+
+  protected IFuture<List<ILookupRow<LOOKUP_KEY>>> scheduleLookup(IContentAssistSearchParam<LOOKUP_KEY> query) {
+    if (query.isByParentSearch()) {
+      return getContentAssistField().callSubTreeLookupInBackground(query.getParentKey(), false);
+    }
+    else if (isTextLookup(query.getSearchText()) && !query.isSelectCurrentValue()) {
+      return getContentAssistField().callTextLookupInBackground(query.getSearchText(), true);
+    }
+    else {
+      return getContentAssistField().callBrowseLookupInBackground(null, true);
+    }
+  }
+
+  protected boolean isTextLookup(String searchText) {
+    return !StringUtility.isNullOrEmpty(searchText) && !getContentAssistField().getWildcard().equals(searchText);
   }
 
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistField.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistField.java
index dae6066..8394eef 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistField.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistField.java
@@ -247,6 +247,8 @@
    */
   void doSearch(String searchText, boolean selectCurrentValue, boolean synchronous);
 
+  void doSearch(IContentAssistSearchParam<LOOKUP_KEY> param, boolean synchronous);
+
   // blocking lookups
   /**
    * Lookup rows by key using {@link ILookupCall#getDataByKey()}. Blocks until the result is available.
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldDataFetchResult.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldDataFetchResult.java
index 40969ba..1a95ea6 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldDataFetchResult.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldDataFetchResult.java
@@ -15,11 +15,11 @@
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
 
 public interface IContentAssistFieldDataFetchResult<LOOKUP_KEY> {
-  String getSearchText();
 
-  RuntimeException getException();
+  Throwable getException();
 
-  List<? extends ILookupRow<LOOKUP_KEY>> getLookupRows();
+  List<ILookupRow<LOOKUP_KEY>> getLookupRows();
 
-  boolean isSelectCurrentValue();
+  IContentAssistSearchParam<LOOKUP_KEY> getSearchParam();
+
 }
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldLookupRowFetcher.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldLookupRowFetcher.java
index 19a35e7..5c6f2bf 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldLookupRowFetcher.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistFieldLookupRowFetcher.java
@@ -38,15 +38,7 @@
    */
   void removePropertyChangeListener(String propertyName, PropertyChangeListener listener);
 
-  /**
-   * @param searchText
-   * @param selectCurrentValue
-   *          select the current proposal value in the proposal table/tree/custom If necessary in a tree, load the tree
-   *          children until the key is found
-   * @param synchronous
-   *          true to execute the lookup call synchronous
-   */
-  void update(String searchText, boolean selectCurrentValue, boolean synchronous);
+  void update(IContentAssistSearchParam<LOOKUP_KEY> searchParam, boolean synchronous);
 
   IContentAssistFieldDataFetchResult<LOOKUP_KEY> getResult();
 
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistSearchParam.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistSearchParam.java
new file mode 100644
index 0000000..3b8491a
--- /dev/null
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IContentAssistSearchParam.java
@@ -0,0 +1,13 @@
+package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
+
+public interface IContentAssistSearchParam<LOOKUP_KEY> {
+
+  String getSearchText();
+
+  LOOKUP_KEY getParentKey();
+
+  boolean isSelectCurrentValue();
+
+  boolean isByParentSearch();
+
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IncrementalTreeBuilder.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IncrementalTreeBuilder.java
index bed73e1..fb2ff12 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IncrementalTreeBuilder.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/IncrementalTreeBuilder.java
@@ -3,6 +3,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 
@@ -25,26 +26,76 @@
 
   private final IKeyLookupProvider<LOOKUP_KEY> m_provider;
 
+  private final Map<LOOKUP_KEY, ILookupRow<LOOKUP_KEY>> m_keyCache = new HashMap<>();
+
   public IncrementalTreeBuilder(IKeyLookupProvider<LOOKUP_KEY> provider) {
     m_provider = provider;
   }
 
-  public Collection<ILookupRow<LOOKUP_KEY>> getRowsWithParents(List<? extends ILookupRow<LOOKUP_KEY>> lookupRows, ITree existingTree) {
-    Map<LOOKUP_KEY, ILookupRow<LOOKUP_KEY>> allRows = new HashMap<>();
+  /**
+   * @param lookupRows
+   * @param selectedKey
+   *          selected key or null
+   * @param existingTree
+   * @return
+   */
+  public List<ILookupRow<LOOKUP_KEY>> getRowsWithParents(List<ILookupRow<LOOKUP_KEY>> lookupRows, LOOKUP_KEY parent, ITree existingTree) {
+    List<ILookupRow<LOOKUP_KEY>> res = new ArrayList<>();
+
+    cacheKeys(lookupRows);
+    HashSet<LOOKUP_KEY> allRows = new HashSet<>();
+
     List<List<ILookupRow<LOOKUP_KEY>>> paths = createPaths(lookupRows, existingTree);
-    for (List<ILookupRow<LOOKUP_KEY>> path : paths) {
-      for (ILookupRow<LOOKUP_KEY> row : path) {
-        allRows.put(row.getKey(), row);
+    if (parent == null) {
+      //ensure, the path to the root is included for every node
+      for (List<ILookupRow<LOOKUP_KEY>> path : paths) {
+        for (ILookupRow<LOOKUP_KEY> row : path) {
+          if (!allRows.contains(row.getKey())) {
+            allRows.add(row.getKey());
+            res.add(row);
+          }
+
+        }
       }
     }
-    return allRows.values();
+    else {
+      //ensure that rows other then children of the parent are included
+      for (List<ILookupRow<LOOKUP_KEY>> path : paths) {
+        if (contains(parent, path)) {
+          ILookupRow<LOOKUP_KEY> leaf = path.get(path.size() - 1);
+          if (!allRows.contains(leaf.getKey())) {
+            allRows.add(leaf.getKey());
+            res.add(leaf);
+          }
+        }
+      }
+    }
+    return res;
+  }
+
+  private boolean contains(LOOKUP_KEY key, List<ILookupRow<LOOKUP_KEY>> path) {
+    for (ILookupRow<LOOKUP_KEY> row : path) {
+      if (key.equals(row.getKey())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private ILookupRow<LOOKUP_KEY> getLookupRow(LOOKUP_KEY key) {
+    if (!m_keyCache.containsKey(key)) {
+      ILookupRow<LOOKUP_KEY> row = m_provider.getLookupRow(key);
+      m_keyCache.put(key, row);
+      return row;
+    }
+    return m_keyCache.get(key);
   }
 
   /**
    * Collects the path to the root for each node using the existing tree to lookup parent nodes, if possible. Each path
    * starts with the root node.
    */
-  public List<List<ILookupRow<LOOKUP_KEY>>> createPaths(List<? extends ILookupRow<LOOKUP_KEY>> lookupRows, ITree existingTree) {
+  public List<List<ILookupRow<LOOKUP_KEY>>> createPaths(Collection<? extends ILookupRow<LOOKUP_KEY>> lookupRows, ITree existingTree) {
     Map<LOOKUP_KEY, ILookupRow<LOOKUP_KEY>> parentMap = createParentMap(existingTree);
     List<List<ILookupRow<LOOKUP_KEY>>> paths = new ArrayList<>();
     for (ILookupRow<LOOKUP_KEY> row : lookupRows) {
@@ -54,7 +105,7 @@
       while (r != null) {
         path.add(0, r);
         if (!parentMap.containsKey(r.getKey())) {
-          ILookupRow<LOOKUP_KEY> parentRow = m_provider.getLookupRow(r.getParentKey());
+          ILookupRow<LOOKUP_KEY> parentRow = getLookupRow(r.getParentKey());
           parentMap.put(r.getKey(), parentRow);
         }
         r = parentMap.get(r.getKey());
@@ -64,6 +115,12 @@
     return paths;
   }
 
+  private void cacheKeys(Collection<? extends ILookupRow<LOOKUP_KEY>> lookupRows) {
+    for (ILookupRow<LOOKUP_KEY> row : lookupRows) {
+      m_keyCache.put(row.getKey(), row);
+    }
+  }
+
   /**
    * Assumes values of type LOOKUP_ROW_TYPE in tree cells
    */
@@ -85,6 +142,7 @@
         ILookupRow<LOOKUP_KEY> row = getLookupRow(node);
         if (row != null) {
           LOOKUP_KEY key = row.getKey();
+          m_keyCache.put(key, row);
           map.put(key, getLookupRow(parent));
         }
         return true;
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/LookupJobHelper.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/LookupJobHelper.java
new file mode 100644
index 0000000..2547f43
--- /dev/null
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/LookupJobHelper.java
@@ -0,0 +1,50 @@
+package org.eclipse.scout.rt.client.ui.form.fields.smartfield;
+
+import org.eclipse.scout.rt.client.context.ClientRunContexts;
+import org.eclipse.scout.rt.platform.annotations.Internal;
+import org.eclipse.scout.rt.platform.job.DoneEvent;
+import org.eclipse.scout.rt.platform.job.IBlockingCondition;
+import org.eclipse.scout.rt.platform.job.IDoneHandler;
+import org.eclipse.scout.rt.platform.job.IFuture;
+import org.eclipse.scout.rt.platform.job.Jobs;
+
+/**
+ * Job helper methods to be included later in the job framework.
+ */
+@Internal
+public class LookupJobHelper {
+
+  /**
+   * await result while freeing model thread
+   */
+  public static <T> T await(IFuture<T> futureRes) {
+    final IBlockingCondition bc = Jobs.newBlockingCondition(true);
+    futureRes.whenDone(new IDoneHandler<T>() {
+
+      @Override
+      public void onDone(DoneEvent<T> event) {
+        bc.setBlocking(false);
+      }
+    }, ClientRunContexts.copyCurrent());
+    bc.waitFor();
+
+    return futureRes.awaitDoneAndGet();
+  }
+
+  /**
+   * await result while freeing model thread
+   */
+  public static <T> void awaitDone(IFuture<T> futureRes) {
+    final IBlockingCondition bc = Jobs.newBlockingCondition(true);
+    futureRes.whenDone(new IDoneHandler<T>() {
+
+      @Override
+      public void onDone(DoneEvent<T> event) {
+        bc.setBlocking(false);
+      }
+    }, ClientRunContexts.copyCurrent());
+    bc.waitFor();
+    futureRes.awaitDone();
+  }
+
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TableProposalChooser.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TableProposalChooser.java
index 83cf86b..baeeb67 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TableProposalChooser.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TableProposalChooser.java
@@ -18,11 +18,7 @@
 import org.eclipse.scout.rt.client.ui.basic.table.TableListener;
 import org.eclipse.scout.rt.platform.Order;
 import org.eclipse.scout.rt.platform.annotations.ConfigOperation;
-import org.eclipse.scout.rt.platform.exception.ProcessingException;
-import org.eclipse.scout.rt.platform.status.IStatus;
-import org.eclipse.scout.rt.platform.status.Status;
 import org.eclipse.scout.rt.platform.util.CollectionUtility;
-import org.eclipse.scout.rt.shared.ScoutTexts;
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -107,8 +103,8 @@
     String searchText = null;
     if (result != null) {
       rows = result.getLookupRows();
-      selectCurrentValue = result.isSelectCurrentValue();
-      searchText = result.getSearchText();
+      selectCurrentValue = result.getSearchParam().isSelectCurrentValue();
+      searchText = result.getSearchParam().getSearchText();
     }
     if (rows == null) {
       rows = CollectionUtility.emptyArrayList();
@@ -145,50 +141,6 @@
     }
   }
 
-  protected void updateStatus(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
-    List<? extends ILookupRow<LOOKUP_KEY>> rows = null;
-    RuntimeException exception = null;
-    String searchText = null;
-    if (result != null) {
-      rows = result.getLookupRows();
-      exception = result.getException();
-      searchText = result.getSearchText();
-    }
-    if (rows == null) {
-      rows = CollectionUtility.emptyArrayList();
-    }
-    String statusText = null;
-    int severity = IStatus.INFO;
-    if (exception != null) {
-      if (exception instanceof ProcessingException) {
-        statusText = ((ProcessingException) exception).getStatus().getMessage();
-      }
-      else {
-        statusText = exception.getMessage();
-      }
-      severity = IStatus.ERROR;
-    }
-    else if (rows.size() <= 0) {
-      if (getContentAssistField().getWildcard().equals(searchText)) {
-        statusText = ScoutTexts.get("SmartFieldNoDataFound");
-      }
-      else {
-        statusText = ScoutTexts.get("SmartFieldCannotComplete", (searchText == null) ? ("") : (searchText));
-      }
-      severity = IStatus.WARNING;
-    }
-    else if (rows.size() > m_contentAssistField.getBrowseMaxRowCount()) {
-      statusText = ScoutTexts.get("SmartFieldMoreThanXRows", "" + m_contentAssistField.getBrowseMaxRowCount());
-      severity = IStatus.INFO;
-    }
-    if (statusText != null) {
-      setStatus(new Status(statusText, severity));
-    }
-    else {
-      setStatus(null);
-    }
-  }
-
   /**
    * Override this method to change that behavior of what is a single match.
    * <p>
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TreeProposalChooser.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TreeProposalChooser.java
index 3e6bd01..7487fd0 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TreeProposalChooser.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/form/fields/smartfield/TreeProposalChooser.java
@@ -13,11 +13,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.regex.Pattern;
 
-import org.eclipse.scout.rt.client.context.ClientRunContexts;
-import org.eclipse.scout.rt.client.job.ModelJobs;
-import org.eclipse.scout.rt.client.session.ClientSessionProvider;
 import org.eclipse.scout.rt.client.ui.MouseButton;
 import org.eclipse.scout.rt.client.ui.basic.cell.Cell;
 import org.eclipse.scout.rt.client.ui.basic.tree.AbstractTree;
@@ -25,27 +21,13 @@
 import org.eclipse.scout.rt.client.ui.basic.tree.AbstractTreeNodeBuilder;
 import org.eclipse.scout.rt.client.ui.basic.tree.ITree;
 import org.eclipse.scout.rt.client.ui.basic.tree.ITreeNode;
-import org.eclipse.scout.rt.client.ui.basic.tree.ITreeNodeFilter;
 import org.eclipse.scout.rt.client.ui.basic.tree.ITreeVisitor;
-import org.eclipse.scout.rt.client.ui.desktop.IDesktop;
-import org.eclipse.scout.rt.platform.BEANS;
 import org.eclipse.scout.rt.platform.Order;
 import org.eclipse.scout.rt.platform.annotations.ConfigOperation;
-import org.eclipse.scout.rt.platform.exception.ExceptionHandler;
-import org.eclipse.scout.rt.platform.job.DoneEvent;
-import org.eclipse.scout.rt.platform.job.IDoneHandler;
-import org.eclipse.scout.rt.platform.job.IFuture;
-import org.eclipse.scout.rt.platform.status.IStatus;
-import org.eclipse.scout.rt.platform.status.Status;
+import org.eclipse.scout.rt.platform.holders.Holder;
 import org.eclipse.scout.rt.platform.util.CollectionUtility;
 import org.eclipse.scout.rt.platform.util.CompareUtility;
-import org.eclipse.scout.rt.platform.util.StringUtility;
-import org.eclipse.scout.rt.platform.util.TriState;
-import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
-import org.eclipse.scout.rt.shared.ScoutTexts;
-import org.eclipse.scout.rt.shared.TEXTS;
 import org.eclipse.scout.rt.shared.services.lookup.ILookupRow;
-import org.eclipse.scout.rt.shared.services.lookup.ILookupRowFetchedCallback;
 import org.eclipse.scout.rt.shared.services.lookup.LookupRow;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -61,23 +43,15 @@
  */
 public class TreeProposalChooser<LOOKUP_KEY> extends AbstractProposalChooser<ITree, LOOKUP_KEY> {
   private static final Logger LOG = LoggerFactory.getLogger(TreeProposalChooser.class);
-
-  private P_ActiveNodesFilter m_activeNodesFilter;
-  private P_MatchingNodesFilter m_matchingNodesFilter;
-  private boolean m_selectCurrentValueRequested;
-  private boolean m_populateInitialTreeDone;
-  private volatile IFuture<Void> m_initialPolulatorFuture;
-  private boolean m_modelExternallyManaged = false;
+  private final KeyLookupProvider m_keyLookupProvider;
 
   public TreeProposalChooser(IContentAssistField<?, LOOKUP_KEY> contentAssistField, boolean allowCustomText) {
     super(contentAssistField, allowCustomText);
+    m_keyLookupProvider = new KeyLookupProvider();
   }
 
   @Override
   protected ITree createModel() {
-    m_activeNodesFilter = new P_ActiveNodesFilter();
-    m_matchingNodesFilter = new P_MatchingNodesFilter();
-
     ITree tree = createConfiguredOrDefaultModel(ITree.class);
     tree.setDefaultIconId(m_contentAssistField.getBrowseIconId());
     return tree;
@@ -110,10 +84,6 @@
 
   @Override
   public void dispose() {
-    if (m_initialPolulatorFuture != null) {
-      m_initialPolulatorFuture.cancel(false);
-    }
-
     m_model.disposeTree();
     m_model = null;
   }
@@ -125,220 +95,151 @@
 
   @Override
   protected void dataFetchedDelegateImpl(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result, int maxCount) {
-    String searchText = null;
-    boolean selectCurrentValue = false;
-    if (result != null) {
-      selectCurrentValue = result.isSelectCurrentValue();
-      searchText = result.getSearchText();
+    if (result.getException() == null) {
+      List<ITreeNode> subTree = getSubtree(result);
+      ITreeNode parentNode = getParent(result);
+
+      try {
+        if (m_model != null) {
+          m_model.setTreeChanging(true);
+          updateSubTree(m_model, parentNode, subTree);
+          expand();
+          selectNode(result);
+        }
+
+      }
+      finally {
+        if (m_model != null) {
+          m_model.setTreeChanging(false);
+        }
+      }
     }
-    if (!m_populateInitialTreeDone) {
-      m_selectCurrentValueRequested = selectCurrentValue;
-      return;
-    }
-    try {
-      m_model.setTreeChanging(true);
-      //
-      m_matchingNodesFilter.update(searchText);
-      m_model.addNodeFilter(m_matchingNodesFilter);
-    }
-    finally {
-      m_model.setTreeChanging(false);
-    }
-    setStatus(null);
+    updateStatus(result);
   }
 
   /**
-   * Populate initial tree using a {@link ClientAsyncJob}. Amount of tree loaded is depending on
-   * {@link IContentAssistField#isBrowseLoadIncremental()}.
-   * <p>
-   * loadIncremnental only loads the roots, whereas !loadIncremental loads the complete tree. Normally the latter is
-   * configured together with {@link IContentAssistField#isBrowseAutoExpandAll()}
+   * Selects a node depending on the search, if available. Selected node must be loaded.
    */
-  @Override
-  protected void init() {
-    if (m_contentAssistField.isBrowseLoadIncremental()) {
-      // Load lookup rows asynchronously
-      loadRootNode();
+  private void selectNode(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
+    if (result.getSearchParam().isSelectCurrentValue()) {
+      ITreeNode currentValueNode = getNode(m_contentAssistField.getValueAsLookupKey());
+      if (currentValueNode != null) {
+        selectValue(currentValueNode);
+      }
     }
     else {
-      setStatus(new Status(ScoutTexts.get("searchingProposals"), IStatus.OK));
-      // Load lookup rows asynchronously
-      m_initialPolulatorFuture = m_contentAssistField.callBrowseLookupInBackground(m_contentAssistField.getWildcard(), 100000, TriState.UNDEFINED, new ILookupRowFetchedCallback<LOOKUP_KEY>() {
-
-        @Override
-        public void onSuccess(List<? extends ILookupRow<LOOKUP_KEY>> rows) {
-          m_initialPolulatorFuture = null;
-
-          List<ITreeNode> subTree = new P_TreeNodeBuilder().createTreeNodes(rows, ITreeNode.STATUS_NON_CHANGED, true);
-
-          m_model.setTreeChanging(true);
-          try {
-            updateSubTree(m_model, m_model.getRootNode(), subTree);
-            if (m_contentAssistField.isBrowseAutoExpandAll()) {
-              m_model.expandAll(m_model.getRootNode());
-            }
-            commitPopulateInitialTree();
-            setStatus(null);
-          }
-          catch (RuntimeException e) {
-            setStatus(new Status(TEXTS.get("RequestProblem"), IStatus.ERROR));
-          }
-          finally {
-            m_model.setTreeChanging(false);
-          }
-        }
-
-        @Override
-        public void onFailure(RuntimeException e) {
-          m_initialPolulatorFuture = null;
-
-          setStatus(new Status(TEXTS.get("RequestProblem"), IStatus.ERROR));
-        }
-      });
-      m_initialPolulatorFuture.addExecutionHint(IContentAssistField.EXECUTION_HINT_INITIAL_LOOKUP);
+      //select first search text
+      selectNodeByText(result.getSearchParam().getSearchText());
     }
   }
 
-  /**
-   * Called when the initial tree has been loaded and the form is therefore ready to accept
-   * {@link #update(boolean, boolean)} requests.
-   */
-  private void commitPopulateInitialTree() {
-    updateActiveFilter();
-    if (m_selectCurrentValueRequested) {
-      if (m_model.getSelectedNodeCount() == 0) {
-        selectCurrentValueInternal();
-      }
+  private void expand() {
+    if (m_contentAssistField.isBrowseAutoExpandAll() && !m_contentAssistField.isBrowseLoadIncremental()) {
+      m_model.expandAll(m_model.getRootNode());
     }
-    m_populateInitialTreeDone = true;
-    m_contentAssistField.doSearch(m_selectCurrentValueRequested, true);
-  }
-
-  private boolean selectCurrentValueInternal() {
-    final LOOKUP_KEY selectedKey = m_contentAssistField.getValueAsLookupKey();
-    if (selectedKey != null) {
-      //check existing tree
-      final ArrayList<ITreeNode> matchingNodes = new ArrayList<ITreeNode>();
-      m_model.visitTree(new ITreeVisitor() {
-        @Override
-        public boolean visit(ITreeNode node) {
-          Object val = node.getCell().getValue();
-          if (val instanceof ILookupRow && CompareUtility.equals(selectedKey, ((ILookupRow) val).getKey())) {
-            matchingNodes.add(node);
-          }
-          return true;
-        }
-      });
-      if (matchingNodes.size() > 0) {
-        selectValue(m_model, matchingNodes.get(0));
-        // ticket 87030
-        for (int i = 1; i < matchingNodes.size(); i++) {
-          ITreeNode node = matchingNodes.get(i);
-          m_model.setNodeExpanded(node, true);
-          m_model.ensureVisible(matchingNodes.get(i));
-        }
-        return true;
-      }
-      else {
-        // load tree
-        ITreeNode node = loadNodeWithKey(selectedKey);
-        if (node != null) {
-          selectValue(m_model, node);
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private void selectValue(ITree tree, ITreeNode node) {
-    if (tree == null || node == null) {
-      return;
-    }
-    tree.selectNode(node);
-    if (tree.isCheckable()) {
-      tree.setNodeChecked(node, true);
+    else {
+      expandNodesWithChildren();
     }
   }
 
-  private ITreeNode loadNodeWithKey(LOOKUP_KEY key) {
-    ArrayList<ILookupRow<LOOKUP_KEY>> path = new ArrayList<ILookupRow<LOOKUP_KEY>>();
-    LOOKUP_KEY t = key;
-    while (t != null) {
-      ILookupRow<LOOKUP_KEY> row = getLookupRowFor(t);
-      if (row != null) {
-        path.add(0, row);
-        t = row.getParentKey();
+  private ITreeNode getParent(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
+    if (m_model != null) {
+      if (result.getSearchParam().getParentKey() == null) {
+        return m_model.getRootNode();
       }
-      else {
-        t = null;
-      }
-    }
-    ITreeNode parentNode = m_model.getRootNode();
-    for (int i = 0; i < path.size() && parentNode != null; i++) {
-      parentNode.ensureChildrenLoaded();
-      parentNode.setExpanded(true);
-      Object childKey = path.get(i).getKey();
-      ITreeNode nextNode = null;
-      for (ITreeNode n : parentNode.getChildNodes()) {
-        if (n.getCell().getValue() instanceof ILookupRow) {
-          if (CompareUtility.equals(((ILookupRow) n.getCell().getValue()).getKey(), childKey)) {
-            nextNode = n;
-            break;
-          }
-        }
-      }
-      parentNode = nextNode;
-    }
-    return parentNode;
-  }
-
-  private ILookupRow<LOOKUP_KEY> getLookupRowFor(LOOKUP_KEY key) {
-    if (key instanceof Number && ((Number) key).longValue() == 0) {
-      key = null;
-    }
-    if (key != null) {
-      IContentAssistField<?, LOOKUP_KEY> sf = (IContentAssistField<?, LOOKUP_KEY>) m_contentAssistField;
-      for (ILookupRow<LOOKUP_KEY> row : sf.callKeyLookup(key)) {
-        return row;
-      }
+      return getNode(result.getSearchParam().getParentKey());
     }
     return null;
   }
 
-  private void updateActiveFilter() {
-    try {
-      m_model.setTreeChanging(true);
-      //
-      if (m_contentAssistField.isActiveFilterEnabled()) {
-        m_activeNodesFilter.update(m_contentAssistField.getActiveFilter());
-      }
-      else {
-        m_activeNodesFilter.update(TriState.TRUE);
-      }
-      m_model.addNodeFilter(m_activeNodesFilter);
+  private List<ITreeNode> getSubtree(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
+    List<? extends ILookupRow<LOOKUP_KEY>> rows = getRows(result);
+    boolean markChildrenLoaded = !m_contentAssistField.isBrowseLoadIncremental() || !result.getSearchParam().isByParentSearch();
+    return new P_TreeNodeBuilder()
+        .createTreeNodes(rows, ITreeNode.STATUS_NON_CHANGED, markChildrenLoaded);
+  }
+
+  /**
+   * @return all new rows to be inserted (including parents of search result)
+   */
+  private List<? extends ILookupRow<LOOKUP_KEY>> getRows(IContentAssistFieldDataFetchResult<LOOKUP_KEY> result) {
+    LOOKUP_KEY parent = result.getSearchParam().getParentKey();
+    return new IncrementalTreeBuilder<LOOKUP_KEY>(m_keyLookupProvider)
+        .getRowsWithParents(result.getLookupRows(), parent, m_model);
+  }
+
+  /**
+   * find first node with matching text
+   */
+  private void selectNodeByText(final String searchText) {
+    if (searchText != null) {
+      m_model.visitTree(new ITreeVisitor() {
+
+        @Override
+        public boolean visit(ITreeNode node) {
+          if (searchText.equals(node.getCell().getText())) {
+            selectValue(node);
+            return false;
+          }
+          return true;
+        }
+      });
     }
-    finally {
-      m_model.setTreeChanging(false);
+  }
+
+  private void expandNodesWithChildren() {
+    m_model.visitTree(new ITreeVisitor() {
+
+      @Override
+      public boolean visit(ITreeNode node) {
+        if (node.getChildNodeCount() > 0) {
+          node.setChildrenLoaded(true);
+          node.setExpanded(true);
+        }
+        return true;
+      }
+    });
+  }
+
+  private void selectValue(ITreeNode node) {
+    if (node != null) {
+      m_model.selectNode(node);
+      if (m_model.isCheckable()) {
+        m_model.setNodeChecked(node, true);
+      }
     }
   }
 
+  /**
+   * Node with a given key. Assumes the node is already loaded at this point
+   */
+  private ITreeNode getNode(final LOOKUP_KEY key) {
+    final Holder<ITreeNode> holder = new Holder<ITreeNode>(ITreeNode.class);
+    m_model.visitTree(new ITreeVisitor() {
+
+      @Override
+      public boolean visit(ITreeNode node) {
+        if (node.getCell().getValue() instanceof ILookupRow && CompareUtility.equals(((ILookupRow) node.getCell().getValue()).getKey(), key)) {
+          holder.setValue(node);
+          return false;
+        }
+        return true;
+      }
+    });
+    return holder.getValue();
+  }
+
   private void updateSubTree(ITree tree, final ITreeNode parentNode, List<ITreeNode> subTree) {
     if (tree == null || parentNode == null || subTree == null) {
       return;
     }
     tree.removeAllChildNodes(parentNode);
     tree.addChildNodes(parentNode, subTree);
-  }
-
-  public void loadRootNode() {
-    if (m_model != null && !m_modelExternallyManaged) {
-      loadChildNodes(m_model.getRootNode());
-    }
+    parentNode.setChildrenLoaded(true);
   }
 
   public void loadChildNodes(ITreeNode parentNode) {
-    if (m_model != null && !m_modelExternallyManaged) {
+    if (m_model != null) {
       try {
         m_model.setTreeChanging(true);
         //
@@ -359,100 +260,13 @@
   @SuppressWarnings("unchecked")
   protected void execLoadChildNodes(final ITreeNode parentNode) {
     if (m_contentAssistField.isBrowseLoadIncremental()) {
-      final IStatus originalStatus = getStatus();
-      setStatus(new Status(ScoutTexts.get("searchingProposals"), IStatus.OK));
-
-      //load node
       ILookupRow<LOOKUP_KEY> b = (LookupRow) (parentNode != null ? parentNode.getCell().getValue() : null);
       LOOKUP_KEY parentKey = b != null ? b.getKey() : null;
-      m_contentAssistField.callSubTreeLookupInBackground(parentKey, TriState.UNDEFINED, false)
-          .whenDone(new IDoneHandler<List<ILookupRow<LOOKUP_KEY>>>() {
-            @Override
-            public void onDone(DoneEvent<List<ILookupRow<LOOKUP_KEY>>> event) {
-              final List<? extends ILookupRow<LOOKUP_KEY>> result = event.getResult();
-              final Throwable exception = event.getException();
-
-              ModelJobs.schedule(new IRunnable() {
-
-                @Override
-                public void run() {
-                  if (result != null) {
-                    List<ITreeNode> subTree = new P_TreeNodeBuilder().createTreeNodes(result, ITreeNode.STATUS_NON_CHANGED, false);
-                    updateSubTree(m_model, parentNode, subTree);
-                    commitPopulateInitialTree();
-
-                    // hide loading status
-                    setStatus(originalStatus);
-                  }
-                  else if (exception != null) {
-                    LOG.error("Error in subtree lookup", exception);
-                    setStatus(new Status(TEXTS.get("RequestProblem"), IStatus.ERROR));
-                  }
-                }
-              }, ModelJobs.newInput(ClientRunContexts.copyCurrent()));
-            }
-          }, ClientRunContexts.copyCurrent());
-    }
-
-  }
-
-  /**
-   * @return the pattern used to filter tree nodes based on the text typed into the smartfield
-   */
-  @ConfigOperation
-  @Order(20)
-  protected Pattern execCreatePatternForTreeFilter(String filterText) {
-    // check pattern
-    String s = filterText;
-    if (s == null) {
-      s = "";
-    }
-    s = s.toLowerCase();
-    IDesktop desktop = ClientSessionProvider.currentSession().getDesktop();
-    if (desktop != null && desktop.isAutoPrefixWildcardForTextSearch()) {
-      s = getContentAssistField().getWildcard() + s;
-    }
-    s = s.replace(getContentAssistField().getWildcard(), "@wildcard@");
-    s = StringUtility.escapeRegexMetachars(s);
-    s = s.replace("@wildcard@", ".*");
-    if (!s.endsWith(".*")) {
-      s = s + ".*";
-    }
-    return Pattern.compile(s, Pattern.DOTALL);
-
-  }
-
-  /**
-   * @return true if the node is accepted by the tree filter pattern defined in
-   *         {@link #execCreatePatternForTreeFilter(String)}
-   */
-  @ConfigOperation
-  @Order(30)
-  protected boolean execAcceptNodeByTreeFilter(Pattern filterPattern, ITreeNode node, int level) {
-    IContentAssistField<?, LOOKUP_KEY> sf = m_contentAssistField;
-    @SuppressWarnings("unchecked")
-    ILookupRow<LOOKUP_KEY> row = (ILookupRow<LOOKUP_KEY>) node.getCell().getValue();
-    if (node.isChildrenLoaded()) {
-      if (row != null) {
-        String q1 = node.getTree().getPathText(node, "\n");
-        String q2 = node.getTree().getPathText(node, " ");
-        if (q1 != null && q2 != null) {
-          String[] path = (q1 + "\n" + q2).split("\n");
-          for (String pathText : path) {
-            if (pathText != null && filterPattern.matcher(pathText.toLowerCase()).matches()) {
-              // use "level-1" because a tree smart field assumes its tree to
-              // have multiple roots, but the ITree model is built as
-              // single-root tree with invisible root node
-              if (sf.acceptBrowseHierarchySelection(row.getKey(), level - 1, node.isLeaf())) {
-                return true;
-              }
-            }
-          }
-        }
-        return false;
+      getContentAssistField().doSearch(ContentAssistSearchParam.createParentParam(parentKey, false), false);
+      if (parentNode != null) {
+        parentNode.setChildrenLoaded(true);
       }
     }
-    return true;
   }
 
   /**
@@ -463,45 +277,26 @@
   @Order(40)
   @Override
   protected ILookupRow<LOOKUP_KEY> execGetSingleMatch() {
-    // when load incremental is set, don't visit the tree but use text-to-key
-    // lookup method on smartfield.
-    if (m_contentAssistField.isBrowseLoadIncremental()) {
-      try {
-        List<? extends ILookupRow<LOOKUP_KEY>> rows = m_contentAssistField.callTextLookup(getSearchText(), 2);
-        if (rows != null && rows.size() == 1) {
-          return rows.get(0);
+    final List<ILookupRow<LOOKUP_KEY>> foundLeafs = new ArrayList<>();
+    ITreeVisitor v = new ITreeVisitor() {
+      @Override
+      public boolean visit(ITreeNode node) {
+        if (node.isEnabled() && node.isLeaf()) {
+          @SuppressWarnings("unchecked")
+          ILookupRow<LOOKUP_KEY> row = (ILookupRow<LOOKUP_KEY>) node.getCell().getValue();
+          if (row != null && row.isEnabled()) {
+            foundLeafs.add(row);
+          }
         }
-        else {
-          return null;
-        }
+        return foundLeafs.size() <= 2;
       }
-      catch (RuntimeException e) {
-        BEANS.get(ExceptionHandler.class).handle(e);
-        return null;
-      }
+    };
+    m_model.visitVisibleTree(v);
+    if (foundLeafs.size() == 1) {
+      return foundLeafs.get(0);
     }
     else {
-      final List<ILookupRow<LOOKUP_KEY>> foundLeafs = new ArrayList<ILookupRow<LOOKUP_KEY>>();
-      ITreeVisitor v = new ITreeVisitor() {
-        @Override
-        public boolean visit(ITreeNode node) {
-          if (node.isEnabled() && node.isLeaf()) {
-            @SuppressWarnings("unchecked")
-            ILookupRow<LOOKUP_KEY> row = (ILookupRow<LOOKUP_KEY>) node.getCell().getValue();
-            if (row != null && row.isEnabled()) {
-              foundLeafs.add(row);
-            }
-          }
-          return foundLeafs.size() <= 2;
-        }
-      };
-      m_model.visitVisibleTree(v);
-      if (foundLeafs.size() == 1) {
-        return foundLeafs.get(0);
-      }
-      else {
-        return null;
-      }
+      return null;
     }
   }
 
@@ -551,46 +346,23 @@
     }
   }
 
-  private class P_MatchingNodesFilter implements ITreeNodeFilter {
-    private Pattern m_searchPattern;
-
-    public P_MatchingNodesFilter() {
-    }
-
-    public void update(String text) {
-      m_searchPattern = execCreatePatternForTreeFilter(text);
-    }
+  private class KeyLookupProvider implements IKeyLookupProvider<LOOKUP_KEY> {
 
     @Override
-    public boolean accept(ITreeNode node, int level) {
-      return execAcceptNodeByTreeFilter(m_searchPattern, node, level);
+    public ILookupRow<LOOKUP_KEY> getLookupRow(LOOKUP_KEY key) {
+      //do not cancel lookups that are already in progress
+      List<ILookupRow<LOOKUP_KEY>> rows = LookupJobHelper.await(m_contentAssistField.callKeyLookupInBackground(key, false));
+      if (rows.size() == 0) {
+        return null;
+      }
+      else if (rows.size() > 1) {
+        LOG.error("More than one row found for key {}", key);
+        return null;
+      }
+
+      return rows.get(0);
     }
+
   }
 
-  private class P_ActiveNodesFilter implements ITreeNodeFilter {
-    private TriState m_ts;
-
-    public P_ActiveNodesFilter() {
-    }
-
-    public void update(TriState ts) {
-      m_ts = ts;
-    }
-
-    @Override
-    public boolean accept(ITreeNode node, int level) {
-      if (m_ts.isUndefined()) {
-        return true;
-      }
-      else {
-        ILookupRow row = (LookupRow) node.getCell().getValue();
-        if (row != null) {
-          return row.isActive() == m_ts.equals(TriState.TRUE);
-        }
-        else {
-          return true;
-        }
-      }
-    }
-  }
 }
diff --git a/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/job/WhenDoneScheduleTest.java b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/job/WhenDoneScheduleTest.java
new file mode 100644
index 0000000..0f8350f
--- /dev/null
+++ b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/job/WhenDoneScheduleTest.java
@@ -0,0 +1,616 @@
+package org.eclipse.scout.rt.platform.job;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.context.RunContexts;
+import org.eclipse.scout.rt.platform.context.RunMonitor;
+import org.eclipse.scout.rt.platform.exception.DefaultExceptionTranslator;
+import org.eclipse.scout.rt.platform.util.concurrent.FutureCancelledException;
+import org.eclipse.scout.rt.platform.util.concurrent.IBiFunction;
+import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
+import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedException;
+import org.eclipse.scout.rt.platform.util.concurrent.TimedOutException;
+import org.eclipse.scout.rt.testing.platform.job.JobTestUtil;
+import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner;
+import org.eclipse.scout.rt.testing.platform.runner.Times;
+import org.eclipse.scout.rt.testing.platform.util.BlockingCountDownLatch;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(PlatformTestRunner.class)
+@Times(100) // for regression purpose
+public class WhenDoneScheduleTest {
+
+  private static final String JOB_MARKER = UUID.randomUUID().toString();
+
+  @Test
+  public void testCascading() {
+    IFuture<String> future = Jobs.schedule(
+        new Callable<String>() {
+
+          @Override
+          public String call() throws Exception {
+            return "a";
+          }
+        }, Jobs.newInput())
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable u) {
+            return result + "b";
+          }
+        }, Jobs.newInput())
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable u) {
+            return result + "c";
+          }
+        }, Jobs.newInput())
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable u) {
+            return result + "d";
+          }
+        }, Jobs.newInput());
+
+    assertEquals("abcd", future.awaitDoneAndGet());
+  }
+
+  @Test
+  public void testCascadingException() {
+    final Exception exception1 = new Exception("JUnit test exception 1");
+    final RuntimeException exception2 = new RuntimeException("JUnit test exception 2");
+    final RuntimeException exception3 = new RuntimeException("JUnit test exception 3");
+    final RuntimeException exception4 = new RuntimeException("JUnit test exception 4");
+
+    IFuture<String> future = Jobs.schedule(
+        new Callable<String>() {
+
+          @Override
+          public String call() throws Exception {
+            throw exception1;
+          }
+        }, Jobs.newInput()
+            .withExceptionHandling(null, false))
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable e) {
+            assertSame(exception1, e);
+            throw exception2;
+          }
+        }, Jobs.newInput()
+            .withExceptionHandling(null, false))
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable e) {
+            assertSame(exception2, e);
+            throw exception3;
+          }
+        }, Jobs.newInput()
+            .withExceptionHandling(null, false))
+        .whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+          @Override
+          public String apply(String result, Throwable e) {
+            assertSame(exception3, e);
+            throw exception4;
+          }
+        }, Jobs.newInput()
+            .withExceptionHandling(null, false));
+
+    try {
+      future.awaitDoneAndGet();
+      fail("exception expected");
+    }
+    catch (Exception e) {
+      assertSame(exception4, e);
+    }
+  }
+
+  @Test
+  public void testNotSameFuture() {
+    // Schedule future
+    IFuture<Void> future = Jobs.schedule(new IRunnable() {
+
+      @Override
+      public void run() throws Exception {
+      }
+    }, Jobs.newInput());
+
+    // Schedule function
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<Void, Throwable, Void>() {
+
+      @Override
+      public Void apply(Void t, Throwable u) {
+        return null;
+      }
+    }, Jobs.newInput());
+
+    assertNotSame(future, functionFuture);
+  }
+
+  @Test
+  public void testResult() {
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        return "abc";
+      }
+    }, Jobs.newInput());
+
+    // Schedule function
+    IFuture<Integer> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Integer>() {
+
+      @Override
+      public Integer apply(String result, Throwable error) {
+        return result.length();
+      }
+    }, Jobs.newInput());
+
+    assertEquals("abc", future.awaitDoneAndGet());
+    assertEquals(3, functionFuture.awaitDoneAndGet().intValue());
+  }
+
+  @Test
+  public void testCancellationOfFuture() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        setupLatch.countDownAndBlock();
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true) // to not work with JUnitExceptionHandler
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    future.cancel(true);
+
+    // Wait until the job jobs are done
+    Jobs.getJobManager().awaitDone(Jobs.newFutureFilterBuilder()
+        .andMatchExecutionHint(JOB_MARKER)
+        .toFilter(), 5, TimeUnit.SECONDS);
+
+    // Verify that the function is not executed
+    assertFalse(functionExecuted.get());
+
+    // Verify that both futures are cancelled
+    assertFutureCancelled(future);
+    assertFutureCancelled(functionFuture);
+  }
+
+  @Test
+  public void testCancellationOfFunctionFuture() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        setupLatch.countDownAndBlock();
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true) // to not work with JUnitExceptionHandler
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    functionFuture.cancel(true);
+    setupLatch.unblock();
+
+    // Wait until the job jobs are done
+    Jobs.getJobManager().awaitDone(Jobs.newFutureFilterBuilder()
+        .andMatchExecutionHint(JOB_MARKER)
+        .toFilter(), 5, TimeUnit.SECONDS);
+
+    // Verify that the function is not executed
+    assertFalse(functionExecuted.get());
+
+    // Verify that future is not cancelled
+    assertEquals("abc", future.awaitDoneAndGet());
+    assertFalse(future.isCancelled());
+    assertTrue(future.isDone());
+
+    // Verify that function future is cancelled
+    assertFutureCancelled(functionFuture);
+  }
+
+  @Test
+  public void testCancellationOfFutureRunMonitor() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    RunMonitor runMonitor = BEANS.get(RunMonitor.class);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        setupLatch.countDownAndBlock();
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true) // to not work with JUnitExceptionHandler
+        .withRunContext(RunContexts.empty().withRunMonitor(runMonitor))
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    runMonitor.cancel(true);
+
+    // Wait until the job jobs are done
+    Jobs.getJobManager().awaitDone(Jobs.newFutureFilterBuilder()
+        .andMatchExecutionHint(JOB_MARKER)
+        .toFilter(), 5, TimeUnit.SECONDS);
+
+    // Verify that the function is not executed
+    assertFalse(functionExecuted.get());
+
+    // Verify that both futures are cancelled
+    assertFutureCancelled(future);
+    assertFutureCancelled(functionFuture);
+  }
+
+  @Test
+  public void testCancellationOfFunctionRunMonitor() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        setupLatch.countDownAndBlock();
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true) // to not work with JUnitExceptionHandler
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+
+    RunMonitor functionRunMonitor = BEANS.get(RunMonitor.class);
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withRunContext(RunContexts.empty().withRunMonitor(functionRunMonitor))
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    functionRunMonitor.cancel(true);
+    setupLatch.unblock();
+
+    // Wait until the job jobs are done
+    Jobs.getJobManager().awaitDone(Jobs.newFutureFilterBuilder()
+        .andMatchExecutionHint(JOB_MARKER)
+        .toFilter(), 5, TimeUnit.SECONDS);
+
+    // Verify that the function is not executed
+    assertFalse(functionExecuted.get());
+
+    // Verify that future is not cancelled
+    assertEquals("abc", future.awaitDoneAndGet());
+    assertFalse(future.isCancelled());
+    assertTrue(future.isDone());
+
+    // Verify that function future is cancelled
+    assertFutureCancelled(functionFuture);
+  }
+
+  @Test
+  public void testCancellationOfSharedRunMonitor() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    RunMonitor sharedRunMonitor = BEANS.get(RunMonitor.class);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        setupLatch.countDownAndBlock();
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true) // to not work with JUnitExceptionHandler
+        .withRunContext(RunContexts.empty().withRunMonitor(sharedRunMonitor))
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withRunContext(RunContexts.empty().withRunMonitor(sharedRunMonitor))
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    sharedRunMonitor.cancel(true);
+
+    // Wait until the job jobs are done
+    Jobs.getJobManager().awaitDone(Jobs.newFutureFilterBuilder()
+        .andMatchExecutionHint(JOB_MARKER)
+        .toFilter(), 5, TimeUnit.SECONDS);
+
+    // Verify that the function is not executed
+    assertFalse(functionExecuted.get());
+
+    // Verify that both futures are cancelled
+    assertFutureCancelled(future);
+    assertFutureCancelled(functionFuture);
+  }
+
+  @Test
+  public void testPostCancellation() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExecutionHint(JOB_MARKER));
+
+    // Schedule function
+    IFuture<String> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, String>() {
+
+      @Override
+      public String apply(String result, Throwable error) {
+        try {
+          setupLatch.countDownAndBlock();
+        }
+        catch (InterruptedException e) {
+          throw new ThreadInterruptedException("", e);
+        }
+        return result.toUpperCase();
+      }
+    }, Jobs.newInput()
+        .withExecutionHint(JOB_MARKER));
+
+    assertTrue(setupLatch.await());
+    // Cancel future which already completed
+    future.cancel(true);
+    setupLatch.unblock();
+    assertEquals("abc", future.awaitDoneAndGet());
+    assertEquals("ABC", functionFuture.awaitDoneAndGet());
+    assertFalse(future.isCancelled());
+    assertFalse(functionFuture.isCancelled());
+  }
+
+  @Test
+  @Times(20)
+  public void testSemaphore() throws InterruptedException {
+    final BlockingCountDownLatch setupLatch = new BlockingCountDownLatch(1);
+
+    IExecutionSemaphore mutex = Jobs.newExecutionSemaphore(1).seal();
+
+    // Schedule arbitrary job with same semaphore
+    Jobs.schedule(new IRunnable() {
+
+      @Override
+      public void run() throws Exception {
+        setupLatch.countDownAndBlock();
+      }
+    }, Jobs.newInput()
+        .withExecutionSemaphore(mutex));
+
+    // Schedule future
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        return "abc";
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, true)); // to not work with JUnitExceptionHandler
+
+    // Schedule function with same semaphore
+    final AtomicBoolean functionExecuted = new AtomicBoolean(false);
+    IFuture<Void> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Void>() {
+
+      @Override
+      public Void apply(String result, Throwable error) {
+        functionExecuted.set(true);
+        return null;
+      }
+    }, Jobs.newInput()
+        .withExecutionSemaphore(mutex));
+
+    assertEquals("abc", future.awaitDoneAndGet(5, TimeUnit.SECONDS));
+
+    JobTestUtil.waitForPermitCompetitors(mutex, 2);
+
+    // Function future must not have commenced execution yet.
+    try {
+      functionFuture.awaitDone(100, TimeUnit.MILLISECONDS);
+      fail("timeout expected");
+    }
+    catch (TimedOutException e) {
+      // NOOP
+    }
+
+    assertFalse(functionExecuted.get());
+    setupLatch.unblock();
+    functionFuture.awaitDone(5, TimeUnit.SECONDS);
+    assertTrue(functionExecuted.get());
+
+    assertFalse(future.isCancelled());
+    assertTrue(future.isDone());
+    assertFalse(functionFuture.isCancelled());
+    assertTrue(functionFuture.isDone());
+  }
+
+  @Test
+  public void testException() throws Exception {
+    final Exception testException = new Exception("JUnit test exception");
+
+    final AtomicReference<Throwable> actualException = new AtomicReference<>();
+    final AtomicReference<Object> actualResult = new AtomicReference<>();
+
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        throw testException;
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, false)); // to not work with JUnitExceptionHandler
+
+    IFuture<Integer> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Integer>() {
+
+      @Override
+      public Integer apply(String result, Throwable error) {
+        actualResult.set(result);
+        actualException.set(error);
+        return 123;
+      }
+    }, Jobs.newInput());
+
+    // Wait until function future completed
+    functionFuture.awaitDone();
+
+    try {
+      future.awaitDoneAndGet(DefaultExceptionTranslator.class);
+      fail("exception expected");
+    }
+    catch (Exception e) {
+      assertSame(testException, e);
+    }
+
+    assertEquals(123, functionFuture.awaitDoneAndGet(DefaultExceptionTranslator.class).intValue());
+    assertSame(testException, actualException.get());
+    assertNull(actualResult.get());
+    assertFalse(future.isCancelled());
+    assertFalse(functionFuture.isCancelled());
+  }
+
+  @Test
+  public void testExceptionInFunction() throws Exception {
+    final RuntimeException testException = new RuntimeException("JUnit test exception");
+
+    final AtomicReference<Throwable> actualException = new AtomicReference<>();
+    final AtomicReference<Object> actualResult = new AtomicReference<>();
+
+    IFuture<String> future = Jobs.schedule(new Callable<String>() {
+
+      @Override
+      public String call() throws Exception {
+        return "abc";
+      }
+    }, Jobs.newInput());
+
+    IFuture<Integer> functionFuture = future.whenDoneSchedule(new IBiFunction<String, Throwable, Integer>() {
+
+      @Override
+      public Integer apply(String result, Throwable error) {
+        actualResult.set(result);
+        actualException.set(error);
+        throw testException;
+      }
+    }, Jobs.newInput()
+        .withExceptionHandling(null, false)); // to not work with JUnitExceptionHandler
+
+    // Wait until function future completed
+    functionFuture.awaitDone();
+
+    assertEquals("abc", future.awaitDoneAndGet());
+    assertNull(actualException.get());
+    assertEquals("abc", actualResult.get());
+
+    try {
+      functionFuture.awaitDoneAndGet();
+      fail("exception expected");
+    }
+    catch (Exception e) {
+      assertSame(testException, e);
+    }
+
+    assertFalse(future.isCancelled());
+    assertFalse(functionFuture.isCancelled());
+  }
+
+  private static void assertFutureCancelled(IFuture<?> future) {
+    try {
+      future.awaitDoneAndGet();
+      fail("cancellation excepted");
+    }
+    catch (FutureCancelledException e) {
+      assertTrue(future.isCancelled());
+      assertTrue(future.isDone());
+    }
+  }
+}
diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/IFuture.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/IFuture.java
index b94789a..d62dbaf 100644
--- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/IFuture.java
+++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/IFuture.java
@@ -14,6 +14,7 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.scout.rt.platform.context.RunContext;
+import org.eclipse.scout.rt.platform.context.RunMonitor;
 import org.eclipse.scout.rt.platform.exception.DefaultRuntimeExceptionTranslator;
 import org.eclipse.scout.rt.platform.exception.IExceptionTranslator;
 import org.eclipse.scout.rt.platform.filter.AndFilter;
@@ -25,6 +26,7 @@
 import org.eclipse.scout.rt.platform.job.listener.JobEvent;
 import org.eclipse.scout.rt.platform.util.IRegistrationHandle;
 import org.eclipse.scout.rt.platform.util.concurrent.FutureCancelledException;
+import org.eclipse.scout.rt.platform.util.concurrent.IBiFunction;
 import org.eclipse.scout.rt.platform.util.concurrent.ICancellable;
 import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedException;
 import org.eclipse.scout.rt.platform.util.concurrent.TimedOutException;
@@ -260,6 +262,32 @@
   IFuture<RESULT> whenDone(IDoneHandler<RESULT> callback, RunContext runContext);
 
   /**
+   * Schedules a new job to execute the given function after <code>this</code> future completes. Thereby, the function
+   * is provided with the result or failure of <code>this</code> future, and is invoked only if neither
+   * <code>this</code> future, nor the function's future, nor the {@link RunMonitor} associated with the function's job
+   * input is cancelled. If the evaluation of the function throws an exception, it is relayed to consumers of the
+   * returned future.
+   * <p>
+   * Unlike {@link #whenDone(IDoneHandler, RunContext)}, the future returned does not represent <code>this</code>
+   * future, but the function's future instead.
+   * <p>
+   * If the function's future or {@link RunContext} are cancelled, this does not imply that <code>this</code> future is
+   * cancelled as well. Propagation of cancellation works only the other way round.
+   *
+   * @param function
+   *          the function to be executed upon completion of <code>this</code> future, and is invoked only if neither
+   *          <code>this</code> future, nor the function's future, nor the {@link RunMonitor} associated with the
+   *          function's job input is cancelled.
+   *          <p>
+   *          The function's first argument is provided with <code>this</code> future's result and the second with
+   *          <code>this</code> future's exception (if any).
+   * @param input
+   *          input to schedule the function.
+   * @return the future representing the asynchronous execution of the function.
+   */
+  <FUNCTION_RESULT> IFuture<FUNCTION_RESULT> whenDoneSchedule(IBiFunction<RESULT, Throwable, FUNCTION_RESULT> function, JobInput input);
+
+  /**
    * Registers the given listener to be notified about all job lifecycle events related to this Future. If the listener
    * is already registered, that previous registration is replaced.
    *
diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/CompletionPromise.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/CompletionPromise.java
index 76b2956..4bc7aa9 100644
--- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/CompletionPromise.java
+++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/CompletionPromise.java
@@ -85,8 +85,6 @@
     }
 
     // Notify registered handlers asynchronously.
-    // Notice: Do not notify via Jobs.schedule(...), because for every job, JobManager registers a whenDone handler as well
-    //         in order to unregister the associated Quartz Trigger. Otherwise, that could cause an infinite recursion.
     if (!m_handlers.isEmpty()) {
       m_executor.execute(new Runnable() {
 
diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/JobFutureTask.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/JobFutureTask.java
index 5c03ab8..e5abebb 100644
--- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/JobFutureTask.java
+++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/job/internal/JobFutureTask.java
@@ -23,23 +23,29 @@
 import java.util.concurrent.RunnableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.eclipse.scout.rt.platform.BEANS;
 import org.eclipse.scout.rt.platform.annotations.Internal;
 import org.eclipse.scout.rt.platform.chain.callable.CallableChain;
 import org.eclipse.scout.rt.platform.chain.callable.ICallableDecorator;
 import org.eclipse.scout.rt.platform.context.RunContext;
+import org.eclipse.scout.rt.platform.context.RunContexts;
 import org.eclipse.scout.rt.platform.context.RunMonitor;
+import org.eclipse.scout.rt.platform.exception.DefaultExceptionTranslator;
 import org.eclipse.scout.rt.platform.exception.DefaultRuntimeExceptionTranslator;
 import org.eclipse.scout.rt.platform.exception.IExceptionTranslator;
+import org.eclipse.scout.rt.platform.exception.NullExceptionTranslator;
 import org.eclipse.scout.rt.platform.exception.PlatformException;
 import org.eclipse.scout.rt.platform.filter.IFilter;
+import org.eclipse.scout.rt.platform.job.DoneEvent;
 import org.eclipse.scout.rt.platform.job.ExecutionTrigger;
 import org.eclipse.scout.rt.platform.job.IDoneHandler;
 import org.eclipse.scout.rt.platform.job.IExecutionSemaphore;
 import org.eclipse.scout.rt.platform.job.IFuture;
 import org.eclipse.scout.rt.platform.job.JobInput;
 import org.eclipse.scout.rt.platform.job.JobState;
+import org.eclipse.scout.rt.platform.job.Jobs;
 import org.eclipse.scout.rt.platform.job.listener.IJobListener;
 import org.eclipse.scout.rt.platform.job.listener.JobEvent;
 import org.eclipse.scout.rt.platform.job.listener.JobEventData;
@@ -47,6 +53,7 @@
 import org.eclipse.scout.rt.platform.util.Assertions;
 import org.eclipse.scout.rt.platform.util.IRegistrationHandle;
 import org.eclipse.scout.rt.platform.util.ToStringBuilder;
+import org.eclipse.scout.rt.platform.util.concurrent.IBiFunction;
 import org.quartz.Calendar;
 import org.quartz.SchedulerException;
 import org.quartz.Trigger;
@@ -430,6 +437,71 @@
   }
 
   @Override
+  public <FUNCTION_RESULT> IFuture<FUNCTION_RESULT> whenDoneSchedule(final IBiFunction<RESULT, Throwable, FUNCTION_RESULT> function, final JobInput input) {
+    Assertions.assertNotNull(input, "Input must not be null");
+    Assertions.assertNotNull(function, "Function must not be null");
+
+    final IFuture<RESULT> currentFuture = this;
+
+    // Create an execution guard to allocate a worker thread only after entering done state of this future.
+    final IExecutionSemaphore executionGuard = Jobs.newExecutionSemaphore(0);
+    whenDone(new IDoneHandler<RESULT>() {
+
+      @Override
+      public void onDone(final DoneEvent<RESULT> event) {
+        executionGuard.withPermits(1);
+      }
+    }, null);
+
+    // Upon cancellation of this future, cancel the function's future as well.
+    final RunMonitor functionRunMonitor = BEANS.get(RunMonitor.class);
+    m_runMonitor.registerCancellable(functionRunMonitor);
+
+    // Upon cancellation of the function's RunMonitor, cancel the function's future as well (to reflect proper cancellation status).
+    if (input.getRunContext() != null) {
+      input.getRunContext().getRunMonitor().registerCancellable(functionRunMonitor);
+    }
+
+    // Schedule the job to be executed upon this future enters done state.
+    // However, the execution guard ensures that a worker thread is only allocated upon this future enters done state.
+    return Jobs.schedule(new Callable<FUNCTION_RESULT>() {
+
+      @Override
+      public FUNCTION_RESULT call() throws Exception {
+        // Sanity check for cancellation because cancellation may not be percolated yet.
+        if (currentFuture.isCancelled()) {
+          IFuture.CURRENT.get().cancel(false);
+          return null;
+        }
+
+        // At this point, the depending future finished without being cancelled.
+        final AtomicReference<RESULT> rRef = new AtomicReference<>();
+        final AtomicReference<Throwable> tRef = new AtomicReference<>();
+        try {
+          rRef.set(currentFuture.awaitDoneAndGet(NullExceptionTranslator.class));
+        }
+        catch (final Throwable t) { // NOSONAR
+          tRef.set(BEANS.get(DefaultExceptionTranslator.class).unwrap(t));
+        }
+
+        // Schedule the job to run the function in the proper context (RunContext, ExecutionSemaphore, ExecutionHints, ...).
+        // However, if the function's RunContext is cancelled, it will not be executed.
+        return Jobs.schedule(new Callable<FUNCTION_RESULT>() {
+
+          @Override
+          public FUNCTION_RESULT call() throws Exception {
+            return function.apply(rRef.get(), tRef.get());
+          }
+        }, input).awaitDoneAndGet(DefaultExceptionTranslator.class);
+      }
+    }, Jobs.newInput()
+        .withName("[jobmanager] Waiting for 'whenDoneFunction' to execute [job={}, function={}, functionJobName={}]", getJobInput().getName(), function, input.getName())
+        .withExceptionHandling(null, false) // propagate a potential exception without handling it
+        .withRunContext(RunContexts.empty().withRunMonitor(functionRunMonitor))
+        .withExecutionSemaphore(executionGuard));
+  }
+
+  @Override
   public IRegistrationHandle addListener(final IJobListener listener) {
     return addListener(null, listener);
   }
@@ -627,7 +699,7 @@
    * Creates the Quartz Trigger to fire execution.
    */
   protected OperableTrigger createQuartzTrigger(final JobInput input) {
-    TriggerBuilder<Trigger> builder = TriggerBuilder.newTrigger()
+    final TriggerBuilder<Trigger> builder = TriggerBuilder.newTrigger()
         .forJob(JobFutureTask.class.getSimpleName());
 
     final ExecutionTrigger executionTrigger = input.getExecutionTrigger();
diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/util/concurrent/IBiFunction.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/util/concurrent/IBiFunction.java
new file mode 100644
index 0000000..fc21db2
--- /dev/null
+++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/util/concurrent/IBiFunction.java
@@ -0,0 +1,23 @@
+package org.eclipse.scout.rt.platform.util.concurrent;
+
+/**
+ * Represents a function that accepts two arguments and produces a result. This is the two-arity specialization of
+ * <code>Function</code>.
+ * <p>
+ * This is a surrogate for <code>BiFunction</code> contained in Java 8, and is to conform with Java 7 compliance.
+ *
+ * @param <T>
+ *          the type of the first argument to the function
+ * @param <U>
+ *          the type of the second argument to the function
+ * @param <R>
+ *          the type of the result of the function
+ */
+public interface IBiFunction<T, U, R> {
+
+
+  /**
+   * Applies this function to the given argument.
+   */
+  R apply(T t, U u);
+}