3.5.5.3. Собственные типы действий

В проекте можно создать собственные типы действий или переопределить существующие стандартные типы.

Предположим, что вы хотите создать действие, которое бы показывало имя экземпляра текущей сущности, выбранной в таблице, и использовать это действие в различных экранах, указывая только его тип. Чтобы это сделать, необходимо выполнить следующие шаги:

  1. Создайте для действия отдельный класс с аннотацией @ActionType, в которой укажите желаемое имя типа:

    package com.company.sample.web.actions;
    
    import com.haulmont.cuba.core.entity.Entity;
    import com.haulmont.cuba.core.global.MetadataTools;
    import com.haulmont.cuba.gui.ComponentsHelper;
    import com.haulmont.cuba.gui.Notifications;
    import com.haulmont.cuba.gui.components.ActionType;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.components.actions.ItemTrackingAction;
    
    import javax.inject.Inject;
    
    @ActionType("showSelected")
    public class ShowSelectedAction extends ItemTrackingAction {
    
        @Inject
        private MetadataTools metadataTools;
    
        public ShowSelectedAction(String id) {
            super(id);
            setCaption("Show Selected");
        }
    
        @Override
        public void actionPerform(Component component) {
            Entity selected = getTarget().getSingleSelected();
            if (selected != null) {
                Notifications notifications = ComponentsHelper.getScreenContext(target).getNotifications();
                notifications.create()
                        .withType(Notifications.NotificationType.TRAY)
                        .withCaption(metadataTools.getInstanceName(selected))
                        .show();
            }
        }
    }
  2. В файле web-spring.xml добавьте элемент <gui:actions>, и в его атрибуте base-packages укажите пакет, в котором нужно искать ваши аннотированные действия:

    <beans ... xmlns:gui="http://schemas.haulmont.com/cuba/spring/cuba-gui.xsd">
        <!-- ... -->
        <gui:actions base-packages="com.company.sample.web.actions"/>
    </beans>
  3. Теперь вы можете использовать действие в дескрипторах экрана, просто указывая его тип:

    <groupTable id="customersTable">
        <actions>
            <action id="show" type="showSelected"/>
        </actions>
        <columns>
            <!-- ... -->
        </columns>
        <buttonsPanel>
            <button action="customersTable.show"/>
        </buttonsPanel>
    </groupTable>

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

Поддержка в CUBA Studio и редактируемые свойства для собственных действий

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

  • Возможность выбрать тип действия в списке стандартных действий во время добавления нового действия в экран из палитры или при выполнении +AddAction в таблице.

  • Быстрая навигация из места использования действия к классу действия по нажатию Ctrl + B или Ctrl-клику мыши, когда курсор находится на типе действия в дескрипторе экрана (например на атрибуте showSelected в XML-фрагменте <action id="sel" type="showSelected">).

  • Редактирование определенных пользователем свойств действия в панели Component Inspector.

  • Генерация обработчиков событий и делегирующих методов, объявленных в классе действия для кастомизации его логики.

  • Поддержка параметризации генерик-типом. Генерик-тип определяется как класс сущности, используемый в таблице (компонент-владелец действия).

Аннотация @com.haulmont.cuba.gui.meta.StudioAction используется для пометки собственного класса действия, содержащего дополнительные свойства, определенные пользователем. Собственные действия нужно помечать этой аннотацией, однако в данный момент Studio не использует дополнительных атрибутов аннотации @StudioAction.

Аннотация @com.haulmont.cuba.gui.meta.StudioPropertiesItem используется, чтобы пометить setter-метод свойства действия как редактируемое свойство. Такие свойства будут отображаться и редактироваться в панели Component Inspector дизайнера экранов. Аннотация имеет следующие атрибуты:

  • name - название атрибута, который будет сохраняться в XML. Если не установлено, то название будет определено по именованию setter-метода.

  • type - тип свойства. Используется панелью Inspector, чтобы создать подходящий компонент ввода, предоставляющий подсказки и базовую валидацию. Описания всех типов свойств приведены здесь.

  • caption - подпись свойства, отображается в панели Inspector.

  • description - дополнительное описание свойства, отображается в панели Inspector как всплывающая подсказка по наведению мыши.

  • category - категория свойства в панели Inspector (на данный момент еще не используется дизайнером экранов).

  • required - обязательность свойства. Если свойство обязательное, то панель Inspector не позволит ввести для него пустое значение.

  • defaultValue - значение свойства по умолчанию, т.е. значение, которое неявно используется действием, если соответствующий XML атрибут отсутствует. Значение по умолчанию не будет записываться в XML.

  • options - список вариантов для свойства действия, например для типа свойства ENUMERATION.

