Статьи

Слабосвязанные Creatable возможности для приложений CRUD

В первой части вы познакомились с концепцией слабосвязанных перезагружаемых возможностей, основанных на работе, проделанной Антонио на его кухне платформы NetBeans . Расширение этой концепции было показано во второй части , где было представлено SaveableEntityCapability, которое является дополнением к NetBeans SaveCookie, так же, как ReloadableEntityCapability сопровождает ReloadableViewCapability в первой части.

Сегодня давайте посмотрим, как «C» в CRUD (т. Е. «Создавать») можно обрабатывать с помощью «подхода подхода». В нашем приложении мы получим возможность «Создать», которая будет доступна в зависимости от текущего выбора. На первом снимке экрана вы видите выбранный корневой узел, следовательно, действие New включено и может быть вызвано с панели инструментов («New» обозначается желтым значком слева на панели инструментов ниже), в то время как Delete и Сохранить отключены, потому что при выборе корневого узла нет ничего, что имеет смысл удалять или сохранять:

Примечание. Как видите, я сейчас использую базу данных, отличную от вчерашней. Вместо EclipseLink, Derby и образца базы данных NetBeans Derby я теперь использую EclipseLink, MySQL и таблицу Trip из базы данных Sakila . Причина такого изменения заключается в том, что при удалении записей я столкнулся с рядом проблем на уровне JPA с базой данных примера Derby NetBeans, что я хотел бы решить когда-нибудь. Однако цель этой серии статей — узнать о том, как использовать возможности в приложениях платформы NetBeans, что является отдельной темой для JPA, поэтому я оставлю эти подробности в другой раз.

На следующем снимке экрана вы можете видеть выбранный дочерний узел, что означает, что в дополнение к тому, что новое действие остается включенным (пользователи будут ожидать создания новых поездок в любом месте приложения, независимо от выбора), теперь также включено действие Удалить, потому что каждый Поездка может быть удалена, либо по отдельности, либо с помощью множественного выбора:

Наконец, на снимке экрана ниже выбран компонент редактора, и теперь действие «Удалить» отключено (поскольку это действие имеет значение только при выборе дочернего узла), а действие «Новое» снова включено (поскольку снова пользователь будет ожидать создания новых командировок независимо от текущего выбора), в то время как кнопка «Сохранить» теперь включена, так как курсор находится в текстовом поле (фактическое изменение еще не было сделано, но здесь я предполагаю, что Действие сохранения должно быть доступно всякий раз, когда курсор находится в текстовом поле, независимо от изменений, которые были фактически сделаны):

Итак, основой вышеупомянутого контекстно-зависимого включения, основанного на выборе, является набор «возможностей», которые вы можете увидеть здесь, в текущем состоянии приложения:

Выше единственной новой возможностью является «CreatableEntityCapability» (и «RemoveableEntityCapability», с которой мы будем иметь дело в следующий раз), которая, как и другие XXXEntityCapabilities, работает с моделью. С точки зрения, нам не нужно писать «CreatableViewCapability», точно так же, как нам не нужно было создавать «SaveableViewCapability», потому что платформа NetBeans обеспечивает встроенную поддержку для этой возможности представления. Другими словами, так же, как мы подключили наши изменения представлений для сохранения в реализации SaveCookie вчера, мы теперь подключимся к классу NewType сегодня.

Процесс наращивания наших возможностей такой же, как и раньше. Мы начинаем с добавления метода «create» в наш DAO, определяем новую возможность для создания сущностей, затем добавляем реализацию этой новой возможности в Lookup объекта запроса, после чего используем объект «NewType» для извлечения новая возможность из Lookup объекта Lookup, которая позволяет нам включить «NewAction», который, когда пользователь щелкает по нему, вызывает «новые» диалоговые окна из API-интерфейсов NetBeans, после чего вызывается реализация реализаций объекта перезагружаемых возможностей для обновления вид.

