OperaMasks2.0中的DataGrid之一:定义DataGrid


1. 新版DataGrid介绍
2. 简单的动态定义表格
2.1. 准备数据
2.2. 定义简单的动态表格
2.3. GridSelectionModel
2.4. 使用GridViewProvider进行定义
3. DataGrid的数据操作
3.1. GridDataProvider
3.2. GridRowDataProvider

1. 新版DataGrid介绍

OperaMasks2.0 Release版本对原有的DataGrid组件进行全面改进。新版的DataGrid修改了编程模型,为动态表格提供了更有力的支持和更清晰的数据模型。并且加入了对多表头的支持。下面我们将通过一系列例子来说明AOM2.0中新版DataGrid的用法。

2. 简单的动态定义表格

动态定义是指表格的展现模型完全是在后台的ManagedBean中使用编程方式进行定义,在页面上只需要加入一个标签说明表格所在的位置。动态定义是OperaMasks2.0中推荐的DataGrid定义方式,此方式更适合于应用中根据数据源或业务规则动态创建数据展现的场景,也便于用户自行开发或OperaMasks后续版本加入智能的适配器自动创建表格结构。

2.1. 准备数据

首先,我们先为示例准备一些用于显示的数据。在现实场景中,这些数据可能直接来至数据源,也可能是业务模型处理的结果。在本示例中,使用虚拟的数据来模拟从数据库中取数的过程,实际的应用中,可更替换为相应的取数代码,数据表格组件只关注如何展现数据,并不关注数据来源:
 
public class Quote {
     private String company;
     private double price;
     private double change;
     private Date lastUpdated;
     private String comment;

     public Quote(String company, double price) {
         this.company = company;
         this.price = price;
         this.change = 0;
         this.lastUpdated = new Date();
    }

     public String getCompany() {
         return company;
    }

     public void setCompany(String company) {
         this.company = company;
    }

     public double getPrice() {
         return price;
    }

     public void setPrice( double price) {
         this.price = price;
    }

     public double getChange() {
         return change;
    }

     public void setChange( double change) {
         this.change = change;
    }

     public double getPctChange() {
         return (change * 100) / price;
    }

     public Date getLastUpdated() {
         return lastUpdated;
    }

     public void setLastUpdated(Date lastUpdated) {
         this.lastUpdated = lastUpdated;
    }

     public String getComment() {
         return comment;
    }

     public void setComment(String comment) {
         this.comment = comment;
    }
}


 
以上是一个典型的数据对象,定义了一条记录的数据结构。现在我们来模拟一个取数过程:
 
@ManagedBean(scope= ManagedBeanScope.REQUEST)
public class StockBean
{
     private Quote[] stockData = new Quote[] {
         new Quote( "3m Co.", 71.72),
         new Quote( "Alcoa Inc", 29.01),
         new Quote( "Altria Group Inc.", 83.81),
         new Quote( "American Express Company", 52.55),
         new Quote( "American International Group, Inc.", 64.13),
         new Quote( "Apusic Systems, Inc.", 87.08),
         new Quote( "AT&T Inc.", 31.61),
         new Quote( "Boeing Co.", 75.43),
         new Quote( "Caterpillar Inc.", 67.27),
         new Quote( "Citigroup, Inc.", 49.37),
         new Quote( "E.I. du Pont de Nemours and Company", 40.48),
         new Quote( "Exxon Mobil Corp", 68.1),
         new Quote( "General Electric Company", 34.14),
         new Quote( "General Motors Corporation", 30.27),
         new Quote( "Hewlett-Packard Co.", 36.53),
         new Quote( "Honeywell Intl Inc.", 38.77),
         new Quote( "Intel Corporation", 19.88),
         new Quote( "International Business Machines", 81.41),
         new Quote( "Johnson & Johnson", 64.72),
         new Quote( "JP Morgan & Chase & Co", 45.73),
         new Quote( "McDonald's Corporation", 36.76),
         new Quote( "Merck & Co., Inc.", 40.96),
    };

     public Quote[] getStockData() {
         return stockData;
    }
}

 
以上ManagedBean模拟了一个取数过程,在本示例中,我们并不关心具体数值与取值算法,只需要知道通过#{StockBean.stockData}能获得一个Quote类型的数组就够了。需要注意的是,虽然本例中使用了数组类型(便于初始化),但事实上DataGrid也支持java.util.List或java.util.Set等类型。

