Tuesday, April 29, 2008

Partial ajax update capable list view

interview-how-wicket-does-ajax

I read this interview i think yesterday or day before and something which eelco said actually helped-
"Eelco. The fact that you can encapsulate Ajax in self contained components. My favorite part of Wicket's Ajax is really how easy it is to replace a component on the fly. I typically create the Ajax components I need from scratch (base classes rather)."

I was struggling to think how i could write a list view which was capable of pagination via ajax, only repainting the new items. Today I dared to try from the scratch approach--extending from RefreshingView and I could design few classes which actually can do so. Not only that, but I have tested them and they seem to work perfectly fine.

The AjaxEndlessPageableView and AjaxEndlessDataView I provide here are created looking at code of existing components. They complement the OnScrollEndUpdaterBehaviour in case one wants to implement endless navigation.

(1) The Interface IAjaxPageable is at the top of this class hirarchy




import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.navigation.paging.IPageable;

public interface IAjaxPageable extends IPageable
{

/**
* Sets the page/viewincrement that should be rendered.
*
* @param page
* The page that should be rendered.
*/
void setCurrentPage(int page,AjaxRequestTarget target);


}
(2)The AjaxPageableView--note that we talk in terms of view increments rather pages (decrements are not incorporated in here yet)





import java.util.Iterator;
import java.util.NoSuchElementException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.RefreshingView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.version.undo.Change;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* setCurrentView method can be used to increment the view ajax.
*
*
*/
public abstract class AjaxEndlessPageableView extends RefreshingView implements IAjaxPageable
{
/**
* items shown on first page initially, initially set to Integer.MAX_VALUE
* meaning everything shown on the same page and no extra paging views.
*/
private int itemsOnStartPage = Integer.MAX_VALUE;
protected static Logger log = LoggerFactory.getLogger(AjaxEndlessPageableView.class);

/**
* Items for each view increment initially set to 0 meaning no more paging
* views
*/
private int itemsPerViewIncrement = 0;

private int currentViewNumber = 0;

/**
* <code>cachedItemCount</code> is used to cache the call to
* <code>internalGetItemCount()</code> for the duration of the request
* because that call can potentially be expensive ( a select count query )
* and so we do not want to execute it multiple times.
*/
private int cachedItemCount;

public AjaxEndlessPageableView(String id, IModel model, int itemsOnStartPage)
{
super(id, model);
clearCachedItemCount();
this.itemsOnStartPage = itemsOnStartPage;
}

public AjaxEndlessPageableView(String id, int itemsOnStartPage)
{
super(id);
clearCachedItemCount();
this.itemsOnStartPage = itemsOnStartPage;
}

public AjaxEndlessPageableView(String id, IModel model)
{
super(id, model);
clearCachedItemCount();
}

public AjaxEndlessPageableView(String id)
{
super(id);
clearCachedItemCount();
}

private void clearCachedItemCount()
{
cachedItemCount = -1;
}

private void setCachedItemCount(int itemCount)
{
cachedItemCount = itemCount;
}

private int getCachedItemCount()
{
if (cachedItemCount < 0)
{
throw new IllegalStateException("getItemCountCache() called when cache was not set");
}
return cachedItemCount;
}

private boolean isItemCountCached()
{
return cachedItemCount >= 0;
}

@Override
protected Iterator getItemModels()
{
int offset = getViewOffset();
int size = getViewSize();
Iterator models = getItemModelsInCurrentViewIncrement(offset, size);
models = new CappedIteratorAdapter(models, size);

return models;
}

private boolean isOnStartPage()
{
return getCurrentPage() == 0;
}

/**
* @return the number of items visible
*/
protected int getViewSize()
{
if (isOnStartPage())
{
return internalGetItemsOnStartPage();
}
else
{
return Math.min(internalGetItemsPerViewIncrement(), getRowCount() - getViewOffset());
}
}

@Override
protected void onDetach()
{
clearCachedItemCount();
super.onDetach();
}

/**
* @return the index of the first visible item
*/
protected int getViewOffset()
{
if (isOnStartPage())
{
return 0;
}
else
{
return internalGetItemsOnStartPage() + internalGetItemsPerViewIncrement()
* getPreviousPage();
}
}


/**
* Returns an iterator over models for items in the incremental view
*
* @param offset
* index of first new item in this view increment
* @param size
* number of items that will be showing in the current view
* increment
* @return an iterator over models for items in the current view increment
*/
protected abstract Iterator getItemModelsInCurrentViewIncrement(int offset, int size);

/**
* This call is very costly never ever use it directly. use getRowCount()
*
* @return total item count
*/
protected abstract int internalGetItemCount();

/**
* @return total item count
*/
public final int getRowCount()
{
if (!isVisibleInHierarchy())
{
return 0;
}

if (isItemCountCached())
{
return getCachedItemCount();
}

int count = internalGetItemCount();

setCachedItemCount(count);

return count;
}


@Override
protected abstract void populateItem(Item item);

protected abstract String getJavaScriptToRenderNewDomElement(Item item);

/**
* returns the current view number starting from the start page view number
* which is 0
*
* @see org.apache.wicket.markup.html.navigation.paging.IAjaxPageable#getCurrentPage()
*/
public final int getCurrentPage()
{
int view = currentViewNumber;

/*
* trim current page if its out of bounds this can happen if items are
* deleted between requests
*/

if (view >= getPageCount())
{
view = Math.max(getPageCount() - 1, 0);
setCurrentPage(view);
return view;
}

return view;
}

/**
* returns the previous view number
*
* @see org.apache.wicket.markup.html.navigation.paging.IAjaxPageable#getCurrentPage()
*/
public final int getPreviousPage()
{
return getCurrentPage() - 1;
}

public int getPageCount()
{
int total = getRowCount();
int itemsPerView = internalGetItemsPerViewIncrement();
int itemsOnStartPage = internalGetItemsOnStartPage();
int countIncrementalViews = (total - itemsOnStartPage) / itemsPerView;
int count = countIncrementalViews + 1;// add 1 for start page

if (itemsPerView * countIncrementalViews + itemsOnStartPage < total)
{
count++;
}

return count;
}


/**
* @see org.apache.wicket.markup.html.navigation.paging.IAjaxPageable#setCurrentPage(int)
*/
public final void setCurrentPage(int view)
{
if (view < 0 || (view >= getPageCount() && getPageCount() > 0))
{
throw new IndexOutOfBoundsException("argument [view]=" + view + ", must be 0<=view<"
+ getPageCount());
}

if (currentViewNumber != view)
{
if (isVersioned())
{
addStateChange(new Change()
{
private static final long serialVersionUID = 1L;

private final int old = currentViewNumber;

@Override
public void undo()
{
currentViewNumber = old;
}

@Override
public String toString()
{
return "CurrentViewChange[component: " + getPath() + ", currentView: "
+ old + "]";
}
});

}
}
currentViewNumber = view;
}


/**
* @see org.apache.wicket.markup.html.navigation.paging.IAjaxPageable#setCurrentPage(int,AjaxRequestTarget)
*/
public final void setCurrentPage(int view, AjaxRequestTarget target)
{
setCurrentPage(view);
if (view == 0)
{
target.addComponent(AjaxEndlessPageableView.this);
}
else
{
int itemCountLast = getViewOffset();
for (Iterator it = getItemModels(); it.hasNext();)
{
String id = newChildId();
IModel model = (IModel)it.next();
Item item = newItem(id, itemCountLast, model);
populateItem(item);
item.setOutputMarkupId(true);
add(item);
target.prependJavascript(getJavaScriptToRenderNewDomElement(item));
target.addComponent(item);
itemCountLast++;
}
}
}

public final void nextPage(AjaxRequestTarget target)
{
setCurrentPage(getCurrentPage() + 1, target);
}

public final boolean hasMore()
{
return getViewSize() + getViewOffset() != getRowCount();
}


protected final int internalGetItemsOnStartPage()
{
int totalSize = getRowCount();
return (itemsOnStartPage == Integer.MAX_VALUE || itemsOnStartPage > totalSize)
? totalSize
: itemsOnStartPage;
}

protected final void internalSetItemsOnStartPage(int items, AjaxRequestTarget target)
{
if (items < 1)
{
throw new IllegalArgumentException("Argument [itemsPerPage] cannot be less than 1");
}

if (itemsOnStartPage != items)
{
if (isVersioned())
{
addStateChange(new Change()
{
private static final long serialVersionUID = 1L;

final int old = itemsOnStartPage;

@Override
public void undo()
{
itemsOnStartPage = old;
}

@Override
public String toString()
{
return "ItemsPerPageChange[component: " + getPath() + ", itemsPerPage: "
+ old + "]";
}
});
}
}

itemsOnStartPage = items;

// because items per page can effect the total number of pages and also
// the view on the first page
setCurrentPage(0, target);

}

public int internalGetItemsPerViewIncrement()
{
return itemsPerViewIncrement;
}

@Override
protected void onBeforeRender()
{
if (!(AjaxRequestTarget.class.isAssignableFrom(getRequestCycle().getRequestTarget()
.getClass())))
{
setCurrentPage(0);
}
super.onBeforeRender();
}

public void internalSetItemsPerViewIncrement(int items)
{
if (items < 1)
{
throw new IllegalArgumentException("Argument [itemsPerPage] cannot be less than 1");
}

if (itemsPerViewIncrement != items)
{
if (isVersioned())
{
addStateChange(new Change()
{
private static final long serialVersionUID = 1L;

final int old = itemsPerViewIncrement;

@Override
public void undo()
{
itemsPerViewIncrement = old;
}

@Override
public String toString()
{
return "ItemsPerPageChange[component: " + getPath() + ", itemsPerPage: "
+ old + "]";
}
});
}
}

itemsPerViewIncrement = items;
}

// /////////////////////////////////////////////////////////////////////////
// HELPER CLASSES
// /////////////////////////////////////////////////////////////////////////

/**
* Iterator adapter that makes sure only the specified max number of items
* can be accessed from its delegate.
*/
private static class CappedIteratorAdapter implements Iterator
{
private final int max;
private int index;
private final Iterator delegate;

/**
* Constructor
*
* @param delegate
* delegate iterator
* @param max
* maximum number of items that can be accessed.
*/
public CappedIteratorAdapter(Iterator delegate, int max)
{
this.delegate = delegate;
this.max = max;
}

/**
* @see java.util.Iterator#remove()
*/
public void remove()
{
throw new UnsupportedOperationException();
}

/**
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext()
{
return (index < max) && delegate.hasNext();
}

/**
* @see java.util.Iterator#next()
*/
public Object next()
{
if (index >= max)
{
throw new NoSuchElementException();
}
index++;
return delegate.next();
}

};

}