Заметьте, что для свойств действий поддерживается только ограниченный набор Java типов:

  • Примитивные типы: String, Boolean, Byte, Short, Integer, Long, Float, Double.

  • Перечисления.

  • java.lang.Class.

  • java.util.List - список, состоящий из элементов, чьи типы указаны выше. Панель Inspector не имеет точно подходящего компонента для этого Java класса, поэтому этот тип должен вводиться как обычная строка, и помечаться как PropertyType.STRING.

Примеры:

private String contentType = "PLAIN";
private Class<? extends Screen> dialogClass;
private List<Integer> columnNumbers = new ArrayList<>();

@StudioPropertiesItem(name = "ctype", type = PropertyType.ENUMERATION, description = "Email content type", (1)
        defaultValue = "PLAIN", options = {"PLAIN", "HTML"}
)
public void setContentType(String contentType) {
    this.contentType = contentType;
}

@StudioPropertiesItem(type = PropertyType.SCREEN_CLASS_NAME, required = true) (2)
public void setDialogClass(Class<? extends Screen> dialogClass) {
    this.dialogClass = dialogClass;
}

@StudioPropertiesItem(type = PropertyType.STRING) (3)
public void setColumnNumbers(List<Integer> columnNumbers) {
    this.columnNumbers = columnNumbers;
}
1 - строковое свойство с ограниченным набором вариантов ввода и значением по умолчанию.
2 - обязательное свойство с набором вариантов - списком классов экранов, определенных в проекте.
3 - список целых чисел. Тип свойства установлен как STRING, т.к. панель Inspector не содержит подходящего компонента ввода.

Также Studio предоставляет поддержку для событий и делегирующих методов в собственных действиях - такую же, как и для встроенных UI компонентов. Для объявления слушателя события или делегирующего метода в классе действия не требуется никаких дополнительных аннотаций. Пример их объявления приведен ниже.

Пример настраиваемого действия: SendByEmailAction

Этот пример демонстрирует:

  • объявление и аннотирование класса собственного действия.

  • аннотирование редактируемых параметров действия.

  • объявление собственного класса события, производимого действием, и его обработчика.

  • объявление делегирующих методов.

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

Исходный код действия:

@StudioAction(category = "List Actions", description = "Sends selected entity by email") (1)
@ActionType("sendByEmail") (2)
public class SendByEmailAction<E extends Entity> extends ItemTrackingAction { (3)

    private final MetadataTools metadataTools;
    private final EmailService emailService;

    private String recipientAddress = "admin@example.com";

    private Function<E, String> bodyGenerator;
    private Function<E, List<EmailAttachment>> attachmentProvider;

    public SendByEmailAction(String id) {
        super(id);
        setCaption("Send by email");
        emailService = AppBeans.get(EmailService.NAME);
        metadataTools = AppBeans.get(MetadataTools.NAME);
    }

    @StudioPropertiesItem(required = true, defaultValue = "admin@example.com") (4)
    public void setRecipientAddress(String recipientAddress) {
        this.recipientAddress = recipientAddress;
    }

    public Subscription addEmailSentListener(Consumer<EmailSentEvent> listener) { (5)
        return getEventHub().subscribe(EmailSentEvent.class, listener);
    }

    public void setBodyGenerator(Function<E, String> bodyGenerator) { (6)
        this.bodyGenerator = bodyGenerator;
    }

    public void setAttachmentProvider(Function<E, List<EmailAttachment>> attachmentProvider) { (7)
        this.attachmentProvider = attachmentProvider;
    }

