12

我正在尝试使用自定义列标题实现一个 CellTable,该列标题在普通列文本下方显示一个搜索框(简单文本框)。
SearchBox 应该允许用户过滤 CellTable。它应该看起来像这样:

  |Header  1|Header 2 |
  |SEARCHBOX|SEARCHBOX|
  -------------------------------------------------------
  |    ROW 1 
  ------------------------------------------------------
  |    ROW 2 

一旦用户在 SearchBox 中输入一个字符,就会触发RangeChangeEvent,这会导致服务器请求,并且 CellTable 会使用新的过滤列表进行更新。

基本上一切正常。然而,一旦 CellTable 刷新,SearchBox就会失去焦点,用户必须再次用鼠标单击 SearchBox 才能输入新字符。

这可能与在 CellTable 刷新后调用自定义表头及其单元格的 render 方法有关。
有什么办法可以将焦点重新设置回 SearchBox?我试图设置tabindex=0但没有帮助。

自定义标题类

public static class SearchHeader extends Header<SearchTerm> {
    @Override
    public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
    }
    private SearchTerm searchTerm;
    public SearchHeader(SearchTerm searchTerm,ValueUpdater<SearchTerm> valueUpdater) {
        super(new SearchCell());
        setUpdater(valueUpdater);
        this.searchTerm = searchTerm;
    }
    @Override
    public SearchTerm getValue() {
        return searchTerm;
    }
 }

自定义搜索单元(用于自定义标题)

当用户在 SearchBox 中键入内容时, isChanged布尔标志设置为true ,如果 SearchBox 失去焦点,则设置回false 。我添加了这个标志以区分哪个 SearchBox 获得焦点(以防我使用多个 SearchBox)

public static class SearchCell extends AbstractCell<SearchTerm> {

    interface Template extends SafeHtmlTemplates {
        @Template("<div style=\"\">{0}</div>")
        SafeHtml header(String columnName);

        @Template("<div style=\"\"><input type=\"text\" value=\"{0}\"/></div>")
        SafeHtml input(String value);
    }

    private static Template template;
    private boolean isChanged = false;

    public SearchCell() {
        super("keydown","keyup","change","blur");
        if (template == null) {
            template = GWT.create(Template.class);
        }
    }

    @Override
    public void render(com.google.gwt.cell.client.Cell.Context context,
        SearchTerm value, SafeHtmlBuilder sb) {
        sb.append(template.header(value.getCriteria().toString()));
        sb.append(template.input(value.getValue()));
    }

    @Override
    public void onBrowserEvent(Context context,Element parent, SearchTerm value,NativeEvent event,ValueUpdater<SearchTerm> valueUpdater) {
        if (value == null)
            return;
        super.onBrowserEvent(context, parent, value, event, valueUpdater);
        if ("keyup".equals(event.getType()))
        {
            isChanged = true;
            InputElement elem = getInputElement(parent);
            value.setValue(elem.getValue());
            if (valueUpdater != null)
                valueUpdater.update(value);
        }
        else if ("blur".equals(event.getType())) {
            isChanged =false;
        }
     }

     protected InputElement getInputElement(Element parent) {
         Element elem = parent.getElementsByTagName("input").getItem(0);
         assert(elem.getClass() == InputElement.class);
         return elem.cast();
     }
}

CellTable 的初始化代码

NameColumn是具有适当类型的抽象Column类的实现。它在内部使用TextCell

ValueUpdater<SearchTerm> searchUpdater = new ValueUpdater<SearchTerm>() {
    @Override
    public void update(AccessionCellTableColumns.SearchTerm value) {
        // fires a server request to return the new filtered list
        RangeChangeEvent.fire(table, new Range(table.getPageStart(), table.getPageSize())); 
    }
};

table.addColumn(new NameColumn(searchTerm),new SearchHeader(searchTerm,searchUpdater));
4

2 回答 2

21

瘦的

不幸的是,GWT 对自定义列标题的支持至少可以说有点不靠谱。如果有人喜欢使用 AbstractCell 类,你就会明白我的意思。此外,在列标题单元格中实现复合(嵌套小部件)的正确方法是失败,因为我无法让它正常工作,也没有找到 CompositeCell 工作的任何可行示例。

