【问题标题】:CellTable with custom Header containing SearchBox and Focus Problem带有包含 SearchBox 和焦点问题的自定义 Header 的 CellTable
【发布时间】:2011-09-19 08:32:57
【问题描述】:

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

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

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

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

这可能是相关的在 CellTable 刷新后调用自定义标题及其单元格的渲染方法这一事实。
有什么方法可以将焦点设置回 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));

【问题讨论】:

  • 这个问题有什么解决办法吗?
  • 好吧,我最终通过在 CellTable-Header above 添加单独的 SearchBoxes 并使用 CSS 将它们与 Header 对齐来解决这个问题。但是我认为 GWT 2.4 添加了一些关于 CellTable/DataGrid 的新功能,这可能会以原始方式解决它。
  • 如果你把 focus="true" 属性放在那些搜索框上会发生什么?
  • @milan:我试过了,我也试过设置 tabIndex,但没用。唯一有效的方法是安排一个延迟命令,该命令在重绘单元格表以设置焦点后触发。但是有明显的滞后。
  • @umit 很抱歉,但我没有其他方式可以联系到您,这是紧急情况。我有一个关于 gwt 本身的问题,请你帮帮我。问题是:stackoverflow.com/questions/20509894/…

标签: java gwt gwt-2.2-celltable


【解决方案1】:

瘦子

不幸的是,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>>();
}

要创建列,请创建在数据网格构造函数中调用的 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 table .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 数据网格的东西有帮助。我知道创建自己很痛苦,所以希望这会在未来为大家节省大量时间。祝你好运!

【讨论】:

  • 感谢详细的回答。我最终以某种类似的方式实现了,但是到目前为止还没有你的解决方案那么先进。然而,AFAIK GWT 2.5 将添加一些新类来构建那些高级自定义(标题)行。所以我认为 GWT 2.5 发布后会变得更容易
  • 不,我用 gwt2.5rc1 构建了这个。解决这个问题需要重写 gwt 如何创建和渲染单元格。如果有的话,我不会很快看到这种情况发生,因为这就是自 gwt 的第一个版本以来的情况 =/ 好吧,他们不称它为高级,哈哈
  • 这应该被授予最长答案徽章:)
  • 问一个很长的问题,你会得到一个很长的答案=)
【解决方案2】:

在 Blogger 中,这些代码如下

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

Content that you want to show on static pages[1] 上查看更多信息

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-07
    • 2023-03-16
    • 2014-04-28
    • 2011-07-03
    • 2020-03-25
    • 1970-01-01
    相关资源
    最近更新 更多