Platinum Partner
netbeans,lookup context selection sticky

Sticky Lookup

With the Certified NetBeans Platform Training in Stellenbosch by Geertjan drawing to a close, we have had many enjoyable discussions and ended up with a very optimistic and excited outlook on how the NetBeans Platform will improve current practises and the products we produce at ISS International.

One interesting point of discussion about context sensitivity for a master-detail view application came up while porting a non-NetBeans Platform application to the NetBeans Platform. The application in question is by default vertically split into two main areas, the top part showing a list of business accounts, and the bottom part showing a list of order baskets corresponding to the currently selected business accounts. And there is a one-to-many relationship between business accounts and order baskets.

Both of the involved TopComponents have embedded OutlineViews, independent ExplorerManagers, and expose their currently selected nodes (BusAcctNode and OrderBasketNode, respectively). In particular, our first iteration of the ExplorerManager in OrderBasketTopComponent had the following:


this.em = new ExplorerManager();
this.em.setRootContext(new AbstractNode(Children.create(new OrderBasketNodeChildFactory(), true)));

in its constructor, where OrderBasketNodeChildFactory was set up as follows:

public class OrderBasketNodeChildFactory extends ChildFactory<OrderBasket> implements LookupListener {

private final Lookup.Result<BusAcct> busAcctResult;

public OrderBasketNodeChildFactory() {
Lookup lookup = Utilities.actionsGlobalContext();

this.busAcctResult = lookup.lookupResult(BusAcct.class);
this.busAcctResult.addLookupListener(this);
resultChanged(new LookupEvent(busAcctResult));
}

@Override
protected boolean createKeys(List<OrderBasket> toPopulate) {
for (BusAcct businessAccount : busAcctResult.allInstances()) {
toPopulate.addAll(businessAccount.getOrderBasketList());
}
return true;
}

@Override
protected Node createNodeForKey(OrderBasket key) {
try {
return new OrderBasketNode(key);
} catch (IntrospectionException ex) {
Exceptions.printStackTrace(ex);
return null;
}
}

@Override
public void resultChanged(LookupEvent ev) {
refresh(true);
}
}

Upon using the application, some misbehaviour was experienced -- when selecting new business accounts in the list of business accounts, the LookupListener installed by OrderBasketNodeChildFactory (itself) correctly refreshed its children (as the selected BusAccts changed), and a list of the corresponding OrderBaskets were shown in the list of order baskets below.

However, when subsequently selecting one of these order baskets, instead of the expected (a), this resulted in (b)!

(a) Selection of order basket (b) List of order baskets disappear upon selection

 

Of course, this makes perfect sense -- by selecting the order basket below, we no longer had a BusAcct in Utilities.actionsGlobalContext(), but one or more OrderBaskets instead, and thus OrderBasketNodeChildFactory behaved as expected.

How to remedy this? Our first iteration of this was by instead of listening to Utilities.actionsGlobalContext(), rather listen to the Lookup of the specific TopComponent in question (which in this case, was BusinessAccountTopComponent). To do this, we replace


Lookup lookup = Utilities.actionsGlobalContext();

in OrderBasketNodeChildFactory with

Lookup lookup = WindowManager.getDefault().findTopComponent("BusinessAccountTopComponent").getLookup();

This solved the selection problem, as the OrderBasketChildFactory was now always listening on the BusinessAccountTopComponent for BusAccts, irrespective of what which TopComponent had focus.

However, even though our BusinessAccountTopComponent and OrderBasketTopComponent were in different modules without dependencies on each other (only shared dependencies on a common domain module containing BusAcct, OrderBasket, ...), this felt like an uneasy solution, since we were now depending on the name of the TopComponent as an identifier of which TopComponent to listen on (which could potentially change, depending on the whims of the developer maintaining that module).

Also, we would want to have the possibility of a new master view module being added, that could seamlessly use the existing detail view without requiring modifications or API changes.

A possible strategy is the following, which enables us to continue working with Utilities.actionsGlobalContext(), and away from listening to a specific TopComponent (which may or may not be there in the first place). The general sketch of the idea is to wrap Utilities.actionsGlobalContext() in a StickyLookup -- a lookup which makes objects of a specified class "sticky". While the contents of a specific Lookup may change over time as objects of certain types move in and out of it, the sticky class (S) of a StickyLookup ensures that performing a .lookup(S.class) or .lookupAll(S.class) will always result in the last non-empty set of returned instances during its lifetime.

Specifically, for our use case, this will ensure that the last instances of BusAcct that were visible in Utilities.actionsGlobalContext() would remain visible, and a focus change that would remove them from Utilities.actionsGlobalContext() would not remove them from the wrapping StickyLookup.

Without further ado, a first iteration of StickyLookup:


/**
* @author ernest
*/
public class StickyLookup extends ProxyLookup implements LookupListener {
private final Lookup lookup;
private final Class clazz;
private final Lookup.Result result;
private final InstanceContent ic;
private final Set icContent = new HashSet();

public StickyLookup(final Lookup lookup, final Class<?> clazz) {
this(lookup, clazz, new InstanceContent());
}

private StickyLookup(final Lookup lookup, final Class<?> clazz, InstanceContent ic) {
super(Lookups.exclude(lookup, clazz), new AbstractLookup(ic));
this.lookup = lookup;
this.clazz = clazz;
this.ic = ic;

// initialize (pull this from wrapped lookup)
for (Object t : lookup.lookupAll(clazz)) {
ic.add(t);
icContent.add(t);
}

this.result = lookup.lookupResult(clazz);
this.result.addLookupListener(this);
}

@Override
public void resultChanged(LookupEvent ev) {
boolean empty = true;
if (lookup.lookup(clazz) != null) {
empty = false;
}
if (empty) {
for (Object obj : icContent) {
ic.add(obj); // add 'em!
}
return; // don't force refresh at all, as nothing of type clazz is selected and we should therefore preserve what we have
} else {
// not empty, reset contents
Collection<?> lookupAll = lookup.lookupAll(clazz);
List<Object> toRemove = new ArrayList<Object>();
for (Object obj : icContent) {
if (lookupAll.contains(obj)) {
continue;
}
ic.remove(obj);
toRemove.add(obj);
}
for (Object obj : toRemove) {
icContent.remove(obj);
}
for (Object obj : lookupAll) {
if (!icContent.contains(obj)) {
ic.add(obj);
icContent.add(obj);
}
}
}
}
}

Using this, we can now throw away our dependency on any specific TopComponent that provide BusAccts, but rather change our original

Lookup lookup = Utilities.actionsGlobalContext();

to the compact

Lookup lookup = new StickyLookup(Utilities.actionsGlobalContext(), BusAcct.class);

which has the desired effect, with the added advantage that we are not introducing a Lookup that is writable by others at all, which is maybe a downside of the Central Lookup by Tim Boudreau, Wade Chandler, and Fabrizio Giudici.
{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}