如果您的数据网格实现了一个 ColumnSortHandler 的排序(大声笑,那就是 phunny),您可能具有键或鼠标事件的嵌套 UI 对象将触发列排序。失败。同样,我找不到一种方法来重载 columnsort 事件以排除通过与嵌套列标题 ui 组件/小部件交互而触发的触发器。更不用说您需要通过将内联 HTML 写入构建单元的模板接口来抽象地定义嵌套组件。这几乎不是一个优雅的选择,因为它迫使开发人员必须编写原生 JavaScript 代码来创建和控制与列标题中的嵌套组件/小部件关联的处理程序。

这种“正确”的实现技术也不能解决这个问题所解决的焦点问题,并且对于需要具有列单元过滤或自定义呈现的 AsyncProvider(或 ListProvider)数据集的复杂数据网格来说,几乎不是一个很好的解决方案。这样做的表现也很糟糕>_>远非IMO的正确解决方案

严重地???

为了实现功能性列单元格过滤,您必须从 GWT 和疯狂的 JQuery 库出现之前的更传统的动态 javascript/css 应用程序中解决这个问题。我的功能解决方案是“正确”方式与一些狡猾的 css 的混合体。

伪代码如下:

  1. 确保您的网格由 LayoutPanel 包裹
  2. 确保您的网格列由集合/列表管理
  3. 创建自定义列标题以创建过滤区域
  4. 创建过滤容器以将文本框放入
  5. 布局您的网格容器子项(网格、过滤器、寻呼机)
  6. 使用 css 技术将过滤器定位到列标题中
  7. 将事件处理程序添加到过滤器
  8. 添加计时器来处理滤波器输入延迟
  9. 触发网格更新功能以刷新数据、异步或本地列表

哇,希望我还没有失去你,因为要完成这项工作还有很多事情要做


第 1 步:设置 Grid 类以扩展 LayoutPanel

首先,您需要确保创建网格的类可以在您的应用程序中正确支持和调整大小。为此,请确保您的网格类扩展了 LayoutPanel。

public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
     public PagingFilterDataGrid() {
          //ctor initializers
          initDataGrid();
          initColumns();
          updateColumns();
          initPager();
          setupDataGrid();
     }
}

步骤 2:创建托管列

这一步也很简单。而不是直接将新列添加到您的数据网格中,将它们存储到一个列表中,然后使用 foreach 语句以编程方式将它们添加到您的网格中

ColumnModel(您应该能够创建一个数字或日期,或者您想要的任何其他类型的列。为简单起见,我通常在 Web 应用程序中使用字符串数据,除非我明确需要特殊的算术或日期功能)

public abstract class GridStringColumn<M> extends Column<VwGovernorRule, String> {

    private String  text_;
    private String  tooltip_;
    private boolean defaultShown_ = true;
    private boolean hidden_       = false;

    public GridStringColumn(String fieldName, String text, String tooltip, boolean defaultShown, boolean sortable, boolean hidden) {
        super(new TextCell());
        setDataStoreName(fieldName);
        this.text_ = text;
        this.tooltip_ = tooltip;
        this.defaultShown_ = defaultShown;
        setSortable(sortable);
        this.hidden_ = hidden;
    }
}

在您的数据网格类中创建列表以将您的列存储到

public abstract class PagingFilterDataGrid<T> extends LayoutPanel {
    private List<GridStringColumn<T>> columns_ = new ArrayList<GridStringColumn<T>>();
}

要创建列,请创建在 datagrid 构造函数中调用的 initColumn 方法。通常我会扩展一个基本的 datagrid 类,这样我就可以将我的特定网格初始化器放入其中。这会在您的列存储中添加一列。MyPOJODataModel 是您存储数据网格记录的数据结构,通常它是您休眠的 POJO 或来自后端的东西。

