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);
}

Friday, December 28, 2007

Playing with Wicket((The component oriented web application framework)) Components: Part 1 :Choices

We use wicket ( http://wicket.apache.org/) at my current employers. And now that I have written quite some amount of code in wicket, I want to share my code.

No I am not about to quit; my boss allows me to do so... :)

I just hope to get opinions on improvement.

Also I hope it gets to any of someone's use.

Here goes the story::

(If you don't code please forget about reading further or that this blog even exists in this world. )

I start using wicket. Initially everything is cool. With so much code for a developer already coming in the examples and pre-built components. Also I go and read all things wicket(planet wicket).

And as there is so much activity here, similar is on the mailing list.

My first encounter with any difficulty happens when I want to use choice components in certain scenarios.

The first one is the Country,State,City trio. I want them to be in rhythm on all my pages. I also want this to happen using Ajax.

Here are my three wicket guys for the work::

DropDownChoice countryDrpDwn ;//--from core wicket
DropDownChoice stateDrpDwn;
AutoCompleteTextField cityTxtFld;//--from extensions
Then I need to create them so that I have ids used by them internally for , but descriptions shown to the outer world.
And I found IChoiceRenderer does this job.
Now I need models N subject them to the renderer. The use case looks simple enough for a key-value based model, with a simple renderer.
I could provide my countryDrpDwn a list of the key-value models.
But my stateDrpDwn looks dependent on the country value selected. If I state it well, my stateDrpDwn's list model is dependent on the country value.
And further to complicate things enough cityTxtFld's list model is dependent on the stateDrpDwn's selected value.
I realise(and my realisation could be wrong)..that I needed custom model for wicket. Which could fit these components well.
I end up writing custom models, writing code more than what I needed minimally(happens with a lot of people i know ;))..........(imagine of a fast forward in the movie now....I just provide the results here)

I have a set of following :::
1. ChoiceHelper's which help me work with my choices model outside the Choice component.
2.ChoiceModel's which are the end product which can be used with components

Apologies for the very few comments. I put a lot of them here only now when I write all this, consider them part of my talking.

(1)



/**

* Has(Provides) a strict set of choices with no defaults etc.
* @see ChoicesProvider-- factory+defaults for an encapsulated ChoicesHelper
* @see AbstractChoicesHelper--
A default choices helper--provides some basic functionality + 'nested
helper' functionality so that child classes
* don't have to worry about it.
* @author dontEatTooMuch
*
*/

public interface ChoicesHelper extends IClusterable
{
public List list();
public IChoiceRenderer renderer();

/**

* To support any nested helpers..
* e.g. States for a country_key
* If there's no nested list...throw an exception on this call
* @param key
* @return
*/

public ChoicesHelper helper(Object idvalue);
/**
* Calling the helper(String id) will definitely throw an exception if there's no nested list of choices for the key passed..
* This method allows to check if there is a nested helper to avoid calling the above method
* @param key
* @return
*/
public boolean containsNestedHelper(Object idvalue);
/**
* Returns display values of all choices.
* @return
*/

public String[] displayValues();

public String displayValueOf(String id, Component component);

public Object ObjectOf(String id);

}




(2)



public abstract class AbstractChoicesHelper implements ChoicesHelper

{
public Object ObjectOf(String id)
{
if (id == null)

return null;
final List choices = list();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)

{
final Object choice = choices.get(index);
if (renderer.getIdValue(choice, index).equals(id))

{
return choice;
}
}
return null;

}

public String displayValueOf(String id, Component component)
{
if (id == null)

return null;
final List choices = list();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)

{
final Object choice = choices.get(index);
if (renderer.getIdValue(choice, index).equals(id))

{
Object objectValue = renderer.getDisplayValue(choice);
Class objectClass = objectValue == null ? null : objectValue.getClass();

final String displayValue = component == null
? String.valueOf(objectValue)
: component.getConverter(objectClass).convertToString(objectValue,

component.getLocale());
return displayValue;
}
}
return null;

}

public String[] displayValues()
{
final List choices = list();

final ArrayList dispVals = new ArrayList();
final IChoiceRenderer renderer = renderer();
for (int index = 0; index < choices.size(); index++)

{
final Object choice = choices.get(index);
dispVals.add(String.valueOf(renderer.getDisplayValue(choice)));

}

return dispVals.toArray(new String[dispVals.size()]);
}


public IChoiceRenderer renderer()
{
return new ChoiceRenderer();
}

}


(3)



