RichFaces フォーラムで話し合った後、次の解決策が思いつきました (Brendan Healey に感謝します)。
RichLazyDataModel.java
/**
* Extended data model
* @author Brendan Healey
*/
public abstract class RichLazyDataModel<T> extends ExtendedDataModel<T> {
private SequenceRange cachedRange;
private Integer cachedRowCount;
private List<T> cachedList;
private Object rowKey;
public abstract List<T> getDataList(int firstRow, int numRows);
public abstract Object getKey(T t);
public abstract int getTotalCount();
@Override
public void walk(FacesContext ctx, DataVisitor dv, Range range, Object argument) {
SequenceRange sr = (SequenceRange) range;
if (cachedList == null || !equalRanges(cachedRange, sr)) {
cachedList = getDataList(sr.getFirstRow(), sr.getRows());
cachedRange = sr;
}
for (T t : cachedList) {
if (getKey(t) == null) {
/*
* the 2nd param is used to build the client id of the table
* row, i.e. mytable:234:inputname, so don't let it be null.
*/
throw new IllegalStateException("found null key");
}
dv.process(ctx, getKey(t), argument);
}
}
/*
* The rowKey is the id from getKey, presumably obtained from
* dv.process(...).
*/
@Override
public void setRowKey(Object rowKey) {
this.rowKey = rowKey;
}
@Override
public Object getRowKey() {
return rowKey;
}
@Override
public boolean isRowAvailable() {
return (getRowData() != null);
}
@Override
public int getRowCount() {
if (cachedRowCount == null) {
cachedRowCount = getTotalCount();
}
return cachedRowCount;
}
@Override
public T getRowData() {
for (T t : cachedList) {
if (getKey(t).equals(this.getRowKey())) {
return t;
}
}
return null;
}
protected static boolean equalRanges(SequenceRange range1, SequenceRange range2) {
if (range1 == null || range2 == null) {
return range1 == null && range2 == null;
} else {
return range1.getFirstRow() == range2.getFirstRow() && range1.getRows() == range2.getRows();
}
}
/*
* get/setRowIndex are used when doing multiple select in an
* extendedDataTable, apparently. Not tested. Actually, the get method is
* used when using iterationStatusVar="it" & #{it.index}.
*/
@Override
public int getRowIndex() {
if (cachedList != null) {
ListIterator<T> it = cachedList.listIterator();
while (it.hasNext()) {
T t = it.next();
if (getKey(t).equals(this.getRowKey())) {
return it.previousIndex() + cachedRange.getFirstRow();
}
}
}
return -1;
}
@Override
public void setRowIndex(int rowIndex) {
int upperBound = cachedRange.getFirstRow() + cachedRange.getRows();
if (rowIndex >= cachedRange.getFirstRow() && rowIndex < upperBound) {
int index = rowIndex % cachedRange.getRows();
T t = cachedList.get(index);
setRowKey(getKey(t));
}
}
@Override
public Object getWrappedData() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void setWrappedData(Object data) {
throw new UnsupportedOperationException("Not supported yet.");
}
public List<T> getCachedList() {
return cachedList;
}
}
ListState.java
/**
* Holds list state
*/
public class ListState implements Serializable {
private int page;
private Map<String, Serializable> searchCriteria = new HashMap<String, Serializable>();
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public Map<String,Serializable> getSearchCriteria() {
return searchCriteria;
}
}
CardsBean.java
@ManagedBean(name="cardsBean")
public class CardsBean {
@ManagedProperty("#{cardService}")
private CardService cardService;
private ListState state;
private RichLazyDataModel<Card> cardsModel = new RichLazyDataModel<Card>() {
@Override
public List<Card> getDataList(int firstRow, int numRows) {
MyUserDetails user = SecurityUtils.getCurrentUser();
return cardService.findUserCards(user.getUser(), firstRow, numRows, state.getSearchCriteria());
}
@Override
public Object getKey(Card card) {
return card.getId();
}
@Override
public int getTotalCount() {
MyUserDetails user = SecurityUtils.getCurrentUser();
return cardService.countUserCards(user.getUser(), state.getSearchCriteria());
}
};
public RichLazyDataModel<Card> getCards() {
return cardsModel;
}
public String getSearchString() {
return (String)state.getSearchCriteria().get("searchString");
}
public int getCurrentPage() {
return state.getPage();
}
public void setCurrentPage(int page) {
state.setPage(page);
}
public void setSearchString(String searchString) {
state.getSearchCriteria().put("searchString", searchString);
}
public void setCardService(CardService cardService) {
this.cardService = cardService;
}
public boolean isPinned() {
return Boolean.TRUE.equals(state.getSearchCriteria().get("pinned"));
}
public void setPinned(boolean pinned) {
state.getSearchCriteria().put("pinned", pinned);
}
public void togglePinned() {
setPinned(!isPinned());
}
@PostConstruct
public void init() {
state = getFromSession("cardsList", null);
if (state == null) {
state = new ListState();
storeInSession("cardsList", state);
}
}
public <T extends Serializable> T getFromSession(String name, T defaultValue) {
T ret = (T) FacesContext.getCurrentInstance().getExternalContext().getSessionMap().get(name);
if (ret == null) {
ret = defaultValue;
}
return ret;
}
public void storeInSession(String name, Serializable obj) {
FacesContext.getCurrentInstance().getExternalContext().getSessionMap().put(name, obj);
}
}
cards.xhtml (部分)
...
<h:form>
<rich:dataGrid value="#{cardsBean.cards}" var="card" columns="2" elements="20" first="#{cardsBean.currentPage}" style="margin:0 auto;width:70em" id="cardsTable">
<f:facet name="header">
<h:inputText value="#{cardsBean.searchString}">
<a4j:ajax event="keyup" render="cardsTable@body, cardsTable@footer">
<a4j:attachQueue requestDelay="700" ignoreDupResponses="true" />
</a4j:ajax>
</h:inputText>
</f:facet>
<rich:panel id="cd">
<ui:include src="WEB-INF/parts/card.xhtml">
<ui:param name="card" value="#{card}"/>
</ui:include>
</rich:panel>
<f:facet name="footer">
<rich:dataScroller page="#{cardsBean.currentPage}" />
</f:facet>
</rich:dataGrid>
</h:form>
...