@Override
public void initColumns() {
     getColumns().add(new GridStringColumn<MyPOJODataModel>("columnName", "dataStoreFieldName", "column tooltip / description information about this column", true, true, false) {

            @Override
            public String getValue(MyPOJODataModelobject) {
                return object.getFieldValue();
            }
        });
}

现在创建一些代码以将列更新到网格中,确保在调用 initColumns 方法后调用此方法。我们将很快介绍 initFilters 方法。但是,如果您现在需要知道,它是根据集合中的列设置过滤器的方法。您也可以在想要显示/隐藏列或重新排列网格中的列时调用此函数。我知道你喜欢它!

@SuppressWarnings("unchecked")
    public void updateColumns() {
        if (dataGrid_.getColumnCount() > 0) {
            clearColumns();
        }

        for (GridStringColumn<T> column : getColumns()) {
            if (!column.isHidden()) {
                dataGrid_.addColumn((Column<T, ?>) column, new ColumnHeader(column.getText(), column.getDataStoreName()));
            }
        }

        initFilters();
    }

第 3 步:创建自定义列标题

现在我们开始接触有趣的东西,因为我们已经准备好进行过滤的网格和列。这部分与这个问题提出的示例代码相似,但有点不同。我们在这里所做的是创建一个新的自定义 AbstractCell,我们为 GWT 指定一个 html 模板以在运行时呈现。然后我们将这个新的单元格模板注入到我们的自定义标题类中,并将其传递给 gwt 的数据用于在数据网格中创建新列的 addColumn() 方法

您的自定义单元格:

final public class ColumnHeaderFilterCell extends AbstractCell<String> {

    interface Templates extends SafeHtmlTemplates {
        @SafeHtmlTemplates.Template("<div class=\"headerText\">{0}</div>")
        SafeHtml text(String value);

        @SafeHtmlTemplates.Template("<div class=\"headerFilter\"><input type=\"text\" value=\"\"/></div>")
        SafeHtml filter();
    }

    private static Templates templates = GWT.create(Templates.class);

    @Override
    public void render(Context context, String value, SafeHtmlBuilder sb) {
        if (value == null) {
            return;
        }

        SafeHtml renderedText = templates.text(value);

        sb.append(renderedText);

        SafeHtml renderedFilter = templates.filter();
        sb.append(renderedFilter);
    }
}

如果您还没有学会讨厌如何制作自定义单元格,那么您很快就会确定在完成此操作后。接下来,我们需要一个标头来将此单元格注入

列标题:

public static class ColumnHeader extends Header<String> {

        private String name_;

        public ColumnHeader(String name, String field) {
            super(new ColumnHeaderFilterCell());
            this.name_ = name;
            setHeaderStyleNames("columnHeader " + field);
        }

        @Override
        public String getValue() {
            return name_;
        }
    }

如您所见,这是一个非常简单明了的类。老实说,它更像是一个包装器,为什么 GWT 考虑将这些组合到一个特定的列标题单元格中,而不是注入一个通用单元格,这超出了我的理解。也许不是超级花哨,但我相信它会更容易使用

如果您在上面查看您的 updateColumns() 方法,您会看到它在添加列时创建了此 columnheader 类的新实例。还要确保你对静态和最终的内容非常准确,这样当你创建非常大的数据集时,你就不会破坏你的记忆...... IE 20 列的 1000 行是 20000 次调用或模板或您存储的成员的实例。因此,如果您的单元格或标题中的一个成员有 100 个字节,那么仅对于 CTOR 来说,这将变成大约 2MB 或更多的资源。同样,这并不像自定义数据单元格渲染那么重要,但它在您的标题中仍然很重要!!!

现在不要忘记添加你的 CSS

.gridData table {
    overflow: hidden;
    white-space: nowrap;
    table-layout: fixed;
    border-spacing: 0px;
}

.gridData table td {
    border: none;
    border-right: 1px solid #DBDBDB;
    border-bottom: 1px solid #DBDBDB;
    padding: 2px 9px
}

.gridContainer .filterContainer {
    position: relative;
    z-index: 1000;
    top: 28px;
}

.gridContainer .filterContainer td {
    padding: 0 13px 0 5px;
    width: auto;
    text-align: center;
}