2.2. 定义简单的动态表格

在定义DataGrid之前,我们先来看看AOM2.0中的DataGrid所使用的数据模型。下面是一个简单的静态类图:
我们暂时先不用关注RowDataProvider、GridViewProvider与GridSelectionModel,为了定义一个DataGrid模型,理论上至少需要提供三样信息:要展现的数据集(value)、表头模型(GridHeaderModel)、数据列模型(GridColumnModel)。
数据集我们之前已经准备好了,只需要在页面绑定的ManagedBean中加入以下代码进行绑定即可:
       
       
       
       
@ManagedBean(name = "simpleDynamicGridBean", scope = ManagedBeanScope.REQUEST)
public class SimpleDynamicGridBean implements Serializable {

    @Bind(id = "grid")   //对应页面的组件
    @ManagedProperty( "#{StockBean.stockData}")
    Quote[] data;
请注意一点,虽然使用UIDataGrid组件上的setValue()方法也可以达到绑定数据的效果。但是如果直接使用setValue()方法把数据集对象实例赋值给组件类,会导致数据对象与组件类一起被序列化,将会增加系统负担而且会影响性能。因此建议使用页面绑定或者IoVC绑定方式为组件绑定的value,这样在组件类中持有的只是ValueExpression对象,或者为组件类指定GridDataProvider(详见下文)。
现在我们来准备GridHeaderModel,我们可以使用下代码:
       
       
       
       
@Bind(id= "grid", attribute= "headerModel")
     private GridHeaderModel getHeaderModel() {
        GridHeaderModel headerModel = new GridHeaderModel();
        GridHeader header = new GridHeader();
        GridHeaderCell cell;
        cell = new GridHeaderCell();
        cell.setLabel( "公司");
        header.addCell(cell);
        cell = new GridHeaderCell();
        cell.setLabel( "价格");
        header.addCell(cell);
        cell = new GridHeaderCell();
        cell.setLabel( "变化");
        header.addCell(cell);
        cell = new GridHeaderCell();
        cell.setLabel( "百分比");
        header.addCell(cell);
        
        headerModel.addHeader(header);
         return headerModel;
    }
可以看出,一个GridHeaderModel实例可以持有一个或多个GridHeader,每个GridHeader对应表头中的一行。由于这里是简单的表头,我们只需要加入一个GridHeader就够了。而GridHeader也可以持有一个或多个GridHeaderCell,对应每一列的表头。
另外需要注意一点,由于组件状态等原因,现在并不支持直接使用UIDataGrid的setHeaderModel()方法直接设置表头模型,而需要使用@Bind标注或在页面写统一EL表达式等方式把GridHeaderModel实例绑定到“headerModel”属性上。
现在,表头模型就已经定义好了。但是仔细看看定义的代码,显得有些冗长。而且创建元素对象的动作与添加元素对象的动作分开,实际应用中往往引起遗漏。为此,我们也可以将代码改为另一种常用的初始化形式:
       
       
       
       
@Bind(id= "grid", attribute= "headerModel")
     private GridHeaderModel getHeaderModel() {
        GridHeaderModel headerModel = new GridHeaderModel();
        headerModel.addHeader( new GridHeader() {
            {
                GridHeaderCell cell;
                addCell(cell = new GridHeaderCell());
                cell.setLabel( "公司");
                addCell(cell = new GridHeaderCell());
                cell.setLabel( "价格");
                addCell(cell = new GridHeaderCell());
                cell.setLabel( "变化");
                addCell(cell = new GridHeaderCell());
                cell.setLabel( "百分比");
            }
        });

         return headerModel;
    }
现在初始化代码已经比原来简洁了不少,但显然,当需要对cell的属性作更详细的设置时,初始化代码仍然会变得十分冗长。为此,DataGrid还提供了一种DSL(Domain Specific Language)风格的初始化方法。可以将代码作如下修改:
       
       
       
       
@Bind(id= "grid", attribute= "headerModel")
     private GridHeaderModel getHeaderModel() {
         return new GridHeaderModel().headers(
                 new GridHeader().cells(
                         new GridHeaderCell().label( "公司")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "价格")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "变化")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "百分比")
                            .align(Align.CENTER)));
    }
