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