.gridContainer .filterContainer .filterInput {
    width: 100%;
}

.gridData table .columnHeader {
    white-space: normal;
    vertical-align: bottom;
    text-align: center;
    background-color: #EEEEEE;
    border-right: 1px solid #D4D4D4;
}

.gridData table .columnHeader  div img {
    position: relative;
    top: -18px;
}

.gridData table .columnHeader .headerText {
    font-size: 90%;
    line-height: 92%;
}

.gridData table .columnHeader .headerFilter {
    visibility: hidden;
    height: 32px;
}

现在这就是你要添加的所有东西的css。我懒得把它分开,而且我认为你可以弄清楚。gridContainer 是包装数据网格的布局面板,而 gridData 是您的实际数据网格。

现在,当您编译时,您应该会在列标题文本下方看到一个空白。这是您将过滤器放置在使用 css 的位置

第 4 步:创建过滤器容器

现在我们需要一些东西来输入我们的过滤器输入。这个容器还应用了 css,它将向下移动到我们刚刚在标题中创建的空间中。是的,标题中的过滤器实际上和技术上不在标题中。这是避免排序事件问题和失去焦点问题的唯一方法

private HorizontalPanel filterContainer_ = new HorizontalPanel();

和你的过滤器初始化

public void initFilters() {
        filterContainer_.setStylePrimaryName("filterContainer");

        for (GridStringColumn<T> column : getColumns()) {
            if (!column.isHidden()) {
                Filter filterInput = new Filter(column);
                filters_.add(filterInput);
                filterContainer_.add(filterInput);
                filterContainer_.setCellWidth(filterInput, "auto");
            }
        }
    }

您可以看到它需要您的列集合才能正确创建进入容器的过滤器输入此外,过滤器类也会在列中传递,以便将列绑定到特定的过滤器。这允许您访问字段等

public class Filter extends TextBox {

        final private GridStringColumn<T> boundColumn_;

        public Filter(GridStringColumn<T> column) {
            super();
            boundColumn_ = column;
            addStyleName("filterInput " + boundColumn_.getDataStoreName());
            addKeyUpHandler(new KeyUpHandler() {

                @Override
                public void onKeyUp(KeyUpEvent event) {
                    filterTimer.cancel();
                    filterTimer.schedule(FILTER_DELAY);
                }
            });
        }

        public GridStringColumn<T> getBoundColumn() {
            return boundColumn_;
        }
    }

第 5 步:将组件添加到 LayoutPanel

现在,当您初始化网格以将寻呼机和网格添加到布局容器中时,我们不考虑过滤器通常应该占用的垂直高度。由于它被设置为相对位置,z-index 大于网格和列的位置,因此它实际上会出现在标题中。魔法!!!

public void setupDataGrid() {
        add(pagerContainer_);
        setWidgetTopHeight(pagerContainer_, 0, Unit.PX, PAGER_HEIGHT, Unit.PX);
        add(filterContainer_);
        setWidgetTopHeight(filterContainer_, PAGER_HEIGHT + FILTER_HEIGHT, Unit.PX, FILTER_HEIGHT, Unit.PX);
        add(dataGrid_);
        setWidgetTopHeight(dataGrid_, PAGER_HEIGHT, Unit.PX, ScreenManager.getScreenHeight() - PAGER_HEIGHT - BORDER_HEIGHT, Unit.PX);


        pager_.setVisible(true);
        dataGrid_.setVisible(true);
    }

现在对于一些常数

final private static int PAGER_HEIGHT = 32;
final private static int FILTER_HEIGHT = 32;
final private static int BORDER_HEIGHT = 2;

边框高度适用于您的应用程序可能具有的特定 css 样式,从技术上讲,这是确保一切紧密贴合的小块空间。

第 6 步:使用 CSS 魔法

将过滤器从上方放置到列上的特定 css 是这个

.gridContainer .filterContainer {
    position: relative;
    z-index: 1000;
    top: 28px;
}

这会将容器移动到列上并将层放置在标题上方