(3)The Ajax data view..
How to use this data view::
a)@Override getJavaScriptToRenderNewDomElement
b).setRowsPerViewIncrement to your taste
c)dataview.hasMore() tells if there are more rows to show up in next increment
d)dataview.nextPage(ajaxRequestTarget) appends the new rows in the end, without refreshing the whole view





import java.util.Iterator;
import org.apache.wicket.markup.repeater.data.IDataProvider;
import org.apache.wicket.model.IDetachable;

/**
* DataGridView hierarchy can directly extend from this one.
*/
public abstract class AjaxEndlessDataView extends AjaxEndlessPageableView
{

public AjaxEndlessDataView(String id, int itemsOnStartPage, IDataProvider dataProvider)
{
super(id, itemsOnStartPage);
if (dataProvider == null)
{
throw new IllegalArgumentException("argument [dataProvider] cannot be null");
}
this.dataProvider = dataProvider;
}

public AjaxEndlessDataView(String id, IDataProvider dataProvider)
{
super(id);
if (dataProvider == null)
{
throw new IllegalArgumentException("argument [dataProvider] cannot be null");
}
this.dataProvider = dataProvider;
}


@Override
protected Iterator getItemModelsInCurrentViewIncrement(int offset, int size)
{
return new ModelIterator(internalGetDataProvider(), offset, size);
}


private IDataProvider dataProvider;


/**
* @return data provider associated with this view
*/
protected final IDataProvider internalGetDataProvider()
{
return dataProvider;
}


/**
* Helper class that converts input from IDataProvider to an iterator over
* view items.
*
* @author Igor Vaynberg (ivaynberg)
*
*/
private static final class ModelIterator implements Iterator
{
private Iterator items;
private IDataProvider dataProvider;
private int max;
private int index;

/**
* Constructor
*
* @param dataProvider
* data provider
* @param offset
* index of first item
* @param count
* max number of items to return
*/
public ModelIterator(IDataProvider dataProvider, int offset, int count)
{
this.items = dataProvider.iterator(offset, count);
this.dataProvider = dataProvider;
this.max = count;
}

/**
* @see java.util.Iterator#remove()
*/
public void remove()
{
throw new UnsupportedOperationException();
}

/**
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext()
{
return items.hasNext() && (index < max);
}

/**
* @see java.util.Iterator#next()
*/
public Object next()
{
index++;
return dataProvider.model(items.next());
}
}

protected final int internalGetItemCount()
{
return internalGetDataProvider().size();
}

/**
* @see org.apache.wicket.markup.repeater.AbstractPageableView#onDetach()
*/
protected void onDetach()
{
super.onDetach();
if (dataProvider instanceof IDetachable)
{
((IDetachable)dataProvider).detach();
}
}

/**
* Sets the number of items to be displayed per view increment
*
* @param items
* number of items to display per view increment
*/
public void setRowsPerViewIncrement(int items)
{
internalSetItemsPerViewIncrement(items);
}

/**
* @return number of items displayed per view increment
*/
public int getRowsPerViewIncrement()
{
return internalGetItemsPerViewIncrement();
}


}

