Ernest has posted 2 posts at DZone. View Full User Profile

Sticky Lookup

09.03.2010
| 8459 views |
  • submit to reddit

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.
Published at DZone with permission of its author, Ernest Lotter.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Fabrizio Giudici replied on Fri, 2010/09/03 - 2:07pm

Did you try CentralLookup? http://wadechandler.blogspot.com/2007/12/central-lookup-creating-central.html

In the past I've experienced similar "changed-focus-will-scrath-objects" effecs with the Utilities.actionGlobalContext() and, as you said, I didn't want to depend on a TopComponent (bad dependencies: the model shouldn't depend on a View). It turned out that the Central Lookup (wrapped by an EventBus, but this is a matter of taste...) was ok for me, as in the end is an independent model which doesn't have any interaction with the Platform (so, no unexpected effects).

Ernest Lotter replied on Fri, 2010/09/03 - 4:18pm in response to: Fabrizio Giudici

Hi Fabrizio - yes I did try CentralLookup - but the major thing that bothered me is that it would be (at least for this use case) overkill - as it makes it possible for the module doing the order basket updates to explicitly modify the contents of CentralLookup (and for possibly other, evil modules, to interfere in this process!). Using the sticky approach, we've got a purely local Lookup not exposed to anyone else, and don't unnecessarily allow .add() - because it is not needed. I do agree however that there are other use cases where CentralLookup is useful and applicable indeed, this just shows that for this simple selection/focus issue a more localized (and safer) solution is possible.

Toni Epple replied on Sat, 2010/09/04 - 1:46am

Hi Ernest,

I see the point of having a local solution to this. Alternatively you might use the NetBeans SPI for replacing the Utilities.actionsGlobalContext():

http://weblogs.java.net/blog/timboudreau/archive/2007/01/how_to_replace.html

You could use your StickyLookup there or create a simple extension point to mark some TopComponents as sticky, then keep their lookup in focus (and only remove them, when the TopComponents isn't displayed any more). The benefit would be that it changes selection not only for one TopComponent, but generally. So you can develop more TopComponents in the future. In Addition you wouldn't have the drawback of a CentralLookup that everyone can manipulate.

 

 

Ernest Lotter replied on Sat, 2010/09/04 - 6:33am in response to: Toni Epple

Thanks Toni, using the SPI for replacing Utilities.actionsGlobalContext() or an extension point for marking specific TopComponents (and/or certain classes) as sticky sounds like an interesting idea, I'll certainly play with this and see what's possible. Could replacing be a problem for other modules that expect default behaviour from Utilities.actionsGlobalContext() - thus other, unknown modules that actually want the (for example) BusAccts to disappear when they are not selected any more?

Giovanni Dal Maso replied on Tue, 2010/09/07 - 3:01am

We had similar problem but the sticky solution didn’t work for us. What happens when you close the master view? How is the details view notified of an empty selection?

Toni Epple replied on Sat, 2010/09/18 - 3:52pm

Hi Ernest,

 yes it definitely would influence other parts of the application if they listen for the same type of objects. If it is a requirement to keep those separate you could create your own extensionpoint, where you could register windows to be sticky, track it's visibility via windowmanager and an API to retrieve this second ProxyLookup...

 

 

 

Hugo Heden replied on Wed, 2011/01/12 - 6:33am

Hmm, did my comment from before christmas end up in the spam filter? (I put a simpler version of StickyLookup on my blog. But I don't suppose I can link to it from here..? Aargh, those spam filters)

Ernest Lotter replied on Mon, 2011/04/04 - 2:47am in response to: Hugo Heden

I would indeed recommend that people rather use Hugo's implementation - much more compact and readable. It seems that URLs can't be posted here, but a simple web search for Hugo's blog would direct those interested to the better version.

Wade Chandler replied on Sun, 2011/04/10 - 12:00am

Sorry guys. I just now saw this. Just a tad late ;-)

In this particular case, I would probably have added BusAcct to the OrderBasketNode Lookup since OrderBasketNodeChildFactory already knows about the currently selected BusAcct, and then this would have handled the selection problem as the global selection would have that in it then. Granted, that is just glancing at the logic and not having a complete understanding of the rest of the application.

As far as Central Lookup and writability etc, if it is a contained application it doesn't matter as much to me as much as being able to really get the work done with a common universal API. In the context of an RCP the scope is generally limited, and too, the only modules running within that scope are ones I know about. Even then, the global context can be changed in mysterious ways if one really wanted to do something sinister in an open context such as the IDE.

I mean, all one really has to do is wrap the global context and proxy to particular Lookups they have studied during debug and inject mockups of whatever interface is being implemented overriding anything they feel they must. They could even do that using reflection and byte code manipulation. To get an exact class they can simply use the system class loader were that really needed (i.e. package protected and private etc wouldn't help much at that point).

Central Lookup allows for a fairly decent method of making it just as difficult for someone to hijack your module while remaining central. For instance, you can have a shared API which only particular modules you care about can access through friends; in this case talking about classes used in the Lookup. Friends has the same issue with reflection as anything...it can be gotten around with some tedious coding.

Using StickyLookup, unless you only want to limit what you are doing in some logic in its entirety to a single module, you would have to use friends to be able to have more protected code yet access those things from separate modules. But, even in the context of a single module, one can still track the global selection through ExplorerManagers etc.

Too, if someone can get their module in a running application, it doesn't matter. They can do anything at that point including getting running code some where on the system. Thus, the only real issue with security in this regard is code/logic protection versus evil modules doing something necessarily sinister. Code/logic meaning someone melding a module to do some extra tricks and possibly cause state errors etc by accident. This per the RCPs design.

Matt Coleman replied on Fri, 2012/11/16 - 12:59am

as a sell sheet designer  and using NB for a while i find that Sticky lookups are not as simple as i thought 

Cata Nic replied on Mon, 2013/09/02 - 3:50pm

 The Lookup capability has improved  my company's effectiveness with about 30 percents. Thank you for this solution.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.