前面一篇直接使用了Myfaces中的两个Component完成了一个简单的分页,这里将会介绍一种On-demand loading的方法来进行分页,仅仅在需要数据的时候加载。
先来说一些题外话,为了实现这种方式的分页,公司里大约5-6个人做了半个多月的工作,扩展了dataTable,修改了dataScrollor,以及各种其他的方法,但是都不是很优雅。在上个月底的时候,在Myfaces的Mail List中也针对这个问题展开了一系列的讨论,最后有人总结了讨论中提出的比较好的方法,提出了以下的分页方法,也是目前实现的最为优雅的方法,也就是不对dataTable和dataScrollor做任何修改,仅仅通过扩展DataModel来实现分页。
DataModel
是一个抽象类,用于封装各种类型的数据源和数据对象的访问,JSF中dataTable中绑定的数据实际上被包装成了一个DataModel,以消除各种不同数据源和数据类型的复杂性,在前面一篇中我们访问数据库并拿到了一个List,交给dataTable,这时候,JSF会将这个List包装成
ListDataModel
,dataTable访问数据都是通过这个DataModel进行的,而不是直接使用List。
接下来我们要将需要的页的数据封装到一个DataPage中去,这个类表示了我们需要的一页的数据,里面包含有三个元素:datasetSize,startRow,和一个用于表示具体数据的List。datasetSize表示了这个记录集的总条数,查询数据的时候,使用同样的条件取count即可,startRow表示该页的起始行在数据库中所有记录集中的位置。
/** */
/**
* A simple class that represents a "page" of data out of a longer set, ie a
* list of objects together with info to indicate the starting row and the full
* size of the dataset. EJBs can return instances of this type when returning
* subsets of available data.
*/
public
class
DataPage

{
private
int
datasetSize;
private
int
startRow;
private
List data;


/** */
/**
* Create an object representing a sublist of a dataset.
*
*
@param
datasetSize
* is the total number of matching rows available.
*
*
@param
startRow
* is the index within the complete dataset of the first element
* in the data list.
*
*
@param
data
* is a list of consecutive objects from the dataset.
*/
public
DataPage(
int
datasetSize,
int
startRow, List data)

{
this
.datasetSize
=
datasetSize;
this
.startRow
=
startRow;
this
.data
=
data;
}
/** */
/**
* Return the number of items in the full dataset.
*/
public
int
getDatasetSize()

{
return
datasetSize;
}
/** */
/**
* Return the offset within the full dataset of the first element in the
* list held by this object.
*/
public
int
getStartRow()

{
return
startRow;
}
/** */
/**
* Return the list of objects held by this object, which is a continuous
* subset of the full dataset.
*/
public
List getData()

{
return
data;
}
}
接下来,我们要对DataModel进行封装,达到我们分页的要求。该DataModel仅仅持有了一页的数据DataPage,并在适当的时候加载数据,读取我们需要页的数据。
/** */
/**
* A special type of JSF DataModel to allow a datatable and datascroller to page
* through a large set of data without having to hold the entire set of data in
* memory at once.
* <p>
* Any time a managed bean wants to avoid holding an entire dataset, the managed
* bean should declare an inner class which extends this class and implements
* the fetchData method. This method is called as needed when the table requires
* data that isn't available in the current data page held by this object.
* <p>
* This does require the managed bean (and in general the business method that
* the managed bean uses) to provide the data wrapped in a DataPage object that
* provides info on the full size of the dataset.
*/
public
abstract
class
PagedListDataModel
extends
DataModel

{
int
pageSize;
int
rowIndex;
DataPage page;


/** */
/**
* Create a datamodel that pages through the data showing the specified
* number of rows on each page.
*/
public
PagedListDataModel(
int
pageSize)

{
super
();
this
.pageSize
=
pageSize;
this
.rowIndex
=
-
1
;
this
.page
=
null
;
}
/** */
/**
* Not used in this class; data is fetched via a callback to the fetchData
* method rather than by explicitly assigning a list.
*/
public
void
setWrappedData(Object o)

{
if
(o
instanceof
DataPage)

{
this
.page
=
(DataPage) o;
}
else
{
throw
new
UnsupportedOperationException(
"
setWrappedData
"
);
}
}
public
int
getRowIndex()

{
return
rowIndex;
}
/** */
/**
* Specify what the "current row" within the dataset is. Note that the
* UIData component will repeatedly call this method followed by getRowData
* to obtain the objects to render in the table.
*/
public
void
setRowIndex(
int
index)

{
rowIndex
=
index;
}
/** */
/**
* Return the total number of rows of data available (not just the number of
* rows in the current page!).
*/
public
int
getRowCount()

{
return
getPage().getDatasetSize();
}
/** */
/**
* Return a DataPage object; if one is not currently available then fetch
* one. Note that this doesn't ensure that the datapage returned includes
* the current rowIndex row; see getRowData.
*/
private
DataPage getPage()

{
if
(page
!=
null
)

{
return
page;
}
int
rowIndex
=
getRowIndex();
int
startRow
=
rowIndex;
if
(rowIndex
==
-
1
)

{
//
even when no row is selected, we still need a page
//
object so that we know the amount of data available.
startRow
=
0
;
}
//
invoke method on enclosing class
page
=
fetchPage(startRow, pageSize);
return
page;
}
/** */
/**
* Return the object corresponding to the current rowIndex. If the DataPage
* object currently cached doesn't include that index then fetchPage is
* called to retrieve the appropriate page.
*/
public
Object getRowData()

{
if
(rowIndex
<
0
)

{
throw
new
IllegalArgumentException(
"
Invalid rowIndex for PagedListDataModel; not within page
"
);
}
//
ensure page exists; if rowIndex is beyond dataset size, then
//
we should still get back a DataPage object with the dataset size
//
in it
if
(page
==
null
)

{
page
=
fetchPage(rowIndex, pageSize);
}
int
datasetSize
=
page.getDatasetSize();
int
startRow
=
page.getStartRow();
int
nRows
=
page.getData().size();
int
endRow
=
startRow
+
nRows;

if
(rowIndex
>=
datasetSize)

{
throw
new
IllegalArgumentException(
"
Invalid rowIndex
"
);
}
if
(rowIndex
<
startRow)

{
page
=
fetchPage(rowIndex, pageSize);
startRow
=
page.getStartRow();
}
else
if
(rowIndex
>=
endRow)

{
page
=
fetchPage(rowIndex, pageSize);
startRow
=
page.getStartRow();
}
return
page.getData().get(rowIndex
-
startRow);
}
public
Object getWrappedData()

{
return
page.getData();
}
/** */
/**
* Return true if the rowIndex value is currently set to a value that
* matches some element in the dataset. Note that it may match a row that is
* not in the currently cached DataPage; if so then when getRowData is
* called the required DataPage will be fetched by calling fetchData.
*/
public
boolean
isRowAvailable()

{
DataPage page
=
getPage();
if
(page
==
null
)

{
return
false
;
}
int
rowIndex
=
getRowIndex();
if
(rowIndex
<
0
)

{
return
false
;
}
else
if
(rowIndex
>=
page.getDatasetSize())

{
return
false
;
}
else
{
return
true
;
}
}
/** */
/**
* Method which must be implemented in cooperation with the managed bean
* class to fetch data on demand.
*/
public
abstract
DataPage fetchPage(
int
startRow,
int
pageSize);
}
最后,我们需要在Backing Bean中加一些东西,调用业务逻辑,并将数据交给PagedListDataModel,来帮我们完成最后的分页工作。
public
SomeManagedBean
{
.



private
DataPage getDataPage(
int
startRow,
int
pageSize)
{
//
access database here, or call EJB to do so
}
public
DataModel getDataModel()
{

if
(dataModel
==
null
)
{
dataModel
=
new
LocalDataModel(20);
}
return
dataModel;
}
private
class
LocalDataModel
extends
PagedListDataModel
{

public
LocalDataModel(
int
pageSize)
{
super
(pageSize);
}

public
DataPage fetchPage(
int
startRow,
int
pageSize)
{
//
call enclosing managed bean method to fetch the data
return
getDataPage(startRow, pageSize);
}
}
这里面有一个getDataPage的方法,只需要把所有业务逻辑的调用放在这里就可以了,最后业务逻辑调用的结果返回一个List,总条数返回一个int型的count放到DataPage中去就可以了。
为了实现复用,把上面第三段的代码中的LocalDataModel类和getDataPage方法抽到BasePagedBackingBean中,把getDataPage方法改成:
protected abstract DataPage getDataPage(int startRow, int pageSize);
这样我们把所有需要分页的Backing Bean继承自这个抽象类,并实现getDataPage方法即可很容易的实现分页。