    @Override
    public void actionPerform(Component component) {
        if (recipientAddress == null || bodyGenerator == null) {
            throw new IllegalStateException("Required parameters are not set");
        }

        E selected = (E) getTarget().getSingleSelected();
        if (selected == null) {
            return;
        }

        String caption = "Entity " + metadataTools.getInstanceName(selected) + " info";
        String body = bodyGenerator.apply(selected); (8)
        List<EmailAttachment> attachments = attachmentProvider != null ? attachmentProvider.apply(selected) (9)
                : new ArrayList<>();

        EmailInfo info = EmailInfoBuilder.create()
                .setAddresses(recipientAddress)
                .setCaption(caption)
                .setBody(body)
                .setBodyContentType(EmailInfo.TEXT_CONTENT_TYPE)
                .setAttachments(attachments.toArray(new EmailAttachment[0]))
                .build();

        emailService.sendEmailAsync(info); (10)

        EmailSentEvent event = new EmailSentEvent(this, info);
        eventHub.publish(EmailSentEvent.class, event); (11)
    }

    public static class EmailSentEvent extends EventObject { (12)
        private final EmailInfo emailInfo;

        public EmailSentEvent(SendByEmailAction origin, EmailInfo emailInfo) {
            super(origin);
            this.emailInfo = emailInfo;
        }

        public EmailInfo getEmailInfo() {
            return emailInfo;
        }
    }
}
1 - класс действия помечен аннотацией @StudioAction.
2 - id действия устанавливается аннотацией @ActionType.
3 - класс действия параметризован типом E - это тип сущности, которая отображается компонентом таблицы.
4 - адрес получателя email выставлен как редактируемое свойство действия.
5 - метод, регистрирующий слушатель для события EmailSentEvent. Этот метод определяется Studio как объявление обработчика события в действии.
6 - метод, устанавливающий объект Function, этот объект используется для делегирования (контроллеру экрана) логики составления тела письма. Этот метод определяется Studio как объявление делегирующего метода.
7 - объявление другого делегирующего метода - на этот раз он используется для делегирования логики создания вложений к письму. Заметьте, что оба делегирующих метода используют параметр E генерик-типа.
8 - обязательный делегирующий метод (реализованный в контроллере экрана) вызывается для составления текста письма.
9 - если установлен, необязательный делегирующий метод вызывается для генерации вложений к письму.
10 - здесь собственно и посылается email.
11 - событие EmailSentEvent публикуется после успешной посылки письма. Если контроллер экрана был подписан на это событие, то будет вызван соответствующий обработчик.
12 - объявление класса события. Обратите внимание, что в класс события можно добавить поля и таким образом передавать в логику обработки события дополнительные данные.

Если реализовать класс как показано выше, то Studio отобразит новое собственное действие вместе со стандартными действиями в диалоге создания действия:

custom action wizard

Когда действие добавлено в дескриптор экрана, вы можете выбрать его и редактировать его свойства в панели Component Inspector:

custom action properties

Когда свойство собственного действия изменяется в панели Inspector, оно записывается в дескриптор экрана следующим образом:

<action id="sendByEmail" type="sendByEmail">
    <properties>
        <property name="recipientAddress" value="peter@example.com"/>
    </properties>
</action>

Обработчики событий и делегирующие методы действия также отображаются и доступны для генерации в панели Component Inspector:

custom action handlers

Пример сгенерированной логики с реализованными обработчиками события и делегирующими методами выглядит следующим образом:

@UiController("sales_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LookupComponent("customersTable")
@LoadDataBeforeShow
public class CustomerBrowse extends StandardLookup<Customer> {

    @Inject
    private Notifications notifications;

    @Named("customersTable.sendByEmail")
    private SendByEmailAction<Customer> customersTableSendByEmail; (1)

    @Subscribe("customersTable.sendByEmail")
    public void onCustomersTableSendByEmailEmailSent(SendByEmailAction.EmailSentEvent event) { (2)
        notifications.create(Notifications.NotificationType.HUMANIZED)
                .withCaption("Email sent")
                .show();
    }

    @Install(to = "customersTable.sendByEmail", subject = "bodyGenerator")
    private String customersTableSendByEmailBodyGenerator(Customer customer) { (3)
        return "Hello, " + customer.getName();
    }

    @Install(to = "customersTable.sendByEmail", subject = "attachmentProvider")
    private List<EmailAttachment> customersTableSendByEmailAttachmentProvider(Customer customer) { (4)
        return Collections.emptyList();
    }
}
1 - при инжекции действия используется корректный параметр типа.
2 - реализация обработчика события.
3 - реализация делегирующего метода bodyGenerator. Параметр типа Customer подставлен в сигнатуру метода.
4 - реализация делегирующего метода attachmentProvider.