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.