可以看出,使用这种方式,现在用动态方式去定义一个静态表,并不会比在页面中使用标签定义麻烦多少。(注意,以上方式在2008年6月12日之后的SNAP版本中有效)
接下来,我们用同样方式定义数据列模型:
 
@Bind(id= "grid", attribute= "columnModel")
     private GridColumnModel getColumnModel() {
         return new GridColumnModel().columns(
                 new GridColumn( "company")
                    .align(Align.LEFT)
                    .width(200),
                 new GridColumn( "price")
                    .align(Align.RIGHT)
                    .width(30),
                 new GridColumn( "change")
                    .align(Align.RIGHT)
                    .width(30),
                 new GridColumn( "pctChange")
                    .align(Align.RIGHT)
                    .width(30));
    }
 
在服务器上运行页面,即可看到DataGrid的展现效果:
 
最后,我们回过头来看看展现一个简单数据表格的全部代码:
页面:
       
       
       
       
 
"SimpleGrid">
    "grid"
/>

ManagedBean:
 
@ManagedBean(name = "simpleDynamicGridBean", scope = ManagedBeanScope.REQUEST)
public class SimpleDynamicGridBean implements Serializable {

    @Bind(id = "grid")
    @ManagedProperty( "#{StockBean.stockData}")
    Quote[] data;

    @Bind(id= "grid", attribute= "columnModel")
     private GridColumnModel getColumnModel() {
         return new GridColumnModel().columns(
                 new GridColumn( "company")
                    .align(Align.LEFT)
                    .width(300),
                 new GridColumn( "price")
                    .align(Align.RIGHT)
                    .width(100),
                 new GridColumn( "change")
                    .align(Align.RIGHT)
                    .width(50),
                 new GridColumn( "pctChange")
                    .align(Align.RIGHT)
                    .width(50));
    }

    @Bind(id= "grid", attribute= "headerModel")
     private GridHeaderModel getHeaderModel() {
         return new GridHeaderModel().headers(
                 new GridHeader().cells(
                         new GridHeaderCell().label( "公司")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "价格")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "变化")
                            .align(Align.CENTER),
                         new GridHeaderCell().label( "百分比")
                            .align(Align.CENTER)));
    }
}

2.3. GridSelectionModel

让用户对表格中的元素进行选择是DataGrid的基本功能之一。可以通过为DataGrid的selectModel属性绑定不同的GridSelectionModel实例来指定选择模式。例如,在上面例子的代码中加入:
       
       
       
       

@Bind(id= "grid", attribute= "selectionModel")
     private GridSelectionModel getSelectionModel() {
         return new CellSelectionModel();
    }
即可将DataGrid设置为按单元格选择模式。
DataGrid目前支持以下选择模式:
  • RowSelectionModel : 按行选择模式(默认选择模式)。在每行左侧显示行号。允许使用ctrl或shift键加鼠标点击实现多选。
  • CheckboxSelectionModel : 带选择框的按行选择模式。在每行左侧显示选择框,表示当前行是否被选中。直接点击选择框即可多选,并在标题栏提供一个全选框可实现全选。
  • CellSelectionModel : 按单元格选择模式。在每行左侧显示行号。目前不支持多选。

2.4. 使用GridViewProvider进行定义

我们可以发现,事实上GridHeaderModel、GridColumnModel与GridSelectionModel三者确定了一个DataGrid的视图结构。在比较复杂的系统中,我们往往希望重用整个视图结构,而不用为每个相同结构的DataGrid分别绑定GridHeaderModel、GridColumnModel与GridSelectionModel。另外,在OperaMasks2.0中,DataGrid被设计为具有非常强的动态特性,它现在支持使用rebind()方法去改变DataGrid的视图结构,那么,我们会希望能够根据绑定的数据集不同而使用不同的视图结构。为了实现以上需求,我们可以使用绑定GridViewProvider接口的实现类实例的方式来定义DataGrid。
GridViewProvider接口非常直观,用户通过实现以下方法分别返回GridHeaderModel,GridColumnModel与GridSelectionModel实例:
       
       
       
       
public GridSelectionModel getSelectionModel(Object input);
     public GridColumnModel getColumnModel(Object input);
     public GridHeaderModel getHeaderModel(Object input);