Friday, April 11, 2008

Wicket is Simple

Wicket is simple. It took some time for new people working with me to understand. They joined our project a few weeks back. But I found myself incapable of briefing them in a way they could understand. Rather each of them had to have worked actually on wicket to know that. And thats what happened.
One of them told me(After a month of hands on I guess) that wicket was basically made of IModels made of logic which sets and gets values from inputs.It's like the Model is mashed up so really well with the user interaction. Each input can be designed in to a component the way you want. Biggest point, each Componet can be made custom just like that .You could design custom user interactions by just extending from the Component hierarchy. And then there are Behaviors, which can manipulate how a component behaves. All this really as simple as knowing plain object oriented concepts. And what I feel is people who at one/another time wanted to code in core java should try wicket out for themselves. I feel wicket is ready for making/proving itself into a very very useful user-interface api.

Saturday, January 12, 2008

Implementing Endless Pageless Behaviour in wicket

Note:: I have edited the original code bit to support any element like div/table in addition to page.
Endless behavior of a page means you keep scrolling down and the page keeps loading the next view in the same page.
This has been used at many websites as a substitute for pagination and users are happy too.
An example is thoof .
I found the required javascript code at unspace website .

Today what I wanted to do was to implement such a behavior in wicket so that it can be added into any component, and customized.
There are only few things about the behavior you might want to change :

  1. how much distance before reaching the end of page you want to start an Ajax request.
  2. when are you out of further view? i.e. you don't want to load more view into your page once you are out of data.
