3.5.17.4.4. 创建 GWT 组件

在本节中,我们介绍如何创建一个简单的 GWT 组件(由 5 颗星组成的评级字段)及其在应用程序界面中的用法。

rating field component

在 CUBA Studio 中创建一个新项目,并将其命名为 ratingsample

创建 web-toolkit 模块。一个简便的方法就是使用 CUBA Studio:在主菜单,点击 CUBA > Advanced > Manage modules > Create ‘web-toolkit’ Module

为了创建 GWT 组件,需要创建下列文件:

  • RatingFieldWidget.java - web-toolkit 模块中的 GWT 部件。

  • RatingFieldServerComponent.java - Vaadin 组件类。

  • RatingFieldState.java - 组件状态类。

  • RatingFieldConnector.java - 连接器,用于连接客户端代码和服务器组件。

  • RatingFieldServerRpc.java - 为客户端定义服务器 API 的类。

现在创建需要的文件并对其进行必要的更改。

  • web-toolkit 模块中创建 RatingFieldWidget 类。使用下面代码作为其内容:

    1. package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    2. import com.google.gwt.dom.client.DivElement;
    3. import com.google.gwt.dom.client.SpanElement;
    4. import com.google.gwt.dom.client.Style.Display;
    5. import com.google.gwt.user.client.DOM;
    6. import com.google.gwt.user.client.Event;
    7. import com.google.gwt.user.client.ui.FocusWidget;
    8. import java.util.ArrayList;
    9. import java.util.List;
    10. public class RatingFieldWidget extends FocusWidget {
    11. private static final String CLASSNAME = "ratingfield";
    12. // API for handle clicks
    13. public interface StarClickListener {
    14. void starClicked(int value);
    15. }
    16. protected List<SpanElement> stars = new ArrayList<>(5);
    17. protected StarClickListener listener;
    18. protected int value = 0;
    19. public RatingFieldWidget() {
    20. DivElement container = DOM.createDiv().cast();
    21. container.getStyle().setDisplay(Display.INLINE_BLOCK);
    22. for (int i = 0; i < 5; i++) {
    23. SpanElement star = DOM.createSpan().cast();
    24. // add star element to the container
    25. DOM.insertChild(container, star, i);
    26. // subscribe on ONCLICK event
    27. DOM.sinkEvents(star, Event.ONCLICK);
    28. stars.add(star);
    29. }
    30. setElement(container);
    31. setStylePrimaryName(CLASSNAME);
    32. }
    33. // main method for handling events in GWT widgets
    34. @Override
    35. public void onBrowserEvent(Event event) {
    36. super.onBrowserEvent(event);
    37. switch (event.getTypeInt()) {
    38. // react on ONCLICK event
    39. case Event.ONCLICK:
    40. SpanElement element = event.getEventTarget().cast();
    41. // if click was on the star
    42. int index = stars.indexOf(element);
    43. if (index >= 0) {
    44. int value = index + 1;
    45. // set internal value
    46. setValue(value);
    47. // notify listeners
    48. if (listener != null) {
    49. listener.starClicked(value);
    50. }
    51. }
    52. break;
    53. }
    54. }
    55. @Override
    56. public void setStylePrimaryName(String style) {
    57. super.setStylePrimaryName(style);
    58. for (SpanElement star : stars) {
    59. star.setClassName(style + "-star");
    60. }
    61. updateStarsStyle(this.value);
    62. }
    63. // let application code change the state
    64. public void setValue(int value) {
    65. this.value = value;
    66. updateStarsStyle(value);
    67. }
    68. // refresh visual representation
    69. private void updateStarsStyle(int value) {
    70. for (SpanElement star : stars) {
    71. star.removeClassName(getStylePrimaryName() + "-star-selected");
    72. }
    73. for (int i = 0; i < value; i++) {
    74. stars.get(i).addClassName(getStylePrimaryName() + "-star-selected");
    75. }
    76. }
    77. }

    部件(Widget)是一个客户端类,负责在 Web 浏览器中显示组件并处理浏览器事件。它定义了与服务端配合起来工作的接口。在这个的例子中,这些接口是 setValue() 方法和 StarClickListener 接口。

  • RatingFieldServerComponent 是一个 Vaadin 组件类。它定义了服务端代码 API、访问器方法、事件监听器和数据源连接。开发人员在应用程序代码中使用的是这个类的方法。

    1. package com.company.ratingsample.web.toolkit.ui;
    2. import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
    3. import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
    4. import com.vaadin.ui.AbstractField;
    5. // the field will have a value with integer type
    6. public class RatingFieldServerComponent extends AbstractField<Integer> {
    7. public RatingFieldServerComponent() {
    8. // register an interface implementation that will be invoked on a request from the client
    9. registerRpc((RatingFieldServerRpc) value -> setValue(value, true));
    10. }
    11. @Override
    12. protected void doSetValue(Integer value) {
    13. if (value == null) {
    14. value = 0;
    15. }
    16. getState().value = value;
    17. }
    18. @Override
    19. public Integer getValue() {
    20. return getState().value;
    21. }
    22. // define own state class
    23. @Override
    24. protected RatingFieldState getState() {
    25. return (RatingFieldState) super.getState();
    26. }
    27. @Override
    28. protected RatingFieldState getState(boolean markAsDirty) {
    29. return (RatingFieldState) super.getState(markAsDirty);
    30. }
    31. }
  • RatingFieldState 状态类定义客户端和服务器之间发送的数据。它包含在服务端自动序列化并在客户端上反序列化的公共字段。

    1. package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    2. import com.vaadin.shared.AbstractFieldState;
    3. public class RatingFieldState extends AbstractFieldState {
    4. { // change the main style name of the component
    5. primaryStyleName = "ratingfield";
    6. }
    7. // define a field for the value
    8. public int value = 0;
    9. }
  • RatingFieldServerRpc 接口定义了客户端可调用的服务器 API。它的方法可以由 Vaadin 内置的 RPC 机制调用。我们将在此组件中实现此接口。

    1. package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    2. import com.vaadin.shared.communication.ServerRpc;
    3. public interface RatingFieldServerRpc extends ServerRpc {
    4. //method will be invoked in the client code
    5. void starClicked(int value);
    6. }
  • web-toolkit 模块中创建 RatingFieldConnector 类,连接器将客户端代码与服务端连接起来。

    1. package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    2. import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
    3. import com.vaadin.client.communication.StateChangeEvent;
    4. import com.vaadin.client.ui.AbstractFieldConnector;
    5. import com.vaadin.shared.ui.Connect;
    6. // link the connector with the server implementation of RatingField
    7. // extend AbstractField connector
    8. @Connect(RatingFieldServerComponent.class)
    9. public class RatingFieldConnector extends AbstractFieldConnector {
    10. // we will use a RatingFieldWidget widget
    11. @Override
    12. public RatingFieldWidget getWidget() {
    13. RatingFieldWidget widget = (RatingFieldWidget) super.getWidget();
    14. if (widget.listener == null) {
    15. widget.listener = value ->
    16. getRpcProxy(RatingFieldServerRpc.class).starClicked(value);
    17. }
    18. return widget;
    19. }
    20. // our state class is RatingFieldState
    21. @Override
    22. public RatingFieldState getState() {
    23. return (RatingFieldState) super.getState();
    24. }
    25. // react on server state change
    26. @Override
    27. public void onStateChanged(StateChangeEvent stateChangeEvent) {
    28. super.onStateChanged(stateChangeEvent);
    29. // refresh the widget if the value on server has changed
    30. if (stateChangeEvent.hasPropertyChanged("value")) {
    31. getWidget().setValue(getState().value);
    32. }
    33. }
    34. }