接下来我们需要确保 filterContainer 中的单元格与我们的数据网格中的单元格对齐

.gridContainer .filterContainer td {
    padding: 0 13px 0 5px;
    width: auto;
    text-align: center;
}

接下来确保我们的输入根据他们所在的容器单元的大小进行缩放

.gridContainer .filterContainer .filterInput {
    width: 100%;
}

最后,我们要将排序图像指示器向上移动,以便过滤器输入文本框不会隐藏它们

.gridData 表 .columnHeader div img { 位置:相对;顶部:-18px;}

现在,当您编译时,您应该会在列标题上看到过滤器。您可能需要调整 css 以使它们准确排列。这也假设您没有设置任何特殊的列宽。如果这样做,您将需要创建一些额外的功能来手动设置单元格大小并设置宽度以与列同步。为了理智,我已经省略了这一点。


*现在是休息时间了,你快到了!^ _ __ _ __ _ _^*


第 7 步和第 8 步:添加事件处理程序

这是简单的部分。如果您从上面查看过滤器类,请注意此方法主体

addKeyUpHandler(new KeyUpHandler() {

                @Override
                public void onKeyUp(KeyUpEvent event) {
                    filterTimer.cancel();
                    filterTimer.schedule(FILTER_DELAY);
                }
            });

创建您的过滤器计时器

private FilterTimer filterTimer = new FilterTimer();

确保在类主体的根目录中指定字段而不是内联字段。

private class FilterTimer extends Timer {

        @Override
        public void run() {
            updateDataList();
        }
    }

需要一个计时器,以便每次用户输入值时都不会触发事件。很多人添加了 onblur 或其他愚蠢的处理程序,但它毫无意义。用户一次只能将数据输入一个字段,因此它是一个静音点。只需使用 onKeyUp 处理程序..

第 9 步:更新您的网格

现在,当我们调用 updateDataList (也应该从您的 onRangeChanged 事件中调用(用于排序和数据加载)时,我们希望通过我们的过滤器集合来迭代用户输入的应用过滤器。我个人将所有请求参数存储到一个便于访问和更新的哈希图。然后只需将整个地图传递到我的请求引擎中,该引擎会执行您的 RPC 或 RequestFactory 工作

public void updateDataList() {
        initParameters();

        // required parameters controlled by datagrid
        parameters_.put("limit", limit_ + "");
        parameters_.put("offset", offset_ + "");

        // sort parameters
        if (sortField_.equals("") || sortOrder_.equals("")) {
            parameters_.remove("sortField");
            parameters_.remove("sortDir");
        } else {
            parameters_.put("sortField", sortField_);
            parameters_.put("sortDir", sortOrder_);
        }

        // filter parameters
        for (Filter filter : filters_) {
            if (!filter.getValue().equals("")) {
                CGlobal.LOG.info("filter: " + filter.getBoundColumn().getDataStoreName() + "=" + filter.getValue());
                parameters_.put(filter.getBoundColumn().getDataStoreName(), filter.getValue());
            }
        }

        RequestServiceAsync requestService = (RequestServiceAsync) GWT.create(RequestService.class);
        requestService.getRequest("RPC", getParameters(), new ProviderAsyncCallback());
    }

您可以看到我们需要如何以及为什么需要将过滤器绑定到列,因此当我们遍历过滤器时,我们可以获得存储的字段名称。通常我只是将字段名和过滤器值作为查询参数传递,而不是将所有过滤器作为单个过滤器查询参数传递。这是更可扩展的方式,并且很少有您的 db 列应该 == 查询参数(如上面的 sortDir 或 sortField )的保留字的情况。

*完成 < _ __ _ _>*


好吧,我希望这可以帮助大家了解一些高级 gwt 数据网格的东西。我知道创建自己很痛苦,所以希望这会在未来为大家节省大量时间。祝你好运!

于 2012-08-15T18:09:12.180 回答
0

在 Blogger 中,这些代码如下

<b:if  cond='data:blog.pageType == "static_page"'></b:if>

在您想在静态页面上显示的内容中查看更多信息[1]

于 2019-05-23T10:31:58.837 回答