Some Notes :
  1. The method of implementation is to check at regular intervals if you have reached towards the end of page scrolling.One should have a close look at
    hasMoreViewIncrementsIndicatorComponentId().In this method one should return the
    markup id of a hidden field. Also the model of the hidden field should be boolean. The hidden field repainted on every request to indicate if we have reached the end of the page or element.
  2. Also I would like to mention the use of WicketAjaxDebug in javascript. It will need you to turn on the wicket-ajax debug. Once turned on a link to ajax-debug window will be seen on page, and then it turns out to be really helpful in javascript debugging.


It turned out really very straightforward implementing the behavior. This behavior will send out Ajax request as soon as user is about to reach end of the page.

This is what the code finally looks like:




import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
import org.apache.wicket.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class OnScrollEndUpdaterBahaviour extends AbstractAjaxTimerBehavior
{
private final int preLoadDistance;
private final Duration updateInterval;
protected static Logger log = LoggerFactory.getLogger(OnScrollEndUpdaterBahaviour.class);
private PageOrElement pgOrElement;
private String remainingScrollHeightFunction = "";

public enum PageOrElement {
PAGE, ELEMENT
};

public OnScrollEndUpdaterBahaviour(Duration updateInterval, int preLoadDistance,
PageOrElement pgOrElement)
{
super(updateInterval);
this.pgOrElement = pgOrElement;
this.preLoadDistance = preLoadDistance;
this.updateInterval = updateInterval;

}

@Override
protected void onBind()
{
switch (pgOrElement)
{
case PAGE :
remainingScrollHeightFunction = "getRemainingScrollHeight()";
break;
case ELEMENT :
remainingScrollHeightFunction = "getRemainingScrollHeight('"
+ getComponent().getMarkupId() + "')";
break;
}
super.onBind();
}

public OnScrollEndUpdaterBahaviour(int preLoadDistance, PageOrElement pgOrElement)
{
this(Duration.milliseconds(200), preLoadDistance, pgOrElement);
}

private static final JavascriptResourceReference PAGEUTILJS = new JavascriptResourceReference(
OnScrollEndUpdaterBahaviour.class, "pageutil.js");


@Override
public void renderHead(IHeaderResponse response)
{
super.renderHead(response);
response.renderJavascript("var preloadDistance = " + preLoadDistance + ";\n"
+ "var mouseState = 'up';\n" + "var hasMoreViewIncrements;"
+ "var canMakeAjaxCall= true;" + "\n" + "function onMouseDown(){\n"
+ "mouseState = 'down';\n" + "}\n" + "function onMouseUp(){\n"
+ "mouseState = 'up';\n" + "}\n" + "document.onmousedown = onMouseDown;\n"
+ "document.onmouseup = onMouseUp;\n" + "function pgTOCallBack(){"
+ getJsTimeoutCall(updateInterval) + "}", null);
response.renderJavascriptReference(PAGEUTILJS);
}

@Override
protected CharSequence getPreconditionScript()
{

return // can i make an ajax call?
"hasMoreViewIncrements=$('"
+ hasMoreViewIncrementsIndicatorComponentId()
+ "').value;"
+ "canMakeAjaxCall=( "
+ "mouseState == 'up' "
+ "&& ("
+ remainingScrollHeightFunction
+ " < preloadDistance ) "
+ "&& hasMoreViewIncrements=='true'"
+ ");"
+
// lets put some debug info
"WicketAjaxDebug.logInfo(' mouseState '+mouseState);"
+ "WicketAjaxDebug.logInfo(' getPageHeight() '+getPageHeight());"
+ "WicketAjaxDebug.logInfo('getScrollHeight()'+getScrollHeight());"
+ "WicketAjaxDebug.logInfo(' preloadDistance '+preloadDistance);"
+ "WicketAjaxDebug.logInfo(' hasMoreViewIncrements '+hasMoreViewIncrements);"
+
// if can make ajax call
"if(canMakeAjaxCall)" + "{" + "Wicket.Focus.lastFocusId='"
+ requestInProgressIndicatorComponentId() + "';"
+ "wicketShow('"
+ requestInProgressIndicatorComponentId()
+ "');"
+
// else set up another timeout
"}else{" + "wicketHide('" + requestInProgressIndicatorComponentId() + "');"
+ "pgTOCallBack();" + "}"
+ "WicketAjaxDebug.logInfo(' canMakeAjaxCall '+canMakeAjaxCall);"
+ "return canMakeAjaxCall;";
// && {"+super.getPreconditionScript()+"}
}

protected abstract String hasMoreViewIncrementsIndicatorComponentId();

protected abstract String requestInProgressIndicatorComponentId();

// doing this for better readability
@Override
protected void onTimer(AjaxRequestTarget target)
{
onPageEnd(target);
};

protected abstract void onPageEnd(AjaxRequestTarget target);

}