RatingFieldWidget 类中不定义组件的外观样式,只为关键元素指定样式名称。要定义组件的外观,需要创建样式表文件。简便方法就是使用 CUBA Studio:在主菜单,点击 CUBA > Advanced > Manage themes > Create theme extension。在弹窗中选择 hover 主题。另一个方法是使用 CUBA CLIextend-theme 命令。hover 主题使用了 FontAwesome 的象形符号字体替代了 icons

建议以 SCSS 混入(Mixin)的形式将组件样式放到 components/componentname 目录中的单独文件 componentname.scss 中。在 web 模块的 themes/hover/com.company.ratingsample 目录中创建 components/ratingfield 目录结构。然后在 ratingfield 目录中创建 ratingfield.scss 文件:

gwt theme ext structure

  1. @mixin ratingfield($primary-stylename: ratingfield) {
  2. .#{$primary-stylename}-star {
  3. font-family: FontAwesome;
  4. font-size: $v-font-size--h2;
  5. padding-right: round($v-unit-size/4);
  6. cursor: pointer;
  7. &:after {
  8. content: '\f006'; // 'fa-star-o'
  9. }
  10. }
  11. .#{$primary-stylename}-star-selected {
  12. &:after {
  13. content: '\f005'; // 'fa-star'
  14. }
  15. }
  16. .#{$primary-stylename} .#{$primary-stylename}-star:last-child {
  17. padding-right: 0;
  18. }
  19. .#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
  20. cursor: default;
  21. }
  22. }

将此文件包含在 hover-ext.scss 主题文件中:

  1. @import "components/ratingfield/ratingfield";
  2. @mixin com_company_ratingsample-hover-ext {
  3. @include ratingfield;
  4. }

为了演示组件的工作原理,我们在 web 模块中创建一个新的界面。

将界面命名为 rating-screen

在 IDE 中打开 rating-screen.xml 文件。Rating 组件需要一个容器,我们在界面 XML 中声明它:

  1. <?xml version="1.0" encoding="UTF-8" standalone="no"?>
  2. <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
  3. caption="msg://caption"
  4. messagesPack="com.company.ratingsample.web.screens.rating">
  5. <layout expand="container">
  6. <vbox id="container">
  7. <!-- we'll add vaadin component here-->
  8. </vbox>
  9. </layout>
  10. </window>

打开 RatingScreen.java 界面控制器并添加将组件放置到界面上的代码。

  1. package com.company.ratingsample.web.screens.rating;
  2. import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
  3. import com.haulmont.cuba.gui.components.VBoxLayout;
  4. import com.haulmont.cuba.gui.screen.Screen;
  5. import com.haulmont.cuba.gui.screen.Subscribe;
  6. import com.haulmont.cuba.gui.screen.UiController;
  7. import com.haulmont.cuba.gui.screen.UiDescriptor;
  8. import com.vaadin.ui.Layout;
  9. import javax.inject.Inject;
  10. @UiController("ratingsample_RatingScreen")
  11. @UiDescriptor("rating-screen.xml")
  12. public class RatingScreen extends Screen {
  13. @Inject
  14. private VBoxLayout container;
  15. @Subscribe
  16. protected void onInit(InitEvent event) {
  17. RatingFieldServerComponent field = new RatingFieldServerComponent();
  18. field.setCaption("Rate this!");
  19. container.unwrap(Layout.class).addComponent(field);
  20. }
  21. }

启动应用程序服务并查看结果。

rating screen result