В этой части (которая следует из этой части ) мы добавляем другое представление в наше приложение, которое в конечном итоге будет редактором для выбранного клиента в древовидном представлении. На данный момент он просто отобразит значения выбранного клиента:
Перед тем, как перейти к инструкциям по созданию всего, что вы видите выше, стоит обратить внимание на две интересные вещи:
- Компонент Editor находится в другом модуле, чем компонент Viewer. Посмотрите на скриншот ниже, и вы увидите, что к концу этой статьи приложение будет иметь один дополнительный модуль:
Модуль «Viewer-UI» уже содержит древовидную иерархию, которую вы видите в левом верхнем углу скриншота, а компонент редактора (который охватывает основную область скриншота выше) будет создан в отдельном модуле, который вы создадите. в этой статье называется «Editor-UI». Несмотря на то, что они находятся в отдельных модулях, эти два компонента, то есть древовидная иерархия и компонент редактора, синхронизированы, так что выбор элемента в древовидной иерархии приводит к тому, что те же данные отображаются в компоненте редактора.
- Пункт меню Edit находится в отдельном модуле для узла Customer. Посмотрите на первый скриншот выше и особенно на два пункта меню в древовидной иерархии. Один из этих пунктов меню называется «Редактировать». Этот пункт меню происходит из модуля «Editor-UI», а древовидная иерархия находится в модуле «Viewer-UI». Это хорошая новость, поскольку редактор теперь полностью заменяется другим редактором, который имеет собственный пункт меню «Редактировать», который он сможет зарегистрировать на узле «Клиент», вне модуля «Viewer-UI». Следовательно, редактор полностью заменяем, т.е. не только сам редактор, но и его поддерживающий пункт контекстного меню.
Как реализованы обе вышеуказанные гибкие функции — тема этой статьи. В общих чертах, тема этой статьи — «модульность», которая, если учесть два вышеизложенных момента, полезна для приложения, особенно когда оно становится больше и вы хотите предоставить своим пользователям больше гибкости. Может, им не нужен этот редактор, может, им нужен другой редактор. Возможно, сторонний поставщик хочет предоставить свой супер-умный редактор и продать его пользователям вашего приложения. Все это и многое другое возможно благодаря модульности.
Некоторые аспекты модульности уже должны быть понятны из более ранних частей, поскольку сейчас у нас уже есть 4 модуля, что означает, что мы можем легко заменить одного поставщика базы данных или поставщика постоянства другим, просто удалив модуль и добавив альтернативу, которая делает что-то похожее, но другой. Теперь мы рассмотрим этот вопрос модульности на уровне пользовательского интерфейса при создании приложения Viewer / Editor, показанного выше. В следующей теме мы рассмотрим подходы к тому, как сохранить изменения от редактора до базы данных. Сейчас мы сосредоточимся только на создании пользовательского интерфейса.
Давайте начнем.
- Создать новый модуль. Как указано в приведенных выше аргументах, имеет смысл создать отдельный модуль для нашего редактора, для редактирования нашей сущности Customer. При этом мы даем возможность изолировать все функциональные возможности редактора в определенном модуле, что делает его доступным для обмена с другим модулем, обеспечивающим сопоставимые функции. Итак, как и раньше, щелкните правой кнопкой мыши узел Modules, который вы найдете в окне Projects для приложения, а затем создайте новый модуль с именем «Editor-UI».
- Создайте компонент редактора. Как и прежде, создайте новый компонент Window (т. Е. Класс TopComponent) с помощью мастера New Window Component, используя «Editor» в качестве префикса имени класса, а затем спроектируйте его, как показано на первом снимке экрана: вам понадобятся три JLabels и три поля JTextFields. Вот где текущие значения сущности Customer будут отображены после выбора пункта меню в древовидной иерархии.
- Синхронизируйте древовидную иерархию с компонентом редактора. Здесь начинается магия! Несмотря на то, что древовидная иерархия находится в отдельном модуле, в котором находится компонент редактора, нам необходимо синхронизировать их. Загляните внутрь класса ChildFactory, и вы заметите, что конструктор вашего CustomerNode определен так:
private class CustomerNode extends AbstractNode {
private Customer customer;
private CustomerNode(Customer customer) {
super(Children.LEAF, Lookups.singleton(customer));
this.customer = customer;
setDisplayName(customer.getName());
setIconBaseWithExtension(IMAGE_NODE_BASE);
}
...
...
...Посмотри в звонке в супер класс. Вот где вы видите … что текущий объект Customer добавлен в Lookup. Что это значит простыми словами? Проще говоря, это означает, что текущий объект Customer был добавлен в контекст приложения. Другими словами, объект «Клиент» теперь доступен ВНЕ модуля, в котором находится клиент, ТАКЖЕ за модулем, в котором находится CustomerNode (определенный выше). Прямо сейчас, когда создается CustomerNode, приложение ВСЕ (т. Е. ВСЕ модули в приложении) может получить доступ к этому конкретному экземпляру объекта Customer!
Теперь представьте, если бы у нас был Слушатель в этом контексте? Тогда мы сможем выслушать контекст и определить, что доступен новый экземпляр Customer (что происходит всякий раз, когда создается новый CustomerNode), а затем ДЕЛАТЬ что-то с этим Customer! Это то, что мы будем делать на следующем шаге.
- Слушайте Поиск. Таким образом, всякий раз, когда новый Клиент вводится в контекст, мы хотим, чтобы компонент Editor отображал данные Клиента в TopComponent. Измените подпись класса редактора следующим образом:
public final class EditorTopComponent extends TopComponent implements LookupListener
Now we have a LookupListener on the Editor component, which will listen to new objects being introduced into the application’s context (which will happen, as stated above, whenever a new node is created).
You will find that, because you have now implemented «LookupListener», you will need to define a method called «resultChanged». Here it is for our scenario:
@Override
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection c = r.allInstances();
if (!c.isEmpty()) {
for (Iterator i = c.iterator(); i.hasNext();) {
Customer o = (Customer) i.next();
nameField.setText(o.getName());
cityField.setText(o.getCity());
stateField.setText(o.getState());
}
} else {
nameField.setText("[no name]");
cityField.setText("[no city]");
stateField.setText("[no state]");
}
}As you can see, we populate the three fields in our Editor component based on what is found in the current Customer entity, which is found in the context of our application. And it is found in the context of our application because our CustomerNode puts it there whenever it is created.
The Editor component’s «componentOpened» and «componentClosed» methods need to be redefined to make the above possible, as follows:
private Lookup.Result result = null;
@Override
public void componentOpened() {
Lookup.Template tpl = new Lookup.Template(Customer.class);
result = Utilities.actionsGlobalContext().lookup(tpl);
result.addLookupListener(this);
}
@Override
public void componentClosed() {
result.removeLookupListener(this);
result = null;
}So, when the Editor component opens, we look for the Customer class. Then we add it to a Result object, which is the Lookup object to which we can listen. Then, back in our «resultChanged», whenever that change takes place, we update the JTextFields. Finally, we detach the listener when the Editor component closes.
In fact, this is identical to how Tim Boudreau describes editor/viewer interaction in the first part of his 4-part series on Selection Management. Try that tutorial to get used to this NetBeans Platform idiom.Seriously, try it. If you are a Swing developer, that tutorial should change your life significantly (for the better).
- Attach the «Edit» Menu Item to the Customer Node. Now that we have the Editor component synchronized with the Viewer component, how do we enable the Editor component to open the current content of the Viewer component? First, remember that when we created the Editor window, via the New Window Component wizard, a new menu item was registered in the layer.xml file. Change that registration to the following:
<folder name="Edit">
<folder name="Customer">
<file name="EditorAction.shadow">
<attr name="originalFile" stringvalue="Actions/Window/org-editor-ui-OpenEditorAction.instance"/>
</file>
</folder>
</folder>And then create the action itself, like this, in a package structure that matches the string shown above in the layer.xml file:
public final class OpenEditorAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
public void run() {
EditorTopComponent editor = new EditorTopComponent();
editor.open();
editor.requestActive();
}
});
}
}The above folder structure is unique. The NetBeans Platform, by default, will do nothing at all with a folder structure of «Edit/Customer». Therefore, by default, our menu item will not appear anywhere at all. It will not be displayed so it will never be selected.
Except… if we make the display of this menu item possible. The layer.xml file where the above is defined is one small contribution to a potentially massive filesystem, shared by all the modules in the application. So, in our «Viewer-UI» module, we can LOAD the menu item defined above. Let’s do so now. Open the «Viewer-UI» module and create a new class called «ActionBuilderAction», with all of this content:
public class ActionBuilderAction extends AbstractAction implements Presenter.Popup {
public void actionPerformed(ActionEvent e) {
//Nothing needs to happen here.
}
public JMenuItem getPopupPresenter() {
String folderName = "Edit/Customer";
FileObject folder = Repository.getDefault().getDefaultFileSystem().findResource(folderName);
JMenuItem item = buildEditMenuItem(folder);
return item;
}
private JMenuItem buildEditMenuItem(FileObject folder) {
DataFolder df = DataFolder.findFolder(folder);
DataObject[] folderKids = df.getChildren();
Object instanceObj;
DataObject dob = folderKids[0];
InstanceCookie ck = (InstanceCookie) dob.getCookie(InstanceCookie.class);
try {
instanceObj = ck.instanceCreate();
} catch (Exception ex) {
instanceObj = null;
}
if (instanceObj instanceof Action) {
JMenuItem plainMenuItem = new JMenuItem("Edit");
Actions.connect(plainMenuItem, (Action) instanceObj, true);
return plainMenuItem;
}
return null;
}
}What’s going on here?! Well, look closely at the code. (And then read this interview with Tonny Kohar.) We’re reading the «Edit/Customer» folder in the shared filesystem and then constructing (thanks to InstanceCookie) our menu item from that. In «Edit/Customer», we will find the registration of our menu item, defined within the separate «Editor-UI» module.
Therefore, if there were to be no «Editor-UI» module, but a «Foo» module instead, so long as it had a menu item in its own «Edit/Customer» folder, the menu item would be attached to the CustomerNode, thanks to the layer.xml entries that you see above. And if it had a component listening for the Customer entity, it would be able to do something (edit, probably, but not exclusively, since each module can define for itself what it does with what it listens to) with the Customer entity located in the application’s context.
Finally, we need to bind our special action to the Customer node, together with the Properties menu item that you saw in the first screenshot. Here’s how to do that, within the definition of the CustomerNode:
@Override
public Action[] getActions(boolean context) {
return new Action[]{
new ActionBuilderAction(),
SystemAction.get(PropertiesAction.class),
};
}You can see above that the Properties action is one of the default actions provided by the NetBeans Platform. You’ll need a dependency on the Actions API to use it. Several other similar default actions are available, which we will use in this series of articles as we need to make use of them. Let’s make the Properties action the default action, i.e., the action that will be invoked when a node is double-clicked by the user:
@Override
public Action getPreferredAction() {
return SystemAction.get(PropertiesAction.class);
}
And that’s NetBeans «Modularity» in a nutshell. If you understand the above, and can implement it within an application similar to the one described above, you can indeed call yourself a NetBeans Platform developer. Many avenues are then open to you because you are then able to create loosely coupled components that interact with each other independently. And, soon, you’ll even be able to integrate these components with OSGI bundles, but that’s another story, though watch this space for details.
In the next part, we’ll focus on the Editor component and on ways in which it can store its data back into the database.