================================================================================
How to use the behavior:
component.add(new OnScrollEndUpdaterBahaviour(Duration.milliseconds(200), 50,
PageOrElement.ELEMENT)
{
@Override
protected void onPageEnd(AjaxRequestTarget target)
{
//also refresh the component you need to refresh
target.addComponent(hasMoreField);
}

@Override
protected String hasMoreViewIncrementsIndicatorComponentId()
{
return hasMoreField.getMarkupId();
}

@Override
protected String requestInProgressIndicatorComponentId()
{
return indicator.getMarkupId();
}
});
indicator.setOutputMarkupId(true);
================================================================================
pageutil.js
===========

function getPageHeight(){
var y;
var test1 = document.body.scrollHeight;
var test2 = document.body.offsetHeight
if (test1 > test2) {
y = document.body.scrollHeight;
} else {
y = document.body.offsetHeight;
}
return parseInt(y);
}

function _getWindowHeight(){
if (self.innerHeight) {
frameHeight = self.innerHeight;
} else if (document.documentElement && document.documentElement.clientHeight) {
frameHeight = document.documentElement.clientHeight;
} else if (document.body) {
frameHeight = document.body.clientHeight;
}
return parseInt(frameHeight);
}

function getScrollHeight(){
var y;
// all except Explorer
if (self.pageYOffset) {
y = self.pageYOffset;
} else if (document.documentElement && document.documentElement.scrollTop) {
y = document.documentElement.scrollTop;
} else if (document.body) {
y = document.body.scrollTop;
}
WicketAjaxDebug.logInfo('YYYYY: '+parseInt(y));
return parseInt(y)+_getWindowHeight();
}



function getElementHeight(elid){
var y;
var el= document.getElementById(elid);
var test1 = el.scrollHeight;
var test2 = el.offsetHeight
if (test1 > test2) {
y = el.scrollHeight;
} else {
y = el.offsetHeight;
}
return parseInt(y);
}

function _getElementWindowHeight(elid){
var el= document.getElementById(elid);
if (el.innerHeight) {
frameHeight = el.innerHeight;
} else if (el.clientHeight) {
frameHeight = el.clientHeight;
}
return parseInt(frameHeight);
}

function getElementScrollHeight(elid){
var y;
var el= document.getElementById(elid);
y = el.scrollTop;
WicketAjaxDebug.logInfo('YYYYY: '+parseInt(y));
return parseInt(y)+_getElementWindowHeight(elid);
}

function getRemainingScrollHeight(){
return getPageHeight() - getScrollHeight();
}

function getRemainingScrollHeight(elid){
return getElementHeight(elid) - getElementScrollHeight(elid);
}