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

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

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

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

    @ActionType("showSelected")
    public class ShowSelectedAction extends ItemTrackingAction {
    
        @Autowired
        private MetadataTools metadataTools;
    
        private String description;
    
        public ShowSelectedAction(String id) {
            super(id);
            setCaption("Show Selected");
        }
    
        public String getDescription() {
            return description;
        }
    
        public void setDescription(String description) {
            this.description = description;
        }
    
        @Override
        public void actionPerform(Component component) {
            if (getTarget() != null) {
                Object selected = getTarget().getSingleSelected();
                if (selected != null) {
                    Notifications notifications = ComponentsHelper.getScreenContext(target).getNotifications();
                    notifications.create()
                            .withType(Notifications.NotificationType.TRAY)
                            .withDescription(description)
                            .withCaption(metadataTools.getInstanceName(selected))
                            .show();
                }
            }
        }
    }
  2. Теперь вы можете использовать действие в дескрипторах экрана, просто указывая его тип:

    <groupTable id="custTable" multiselect="true"
                width="100%"
                dataContainer="customersDc">
        <actions>
            <action id="show" type="showSelected">
                <properties>
                    <property name="description" value="Information about the selected item"/>
                </properties>
            </action>
        </actions>
        <columns>
            <column id="firstName"/>
            <column id="lastName"/>
            <column id="rewardPoints"/>
            <column id="level"/>
        </columns>
        <buttonsPanel alwaysVisible="true">
            <button action="custTable.show"/>
        </buttonsPanel>
    </groupTable>

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

Использование собственных действий в Studio

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • java.lang.Class.

  • java.util.List – список, состоящий из элементов, чьи типы указаны выше. Панель Component 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, т.к. панель Component Inspector не содержит подходящего компонента ввода.

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

Пример: SendByEmailAction

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

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

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

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

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

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

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

@StudioAction(target = "io.jmix.ui.component.ListComponent", description = "Sends selected entity by email")(1)
@ActionType("sendByEmail")(2)
public class SendByEmailAction <E> extends ItemTrackingAction {(3)

    @Autowired
    private MetadataTools metadataTools;
    @Autowired
    private Emailer emailer;


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

    @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)
                .setSubject(caption)
                .setBody(body)
                .setBodyContentType(EmailInfo.TEXT_CONTENT_TYPE)
                .setAttachments(attachments.toArray(new EmailAttachment[0]))
                .build();

        emailer.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 отобразит новое собственное действие вместе со стандартными действиями в диалоге создания действия:

studio custom action

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

studio custom action properties

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

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

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

studio custom action handlers

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

@UiController("table-screen")
@UiDescriptor("table-screen.xml")
public class TableScreen extends Screen {

    @Autowired
    private Notifications notifications;

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

    @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.getFirstName();
    }

    @Install(to = "customersTable.sendByEmail", subject = "attachmentProvider")
    private List<EmailAttachment> customersTableSendByEmailAttachmentProvider(Customer customer) {(4)
        return Collections.emptyList();
    }

}
1 При инжекции действия используется корректный параметр типа.
2 Реализация слушателя события.
3 Реализация делегирующего метода bodyGenerator. Параметр типа Customer подставлен в сигнатуру метода.
4 Реализация делегирующего метода attachmentProvider.