传入的参数input为DataGrid所绑定的数据集,这样我们可以根据所绑定的数据类型,数据结构,甚至是数据内容去决定使用何种DataGrid的视图结构。
以下代码片段简单说明了如何动态改变DataGrid的视图结构:
 
@ManagedBean(scope=ManagedBeanScope.SESSION)
public class DynamicBindGridBean  
{
...
     //绑定初始数据
    @Bind(id= "grid", attribute= "value")
     private List data = GridBeanHelper.trainRecord;
    
     //绑定初始视图结构提供者
    @Bind(id= "grid", attribute= "viewProvider")
     private GridViewProvider provider = GridBeanHelper.getTrainViewProvider();

     //绑定组件类
    @Bind(id= "grid")
     private UIDataGrid grid;
...
     //绑定页面中的组件
     //重新绑定数据与GridViewProvider
    @Action
     public void train() {
        grid.setViewProvider(GridBeanHelper.getTrainViewProvider());
         this.data = GridBeanHelper.trainRecord;
        grid.setFirst(0);
        grid.setRows(4);
        grid.rebind();
    }

     //绑定页面中的组件
     //重新绑定另一套数据与GridViewProvider
    @Action
     public void stock() {
        grid.setViewProvider(GridBeanHelper.getStockViewProvider());
         this.data = Arrays.asList(GridBeanHelper.stockData);
        grid.setFirst(0);
        grid.setRows(10);
        grid.rebind();
    }
 
上面的代码片段摘录自OperaMasks2.0组件示例rcdemo的“动态绑定表格”部分,若希望了解运行效果与完整代码请访问 [url]http://www.operamasks.org/rcdemos/index.jsf[/url]

3. DataGrid的数据操作

绑定了数据集之后,为了将数据展现出来,DataGrid需要进行两种数据操作:第一是要从数据集中抽取数据,这个动作是由GridDataProvider完成的;第二是要向OperaMasks引擎提供待展现的单元格数据,这个动作是由GridRowDataProvider完成的。OperaMasks2.0提供了这两种Provider的默认实现,一般情况下用户只需绑定数据集,DataGrid就会自动选用合适的默认实现来进行处理。但用户也可以显式绑定这些provider来对DataGrid的行为进行更精细的控制。

3.1. GridDataProvider

GridDataProvider虚类是DataGrid提取数据的公共接口,它同时考虑了服务器端分页功能。实现类主要通过实现以下方法为DataGrid提供服务:
  • public abstract Object[] getElements();
    返回一个分页的数据集。原则上,此方法的实现应该返回从start开始的limit条记录。
  • public abstract Object getElement(int index);
    返回指定index的数据。
  • public abstract int getTotalCount();
    返回全部记录的总行数。
  • public void sort(GridColumn column, SortDirection direction);
    当DataGrid设置了remoteSort=true时,发起排序请求会调用到此方法。
  • public int getStart();
    返回当前分页所显示的第一条记录在数据集中的位置索引,数据集中第一条记录的索引为0。请注意这个getter对应的setStart()方法会由OperaMasks引擎调用,一般情况用户程序不应调用setStart()方法。
  • public int getLimit();
    返回当前分页每次装载的记录行数。

3.2. GridRowDataProvider

GridRowDataProvider接口是向DataGrid的单元格提供用于展现的数据的接口。这个接口只有一个方法:
  • public Object getLabel(Object rowData, GridColumn column);
    传入的rowData是DataGrid数据集中的一个元素,对应DataGrid中某一行的数据。作为GridRowDataProvider实现类的编写者,应该知道其实际类型。
    传入的column则是DataGrid中的一个GridColumn。
    以上两个参数实际上定位了DataGrid中的一个单元格,本方法返回这个单元格应显示的内容。如果返回值为String类型,则可以直接在DataGrid中显示。若返回值为其他类型,则应通过GridColumn的converter属性指定转换器将返回值转换为字符串类型。
若用户未指定GridRowDataProvider,则OperaMasks会使用一个默认的GridRowDataProvider实现:ValueExpressionRowDataProvider。它提取数据的规则是:
  • 若GridColumn指定了value属性(value属性可以包含统一EL表达式,且允许使用在DataGrid上通过var属性定义的变量名来引用rowData)。则返回value属性的求值结果。
  • 否则,以column的id值为属性名或键值,返回rowData中属性值或元素值。