DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
11 Monitoring and Observability Tools for 2023
Learn more
  1. DZone
  2. Data Engineering
  3. Databases
  4. Sticky Lookup

Sticky Lookup

Ernest Lotter user avatar by
Ernest Lotter
·
Sep. 03, 10 · Interview
Like (0)
Save
Tweet
Share
10.36K Views

Join the DZone community and get the full member experience.

Join For Free

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.
application Relational database Dependency NetBeans Object (computer science) Advantage (cryptography) master Identifier dev

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Apache Kafka Is NOT Real Real-Time Data Streaming!
  • Reliability Is Slowing You Down
  • 7 Most Sought-After Front-End Frameworks for Web Developers
  • gRPC on the Client Side

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: