Статьи

Липкий поиск

После завершения сертифицированного тренинга по платформе NetBeans в Stellenbosch, проведенного Геерджаном , мы провели много приятных дискуссий и получили очень оптимистичный и воодушевленный взгляд на то, как платформа NetBeans улучшит текущую практику и продукты, которые мы производим на ISS International .

Одна интересная тема для обсуждения чувствительности к контексту для приложения с подробным представлением возникла при портировании приложения, не являющегося платформой NetBeans, на платформу NetBeans. Данное приложение по умолчанию разделено по вертикали на две основные области: верхняя часть показывает список бизнес-счетов , а нижняя часть показывает список корзин заказов, соответствующих текущим выбранным бизнес-счетам. И между бизнес-счетами и корзинами заказов существует связь один-ко-многим .

Оба задействованных компонента TopComponents имеют встроенные OutlineView, независимые ExplorerManager и предоставляют свои выбранные в данный момент узлы (BusAcctNode и OrderBasketNode соответственно). В частности, наша первая итерация ExplorerManager в OrderBasketTopComponent имела следующее:


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

в своем конструкторе, где OrderBasketNodeChildFactory был настроен следующим образом:


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);
}
}

При использовании приложения наблюдалось некоторое неправильное поведение — при выборе новых бизнес-аккаунтов в списке бизнес-аккаунтов LookupListener, установленный OrderBasketNodeChildFactory (сам по себе), корректно обновлял свои дочерние элементы (при изменении выбранных BusAccts) и список соответствующих OrderBaskets. были показаны в списке корзин заказа ниже.

Однако при последующем выборе одной из этих корзин заказа вместо ожидаемой (а) это привело к (б)!

(а) Выбор корзины заказа (b) Список корзин заказов исчезает при выборе

 

Конечно, это имеет смысл — выбрав корзину заказов ниже, у нас больше не было BusAcct в Utilities.actionsGlobalContext (), но вместо этого один или несколько OrderBaskets, и, таким образом, OrderBasketNodeChildFactory вел себя как ожидалось.

Как это исправить? Наша первая итерация состояла в том, чтобы вместо прослушивания Utilities.actionsGlobalContext () вместо этого прослушивать Lookup конкретного рассматриваемого TopComponent (который в данном случае был BusinessAccountTopComponent). Для этого мы заменим


Lookup lookup = Utilities.actionsGlobalContext();

в OrderBasketNodeChildFactory с


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

Это решило проблему выбора, так как OrderBasketChildFactory теперь всегда прослушивал BusinessAccountTopComponent для BusAccts, независимо от того, на чем фокусировался TopComponent.

Однако, хотя наши BusinessAccountTopComponent и OrderBasketTopComponent находились в разных модулях без зависимостей друг от друга (только общие зависимости на модуле общего домена, содержащем BusAcct, OrderBasket, …), это казалось непростым решением, поскольку теперь мы зависели от имя TopComponent в качестве идентификатора, который TopComponent должен прослушивать (который может измениться в зависимости от прихотей разработчика, поддерживающего этот модуль).

Кроме того, мы хотели бы иметь возможность добавления нового модуля основного вида, который мог бы беспрепятственно использовать существующий подробный вид, не требуя изменений или изменений API.

Возможная стратегия заключается в следующем, что позволяет нам продолжать работать с Utilities.actionsGlobalContext () и избегать прослушивания определенного TopComponent (который может быть или не быть в первую очередь). Общий эскиз идеи заключается в том, чтобы обернуть Utilities.actionsGlobalContext () в StickyLookup — поиск, который делает объекты указанного класса «липкими». В то время как содержимое определенного Lookup может меняться со временем, когда объекты определенных типов перемещаются внутрь и из него, липкий класс (S) StickyLookup обеспечивает выполнение .lookup (S.class) или .lookupAll (S.class ) всегда будет приводить к последнему непустому набору возвращенных экземпляров в течение его времени жизни.

В частности, для нашего варианта использования это гарантирует, что последние экземпляры BusAcct, которые были видимы в Utilities.actionsGlobalContext (), останутся видимыми, а изменение фокуса, которое удалит их из Utilities.actionsGlobalContext (), не удалит их из переноса. StickyLookup.

Без дальнейших церемоний, первая итерация 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);
}
}
}
}
}

Используя это, мы теперь можем отбросить нашу зависимость от любого конкретного TopComponent, который предоставляет BusAccts, но лучше изменить наш исходный


Lookup lookup = Utilities.actionsGlobalContext();

к компакту


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

что дает желаемый эффект, с дополнительным преимуществом, заключающимся в том, что мы не вводим «Уточняющий запрос», который вообще может быть доступен для записи другими, что, возможно, является недостатком «
Центрального поиска » Тима Будро, Уэйда Чандлера и Фабрицио Джудичи.