/**
* all its subclasses provide similar functionality, the difference is supposed to be just the underlying storage
*
* @param
* key in doAdd(K key,V value) implementation in subclass

* @param
* value in doAdd(K key,V value) implementation in subclass
*/
public abstract class AbstractKeyValChoicesHelper extends AbstractChoicesHelper
{

private HashMap nestedHelpers = new HashMap();

public AbstractKeyValChoicesHelper()
{

super();
}

/**
* Add key-value pairs. Also implements a shortcut where the java.lang.Class
* which extends ChoicesHelper can be passed as key, its getSimpleName()

* name will be used as key, also an instance will be stored of this class
* as nested helper. e.g.add(in.class, "India") where in.class is a
* ChoicesHelper providing list of indian states.
*
* @param _key--

* key for storage and reference to this particular choice node
* @param value--
* display value, which if a non-string, will be converted to
* string,based on the locale
* @return-- AbstractChoicesHelper to call the add as you call append in

* StringBuffer
*/
public final AbstractKeyValChoicesHelper add(Object key, Object value)
{
if (key == null || value == null)

{
throw new IllegalArgumentException("key can't be null");
}
if (key instanceof Class)

{
if ((ChoicesHelper.class.isAssignableFrom((Class)key)))
{

String classSimpleName = ((Class)key).getSimpleName();
try
{

nestedHelpers.put(classSimpleName, ((Class)key).newInstance());
}
catch (Exception e)

{
e.printStackTrace();
}
key = classSimpleName;
}

else
{
throw new IllegalArgumentException(key
+ " is not of type ChoicesHelper, which is the only class supported as key");

}

}
doAdd((K)key, (V)value);

return this;
}

/**
* An instance of nestedhelperclass will be stored as nested helper for
* getting children list of this choice

*
* @param _key--
* key for storage and reference to this particular choice node
* @param value--
* display value, which if a non-string, will be converted to

* string,based on the locale
* @param nestedhelperclass--nested
* helper class name
* @return-- AbstractChoicesHelper to call the add as you call append in
* StringBuffer

*/
public final AbstractKeyValChoicesHelper add(K key, V value, Class nestedhelperclass)
{
String strkey;
if (key == null)

{
throw new IllegalArgumentException("key can't be null");
}
if (key instanceof Class)

{
throw new UnsupportedOperationException("please use the other add method");
}
doAdd(key, value);

if (nestedhelperclass != null)
{
try
{
nestedHelpers.put(key, ((Class)nestedhelperclass).newInstance());

}
catch (Exception e)
{
e.printStackTrace();

}
}
return this;
}

/**

* An instance of nestedhelper will be stored as nested helper for getting
* children list of this choice
*
* @param _key--
* key for storage and reference to this particular choice node

* @param value--
* display value, which if a non-string, will be converted to
* string,based on the locale
* @param nestedhelper--nested
* helper instance

* @return-- AbstractChoicesHelper to call the add as you call append in
* StringBuffer
*/
public final AbstractKeyValChoicesHelper add(K key, V value, ChoicesHelper nestedhelper)

{
String strkey;
if (key == null)
{
throw new IllegalArgumentException("key can't be null");

}
if (key instanceof Class)
{
throw new UnsupportedOperationException("please use the other add method");

}
doAdd(key, value);
if (nestedhelper != null)

{
try
{
nestedHelpers.put(String.valueOf(key), nestedhelper);

}
catch (Exception e)
{
e.printStackTrace();

}
}
return this;
}

public ChoicesHelper helper(Object id)

{
if (!nestedHelpers.containsKey(id))
{
throw new IllegalArgumentException(

"There are no nested choices which you are trying to fetch for key" + id);
}
return ((ChoicesHelper)nestedHelpers.get(id));

}

public boolean containsNestedHelper(Object id)
{
return nestedHelpers.containsKey(id);

}

/**
* don't call this method directly--call add(Object _key,Object value)
*
* @param _key

* @param value
*/
protected abstract void doAdd(K _key, V value);


public String displayValueOf(IModel idmodel, Component component)
{
return displayValueOf(String.valueOf(idmodel.getObject()), component);

}
}




(4)



public abstract class MapChoicesHelper<K, V> extends AbstractKeyValChoicesHelper<Object, Object>

{
private IChoiceRenderer renderer = new IChoiceRenderer()
{
public Object getDisplayValue(Object object)

{
return map().get(object);
}


public String getIdValue(Object object, int index)
{
return object == null ? null : object.toString();

}
};

public IChoiceRenderer renderer()
{
return renderer;

}

/**
* Note that this list should not be modified directly
*/

public List list()

{
return new ArrayList(map().keySet());
}


public void doAdd(Object key, Object value)
{
map().put((K)key, (V)value);

}

public Object ObjectOf(String id)
{
return map().get(id);

}

protected abstract Map<K, V> map();
}



(5)



public class SortedMapChoicesHelper extends MapChoicesHelper{

private SortedMap choicesKeyValMap= new TreeMap();
@Override

protected Map map() {

return choicesKeyValMap;

}
}


(6)