Вышеупомянутый параграф суммирует все, что следует.

  1. Расширить DAO. Это ни в коем случае не идеально, но вот текущее состояние моего DAO. Как указывалось выше, целью этой серии является не создание идеального кода JPA, а просто получение чего-то базового, работающего так, чтобы возможности могли быть построены поверх них:

    import client.Person;
    import client.Trip;
    import client.Triptype;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Random;
    import javax.persistence.EntityManager;
    import javax.persistence.Persistence;
    import javax.persistence.Query;

    public final class TripSearchDAO {

    public List<Trip> search() {
    createTransactionalEntityManager();
    List<Trip> trips = new ArrayList<Trip>();
    List<Trip> resultList = query.getResultList();
    for (Trip c : resultList) {
    trips.add(c);
    }
    return trips;
    }

    public void save(Trip trip) {
    createTransactionalEntityManager();
    em.merge(trip);
    closeTransactionalEntityManager();
    }

    public void create(Trip trip) {
    createTransactionalEntityManager();
    //Create some random content
    //for the foreign key objects:
    Random generator = new Random();
    trip.setPersonid(new Person(generator.nextInt()));
    Triptype tt = new Triptype();
    tt.setDescription("Bla");
    tt.setTriptypeid(generator.nextInt());
    tt.setName("Bla bla");
    tt.setLastupdated(new Date());
    trip.setTriptypeid(tt);
    trip.setTripid(generator.nextInt());
    //Then persist the trip:
    em.persist(trip);
    closeTransactionalEntityManager();
    }

    private EntityManager em;
    private Query query;

    private void createTransactionalEntityManager() {
    em = Persistence.createEntityManagerFactory("TripPU").createEntityManager();
    query = em.createQuery("SELECT t FROM Trip t");
    em.getTransaction().begin();
    }

    private void closeTransactionalEntityManager() {
    em.getTransaction().commit();
    em.close();
    }

    }
  2. Определите CreatableEnityCapability . Итак, теперь мы создаем новый класс, который определяет возможность создания новых поездок:

    import client.Trip;

    public interface CreatableEntityCapability {

    public void create(Trip trip) throws Exception;

    }
  3. Реализуйте возможность. Далее, в объекте запроса мы реализуем эту возможность и, что очень важно, помещаем ее в Lookup объекта запроса, чтобы ее можно было извлечь из нее позже. Ниже вы видите точно такой же класс, как и в предыдущих двух частях (хотя здесь вместо «Клиентов» используются «Поездки») с добавлением новой CreatableEntityCapability:

    import org.my.api.capabilities.ReloadableEntityCapability;
    import org.my.api.capabilities.SaveableEntityCapability;
    import org.my.api.capabilities.CreatableEntityCapability;
    import client.Trip;
    import java.util.ArrayList;
    import java.util.List;
    import org.netbeans.api.progress.ProgressHandle;
    import org.netbeans.api.progress.ProgressHandleFactory;
    import org.openide.util.Lookup;
    import org.openide.util.lookup.AbstractLookup;
    import org.openide.util.lookup.InstanceContent;

    public final class TripQuery implements Lookup.Provider {

    private List<Trip> trips;
    private Lookup lookup;
    private InstanceContent instanceContent;
    private TripSearchDAO dao = new TripSearchDAO();

    public TripQuery() {
    trips = new ArrayList<Trip>();
    // Create an InstanceContent to hold abilities...
    instanceContent = new InstanceContent();
    // Create an AbstractLookup to expose InstanceContent contents...
    lookup = new AbstractLookup(instanceContent);
    // Add a "Reloadable" ability to this entity...
    instanceContent.add(new ReloadableEntityCapability() {
    @Override
    public void reload() throws Exception {
    ProgressHandle handle = ProgressHandleFactory.createHandle("Loading...");
    handle.start();
    List<Trip> result = dao.search();
    for (Trip trip : result) {
    if (!getTrips().contains(trip)) {
    getTrips().add(trip);
    }
    }
    handle.finish();
    }
    });
    // ...and a "Saveable" ability to this entity...
    instanceContent.add(new SaveableEntityCapability() {
    @Override
    public void save(Trip trip) throws Exception {
    dao.save(trip);
    }
    });
    // ...and a "Creatable" ability to this entity:
    instanceContent.add(new CreatableEntityCapability() {
    @Override
    public void create(Trip trip) throws Exception {
    dao.create(trip);
    }
    });
    }

    @Override
    public Lookup getLookup() {
    return lookup;
    }

    public List<Trip> getTrips() {
    return trips;
    }

    }
  4. Зарегистрируйте NewAction. Платформа NetBeans предоставляет класс «NewAction» из коробки, который включается, когда объект «NewType» доступен в Lookup. Прочитайте эту запись в моем блоге , а также учебное руководство по модулю системных свойств NetBeans для получения подробных сведений по этой теме.

    Давайте представим, что «NewAction», то есть действие, которое существует в платформе NetBeans, необходимо вызывать с панели инструментов, как показано на скриншотах ранее. Кроме того, мы хотели бы, чтобы «NewAction» был доступен, когда пользователь щелкает правой кнопкой мыши корневой узел. Для этого создайте новый модуль с именем «MyBranding» и обязательно добавьте файл layer.xml при создании этого нового модуля. В этом файле layer.xml зарегистрируйте Действие следующим образом:

    <folder name="Actions">
    <folder name="RootTrip">
    <file name="org-openide-actions-NewAction.instance">
    <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
    <attr name="delegate" newvalue="org.openide.actions.NewAction"/>
    <attr name="type" stringvalue="org.openide.util.datatransfer.NewType"/>
    <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
    <attr name="displayName" stringvalue="New Trip"/>
    <attr name="iconBase" stringvalue="org/my/branding/record-new.png"/>
    <attr name="noIconInMenu" boolvalue="false"/>
    </file>
    </folder>
    </folder>

    Теперь «NewAction» зарегистрирован в Actions / RootTrip и обратите внимание, что он станет активным, когда «NewType» находится в «Lookup». Это именно то, что нужно «NewAction», если вы посмотрите на исходный код «NewAction».

    Далее мы хотели бы отобразить вышеуказанное действие на панели инструментов. В том же файле layer.xml добавьте следующий файл, который создает теневой файл, который ссылается на вышеописанную регистрацию действия, позволяя запускать действие из панели инструментов:

    <folder name="Toolbars">
    <folder name="File">
    <file name="org-openide-actions-NewAction.shadow">
    <attr name="originalFile" stringvalue="Actions/RootTrip/org-openide-actions-NewAction.instance"/>
    <attr name="position" intvalue="10"/>
    </file>
    </folder>
    </folder>

    Затем добавьте его в контекстное меню корневого узла, например, в RootNode:

    @Override
    public Action[] getActions(boolean context) {
    List<? extends Action> tripActions = Utilities.actionsForPath("Actions/RootTrip");
    return tripActions.toArray(new Action[tripActions.size()]);
    }
  5. Реализуйте NewType. Теперь мы будем смотреть на объект « NewType ». Реализация этого класса API NetBeas должна быть в Lookup, иначе «NewAction» не будет включен. Теперь мы хотим, чтобы «NewAction» был включен в трех различных сценариях, как описано в начале этой статьи. Однако во всех случаях нам нужно иметь доступный узел, потому что только узел может иметь «новый тип» (путем переопределения метода узла «getNewTypes»). Следовательно, у нас есть два разных узла, о которых нужно беспокоиться. Поэтому мы создадим «NewType», который относится к обоим нашим узлам, в модуле «MyAPI», чтобы все модули могли иметь к нему доступ:

    import org.openide.nodes.Node;
    import client.Trip;
    import java.io.IOException;
    import org.my.api.capabilities.CreatableEntityCapability;
    import org.my.api.capabilities.ReloadableEntityCapability;
    import org.my.api.capabilities.ReloadableViewCapability;
    import org.openide.DialogDisplayer;
    import org.openide.NotifyDescriptor;
    import org.openide.util.Exceptions;
    import org.openide.util.NbBundle.Messages;
    import org.openide.util.datatransfer.NewType;
    import static org.my.api.Bundle.*;

    @Messages({
    "LBL_NewDestination_dialog=Trip Destination:",
    "LBL_NewDeparture_dialog=Trip Departure:",
    "TITLE_NewTrip_dialog=New Trip"})
    public class TripType extends NewType {

    private final TripQuery query;
    private final Node node;
    private final boolean root;

    public TripType(TripQuery query, Node node, boolean root) {
    this.query = query;
    this.node = node;
    this.root = root;
    }

    @Override
    public String getName() {
    return TITLE_NewTrip_dialog();
    }

    @Override
    public void create() throws IOException {
    NotifyDescriptor.InputLine msg = new NotifyDescriptor.InputLine(LBL_NewDeparture_dialog(), TITLE_NewTrip_dialog());
    DialogDisplayer.getDefault().notify(msg);
    String departureCity = msg.getInputText();
    msg = new NotifyDescriptor.InputLine(LBL_NewDestination_dialog(), TITLE_NewTrip_dialog());
    Object result = DialogDisplayer.getDefault().notify(msg);
    String destinationCity = msg.getInputText();
    if (NotifyDescriptor.YES_OPTION.equals(result)) {
    try {
    //Create a new Trip object:
    Trip trip = new Trip();
    trip.setDestcity(destinationCity);
    trip.setDepcity(departureCity);
    //Pass the trip to the query's implementation of the create capability:
    CreatableEntityCapability cec = query.getLookup().lookup(CreatableEntityCapability.class);
    cec.create(trip);
    //Refresh the list of trips via the implementation of the reload capability:
    ReloadableEntityCapability r = query.getLookup().lookup(ReloadableEntityCapability.class);
    r.reload();
    //If the Node passed in is the root node, refresh the root node,
    //else refresh the child node only:
    if (!root) {
    ReloadableViewCapability rvcParent = node.getParentNode().getLookup().lookup(ReloadableViewCapability.class);
    rvcParent.reloadChildren();
    } else {
    ReloadableViewCapability rvc = node.getLookup().lookup(ReloadableViewCapability.class);
    rvc.reloadChildren();
    }
    } catch (Exception ex) {
    Exceptions.printStackTrace(ex);
    }
    }
    }

    }
  6. Поместите новый тип в поиск. Теперь, когда у нас есть «NewType», нам нужно поместить его в Lookup! И мы имеем дело с двумя разными узлами, следовательно, с двумя разными поисками. Так как у нас есть класс выше, в модуле API мы можем ссылаться на него где угодно.

    • Корневой узел В качестве переменной класса объявите NewType:

      private TripType tripType = null;

      В конструкторе:

      ...
      ...
      ...
      tripType = new TripType(query, this, true);
      instanceContent.add(tripType);
      ...
      ...
      ...

      Как видите, мы создаем «TripType», который является нашим «NewType», и добавляем его в «Уточнение» корневого узла. Наконец, в RootNode нам нужно предоставить «NewType» с помощью метода «getNewTypes»:

      @Override
      public NewType[] getNewTypes() {
      return new NewType[]{tripType};
      }

      Но почему мы добавляем NewType в Lookup, а также в getNewTypes? Потому что на панели инструментов нам нужно, чтобы кнопка NewAction была включена, когда выбран RootNode, что произойдет только тогда, когда NewType находится в Lookup для RootNode. 

    • Детский узел. Сделайте то же самое, что и выше, в дочернем узле, т. Е. «TripNode» в случае этого конкретного примера.
    • Редактор TopComponent. Наконец, добавьте LookupListener в редактор TopComponent и прослушайте Node в текущем выбранном окне. Сделайте этот узел активированным узлом TopComponent, чтобы NewType узла был доступен для TopComponent, и поместите NewType в поиск в редакторе TopComponent редактора, чтобы активировать кнопку NewAction на панели инструментов.

Следуя приведенным выше инструкциям, теперь вы должны иметь NewAction, работающий во всем приложении, независимо от того, какое окно выбрано. В следующий раз мы добавим DeleteAction к миксу через DeletableEntityCapability, следуя шаблону, подобному приведенному выше.