public class EnumMapChoicesHelper<T> extends MapChoicesHelper{
private EnumMap choicesKeyValueMap;
public EnumMapChoicesHelper(Class enumclass) {

super();
choicesKeyValueMap= new EnumMap(enumclass);
}


@Override
protected Map<Enum,Object> map() {
return choicesKeyValueMap;
}

}




(7)



public abstract class ListChoicesHelper extends AbstractChoicesHelper{
private HashMap<Object, ChoicesHelper> nestedHelpers = new HashMap<Object, ChoicesHelper>();

private int indx=0;
private ArrayList choiceLst = new ArrayList();
public final ListChoicesHelper add(Object obj){

add(obj,null);
return this;
}

public final ListChoicesHelper add(Object obj,ChoicesHelper nestedhelper){

String strkey;
list().add(obj);
strkey=renderer().getIdValue(obj, list().indexOf(obj));

if(nestedhelper!=null){
try {
nestedHelpers.put(strkey,nestedhelper);

} catch (Exception e) {
e.printStackTrace();
}
}

return this;
}


public boolean containsNestedHelper(Object id) {

return nestedHelpers.containsKey(id);
}

public ChoicesHelper helper(Object id) {

if(!nestedHelpers.containsKey(id)){
throw new IllegalArgumentException("There are no nested choices which you are trying to fetch for key"+id);

}
return ((ChoicesHelper)nestedHelpers.get(id));
}


/**
* Should return the same list instance always
*/
public List list(){
return choiceLst;

};

}



(8)



/**
* acts as a factory to build ChoicesHelper & reuses its instance
*/
public abstract class ChoicesProvider implements ChoicesHelper

{
protected abstract ChoicesHelper buildhelper();

private ChoicesHelper helper = null;

public List list()

{
return helper.list();
}

public String[] displayValues()

{
return helper.displayValues();
}

public IChoiceRenderer renderer()

{
return helper.renderer();
}

public ChoicesProvider helper(final Object id)

{
return new ChoicesProvider()
{
@Override
protected ChoicesHelper buildhelper()

{
ChoicesHelper helperForNested;
if (!helper.containsNestedHelper(id))
{

helperForNested = defaultHelper();
}
else
{
helperForNested = helper.helper(id);

}
// Still null!!
if (helperForNested == null)
{
helperForNested = defaultHelper();

}
return helperForNested;
}
};
}


public ChoicesProvider()
{
super();
if (helper == null)

{
helper = buildhelper();
}
if (helper == null)

{
helper = defaultHelper();
}
}


/**

* Method used to return the defaultHelper()
*
* @return
*/
public final ChoicesHelper defaultHelper()

{
KeyValueChoicesHelper defHelper = new KeyValueChoicesHelper();
return defHelper;
}


public boolean containsNestedHelper(Object id)
{
return helper.containsNestedHelper(id);

}

public Object ObjectOf(String id)
{
return helper.ObjectOf(id);

}

public String displayValueOf(String id, Component component)
{
return helper.displayValueOf(id, component);

}
}


(9)



public abstract class ChoicesModel extends LoadableDetachableModel{
protected static Logger log = LoggerFactory.getLogger(ChoicesModel.class);

public IChoiceRenderer renderer(){
return provider().renderer();
}


@Override
protected Object load() {
return provider().list();

}

/**
* Just to add flexibility of doing stuff before returning an object which is a list here,
* this method makes a call to beforeGetObject() which can be overriden
*/

@Override
public Object getObject() {
beforeGetObject();
return super.getObject();

}
public abstract ChoicesHelper provider();

/**
* Override and do any initialization of other things before returning the nested list object

*/
protected void beforeGetObject(){

}
}



(10)



/**
* For nested choices which are dependent on parents choice
* Intelligent dependent model which provides its listof values based on current value of parent
* @author dontEatToomuch
*

*/
public abstract class DependentChoicesModel extends ChoicesModel
{
public DependentChoicesModel()
{

super();
}

/**
* The key with which this list of choices is linked to something else
* For example you wanted to work with all states of india, you will return "in" for

* getParentDependencyValue() (in the parent list of countries india is represented by "in")
* @return
*/
protected abstract Object getParentDependencyValue();


@Override
public final ChoicesHelper provider()
{
return parentprovider().helper(getParentDependencyValue());

}

protected abstract ChoicesHelper parentprovider();
}



(11)



//start countries-----------------------------------------------------------------------------------------------

public class Countries extends ChoicesProvider{

private static ChoicesHelper locationshelper;
static{
new KeyValueChoicesHelper()

.add(in.class, "India")
.add(usa.class,"United States Of America");
}


@Override
protected ChoicesHelper buildhelper() {
return locationshelper;
}


}
//end countries-------------------------------------------------------------------------------------------------
public static class usa extends ChoicesProvider{
@Override

protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("al", "Alabama")

.add("den", "Denver")
.add("was", "Washington");

}
}
//start india----------------------------------------------------------------------------------------------------
public static class in extends ChoicesProvider{

@Override
protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add(ass.class, "Assam")

.add(ap.class, "Andhra Pradesh")
.add(kar.class, "Karnataka");
}

//start states-------------------------------------------------------------------------------------------------------
public static class ass extends ChoicesProvider{
@Override
protected ChoicesHelper buildhelper() {

return new SortedMapChoicesHelper()
.add("dib", "Dibrugarh")
.add("guw", "Guwahati");

}
}

public static class ap extends ChoicesProvider{
@Override

protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("hyd", "Hyderabad");

}
}

public static class kar extends ChoicesProvider{
@Override

protected ChoicesHelper buildhelper() {
return new SortedMapChoicesHelper()
.add("ban", "Bangalore");

}
}
//end states------------------------------------------------------------------------------------------------------
}
//end
india----------------------------------------------------------------------------------------------------



(12)



public class Months {
private static List<Integer> monthslst = new ArrayList<Integer>();


private static IChoiceRenderer renderer = null;
static {
for (int i = Calendar.JANUARY; i <= Calendar.DECEMBER; i++) {

monthslst.add(new Integer(i));
}
renderer = new IChoiceRenderer() {

private static final long serialVersionUID = -8125518552105537844L;

public Object getDisplayValue(Object object) {
String month;

final int value = ((Integer) object).intValue();
switch (value) {

case Calendar.JANUARY:
month = "Jan";
break;
case Calendar.FEBRUARY:

month = "Feb";
break;
case Calendar.MARCH:
month = "Mar";

break;
case Calendar.APRIL:
month = "Apr";
break;

case Calendar.MAY:
month = "May";
break;
case Calendar.JUNE:

month = "Jun";
break;
case Calendar.JULY:
month = "Jul";

break;
case Calendar.AUGUST:
month = "Aug";
break;

case Calendar.SEPTEMBER:
month = "Sep";
break;
case Calendar.OCTOBER:

month = "Oct";
break;
case Calendar.NOVEMBER:
month = "Nov";

break;
case Calendar.DECEMBER:
month = "Dec";
break;

default:
throw new IllegalStateException(value + " is not mapped!");
}
return month;

}

public String getIdValue(Object object, int index) {
return object.toString();

}
};
}

public static List<Integer> list() {

return monthslst;
}

public static IChoiceRenderer renderer() {
return renderer;

}

}


(13)



/**
* example::
* Numbers fromChoices = new Numbers(1, MAX_VAL);

* DropDownChoice fromChoice = new DropDownChoice("from", new
PropertyModel(getModel(),"from"), fromChoices, fromChoices.renderer()));
*
* Numbers toChoices = new DependentChoicesModel()
{
@Override
protected Object getParentDependencyValue()

{
return fromChoice.getModelObject();
}

@Override
protected ChoicesHelper parentprovider()
{

return fromChoices.provider();
}

};

* DropDownChoice toChoice = (FormComponent)new DropDownChoice("to", new PropertyModel(

getModel(), "to"), toChoices, toChoices.renderer())
{
@Override
public boolean isEnabled()
{
return ((Integer)fromChoice.getModelObject() > 0);

}
}.setOutputMarkupId(true);

*
*/
public class Numbers extends ChoicesModel

{
private int from;
private int to;

private ChoicesHelper provider = new AbstractChoicesHelper()

{


public boolean containsNestedHelper(Object id)
{
return true;

}


public ChoicesHelper helper(final Object id)
{
return new AbstractChoicesHelper()

{

public boolean containsNestedHelper(Object id)
{
return false;

}


public ChoicesHelper helper(Object id)
{
throw new IllegalArgumentException();

}


public List list()
{
int fromval = from;

if (id != null)
{
fromval = (Integer)id;

}
ArrayList lst = new ArrayList();
for (int i = fromval + 1; i <= to; i++)

{
lst.add(i);
}
return lst;

}

};
}

public List list()

{
ArrayList lst = new ArrayList();
for (int i = from; i <= to; i++)

{
lst.add(i);
}
return lst;

}
};

public Numbers(int from, int to)

{
super();
this.from = from;
this.to = to;
}


@Override
public ChoicesHelper provider()
{
return provider;
}

}


===============================================
As it is obvious implimenting ajax functionality is a breeze...
There is nothing much to blog about it.
=============================================
Note:::
1. I had learnt by forgetting that i often forget this
(needed for updating the components via ajax)::
setOutputMarkupId(true);
2. I can use all my utility code for my radio choices as well
3. One should look at these::

  1. systemmobile link--basics on dropdown
  2. wicket wiki link
  3. on wicket website
  4. system mobile link--chaining choices- country,state example