First of all here is a brief definition of some of the terms:
Activity Launch Modes - A simple explanation in a table.
An Android Activity launch mode dictates to which Task an Activity is created and how it is instantiated. Its a tiny bit confusing to figure out exactly what launch mode you need from the official documentation so I've created a small table to help.
First of all here is a brief definition of some of the terms:
Task - A "stack of Activities" in your application. If an Activity is sent to the background (by pressing the HOME key whilst viewing it, for example) then the whole Task (and all the Activities inside it) will be sent back as well. If the user then clicks on your application, the task (and the order of its activities) come forward.
Root of a task - The first Activity in a Task. There is always one of these in a Task.
First of all here is a brief definition of some of the terms:
Essentials at Poundland in Colwyn Bay!
This has nothing to with Android, or anything techie but I thouoght it was a quite funny..
My sister took this photo this back in our home town of Colwyn Bay in Poundland shop. Apparently 2 pregnancy kits were a pound - Now that's value for money!
HttpClient and ConnectionPoolTimeoutException
I've recently had a bit of trouble with my application's DefaultHttpClient instance throwing a ConnectionPoolTimeoutException quite consistently after a number of requests to the same location. (See bottom of the entry for a stacktrace).
There doesn't seem to be anything regarding this particular stacktrace in any of the Android developer forums up to now but coincidently Axis2 framework users (Also using Apache HttpClient) reported the same exception occuring after 3 requests to the same location, which is exactly what I experienced.
Here was my instance creation code for the HttpClient:
If you're using TheradSafeClientConnManager be aware that there is a "Max connections per route" property available which is pretty well hidden, and again isn't mentioned in any of the example code I came across. Setting this to a higher value got rid of my issue.
The stacktrace:
Apache HttpClient is obviously used quite a lot by many apps yet it's configurable parameters haven't been explained in android examples as well as they should be I would say.
There doesn't seem to be anything regarding this particular stacktrace in any of the Android developer forums up to now but coincidently Axis2 framework users (Also using Apache HttpClient) reported the same exception occuring after 3 requests to the same location, which is exactly what I experienced.
Here was my instance creation code for the HttpClient:
HttpParams params = new BasicHttpParams(); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); // Where ClientConfig.P_DEFAULT_CONN_TIMEOUT = 15 seconds HttpConnectionParams.setConnectionTimeout(params, ClientConfig.P_DEFAULT_CONN_TIMEOUT); HttpConnectionParams.setSoTimeout(params, ClientConfig.P_DEFAULT_CONN_TIMEOUT); ConnManagerParams.setTimeout(params, ClientConfig.P_DEFAULT_CONN_TIMEOUT); httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(params, schemeRegistry), params);
If you're using TheradSafeClientConnManager be aware that there is a "Max connections per route" property available which is pretty well hidden, and again isn't mentioned in any of the example code I came across. Setting this to a higher value got rid of my issue.
... ConnManagerParams.setMaxConnectionsPerRoute(params, new ConnPerRoute() { @Override public int getMaxForRoute(HttpRoute httproute) { return 10; } }); ...
The stacktrace:
fetch() sending failed to url http://192.168.0.45/blah org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection at org.apache.http.impl.conn.tsccm.ConnPoolByRoute.getEntryBlocking(ConnPoolByRoute.java:353) at org.apache.http.impl.conn.tsccm.ConnPoolByRoute$1.getPoolEntry(ConnPoolByRoute.java:238) at org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager$1.getConnection(ThreadSafeClientConnManager.java:175) at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:325) at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555) at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487) at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:465) at chesspresso.client.android.net.HttpCall.fetch(HttpCall.java:114) at chesspresso.client.android.net.HttpCall.fetch(HttpCall.java:134) at chesspresso.client.android.net.HttpCall.post(HttpCall.java:107) at chesspresso.client.android.net.ServerCallGoogle.loginGoogle(ServerCallGoogle.java:30) at chesspresso.client.android.activity.support.ActivityProcessesGoogle$LoginGoogleTask.doInBackground(ActivityProcessesGoogle.java:72) at chesspresso.client.android.activity.support.ActivityProcessesGoogle$LoginGoogleTask.doInBackground(ActivityProcessesGoogle.java:1) at android.os.AsyncTask$2.call(AsyncTask.java:185) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305) at java.util.concurrent.FutureTask.run(FutureTask.java:137) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561) at java.lang.Thread.run(Thread.java:1096)
Apache HttpClient is obviously used quite a lot by many apps yet it's configurable parameters haven't been explained in android examples as well as they should be I would say.
Samsung Galaxy Tab - Bit of a Gimmick
I don't normally purchase cutting edge technology on their day of release, but this time around the Galaxy Tab caught my attention. Over the last few days I followed internet reviews, opinions and videos of the release of this thing and after a while I convinced myself that I needed to buy one. I definitely needed a Froyo based device of some sorts as I've been using a 1.5 device for a long time as my main phone and that was the only piece of hardware I had for development. For development a decent handset is essential as of course you need to check your work on the real thing every now and again and the latest release emulators can be painfully slow especially in debug mode, so I went to the Carphone Warehouse yesterday on the day of release and purchased one for 499 GBP.
Probably the most expensive gadget I will ever buy, but the best price I could find - A 1GB data only 1-month rolling contract with TalkTalk (and a free month) that I can cancel any time - which I probably will in the next few days. This for 499GBP.
After 24 hours with this thing now I'm a bit surprised at the large number of positive reviews this device has received... Its main flaw is that it doesn't really fill in a space that needs to be filled! It's an oversize mobile phone that simply can't replace your laptop or netbook for any real purpose.
I use Gmail and Google Reader a lot for example, and I was thinking this would be a great device just to read my mail and browse my feeds. Replying to email with the on-screen keyboard is annoying and the mobile gmail client is no where near as good as what you'd receive on your laptop browser. The same with Google Reader. So many features that aren't available that you just end up using your laptop/netbook.
I also thought this thing would be great for Skype. No it's not. Again down to the actual Skype application for Android because it doesn't seem to support video calling at the moment and again there are far too many features that are missing from what you could be installing on your PC. Again, another reason to ditch the tablet and pick up your laptop instead.
Of course this thing won't replace your mobile phone either because it is far too big to carry around in your pocket. I was thinking of taking it to the gym this morning and to try a particular excercise / rep application and maybe to listen to some podcasts, but really having this thing AND my mobile is just silly. It works as a mobile of course and supports Bluetooth etc. but it's far too big to bring up to your ear as a mobile phone.
At a price of usually 530 GBP it is incredibly expensive for a bit of a gimmick really. I guarantee that more often than not it won't allow you to do what you want as well as your current laptop and mobile phone. You'll also look like a bit of an idiot if someone phones you on it.
And another little criticism before I go! I have a spot in the house from which I use my laptop quite often and I usually get 2 bars on my Windows 7 status bar icon. The Tab can't find my access point from the same spot! I didn't notice any review as yet which also commented on the quality of the wifi but in my opinion it's definitely not as good as my laptop wifi interface.
Sorry, but I'm not a convinced with this one. A bad impulse purchase!
Probably the most expensive gadget I will ever buy, but the best price I could find - A 1GB data only 1-month rolling contract with TalkTalk (and a free month) that I can cancel any time - which I probably will in the next few days. This for 499GBP.
After 24 hours with this thing now I'm a bit surprised at the large number of positive reviews this device has received... Its main flaw is that it doesn't really fill in a space that needs to be filled! It's an oversize mobile phone that simply can't replace your laptop or netbook for any real purpose.
I use Gmail and Google Reader a lot for example, and I was thinking this would be a great device just to read my mail and browse my feeds. Replying to email with the on-screen keyboard is annoying and the mobile gmail client is no where near as good as what you'd receive on your laptop browser. The same with Google Reader. So many features that aren't available that you just end up using your laptop/netbook.
I also thought this thing would be great for Skype. No it's not. Again down to the actual Skype application for Android because it doesn't seem to support video calling at the moment and again there are far too many features that are missing from what you could be installing on your PC. Again, another reason to ditch the tablet and pick up your laptop instead.
Of course this thing won't replace your mobile phone either because it is far too big to carry around in your pocket. I was thinking of taking it to the gym this morning and to try a particular excercise / rep application and maybe to listen to some podcasts, but really having this thing AND my mobile is just silly. It works as a mobile of course and supports Bluetooth etc. but it's far too big to bring up to your ear as a mobile phone.
At a price of usually 530 GBP it is incredibly expensive for a bit of a gimmick really. I guarantee that more often than not it won't allow you to do what you want as well as your current laptop and mobile phone. You'll also look like a bit of an idiot if someone phones you on it.
And another little criticism before I go! I have a spot in the house from which I use my laptop quite often and I usually get 2 bars on my Windows 7 status bar icon. The Tab can't find my access point from the same spot! I didn't notice any review as yet which also commented on the quality of the wifi but in my opinion it's definitely not as good as my laptop wifi interface.
Sorry, but I'm not a convinced with this one. A bad impulse purchase!
GAE and Log4j - Getting them to work a bit better together
The Problem
App Engine supports Log4j, and you'll be able to see your log4j statements come through either as a stdout or stderr message. The trouble that I've had is that if you want to make full use of the nice log highlighting GAE supports in its console then all stdout / stderr messages will come through as a warning.
A good portion of Java frameworks use Log4j as their logging framework. I'm using Apache Wicket as my front-end framework for example, and when user has hit upon a bug on a page, I'd like to know about it as an error and not just a warning.
A Solution
Here's a Log4j Appender which I've written that creates JUL log statements based on original Log4j log events. A Log4j -> JUL bridge if you like, passing on the original severity level to JUL. It's pretty basic, but it works for me!
Here's the code for the Appender itself:
Now the GAE logging.properties file...
I'm still a bit unsure of how exactly GAE Maps JUL severities in it's frontend. In JUL you have FINEST, FINER, INFO, WARNING, SEVERE but the front-end seems to display these differently. It has an ERROR and CRITICAL as well as a WARNING. SEVERE seems to be flagged as an ERROR, but how can you flag something as CRITICAL? Maybe it's in some documentation I've probably scanned through :-)
This is a quick implementation of this Appender for me and so far it works, but I'd love to hear some feedback about what it doesn't do (or doesn't do properly) as I've not used it in anger.
App Engine supports Log4j, and you'll be able to see your log4j statements come through either as a stdout or stderr message. The trouble that I've had is that if you want to make full use of the nice log highlighting GAE supports in its console then all stdout / stderr messages will come through as a warning.
A good portion of Java frameworks use Log4j as their logging framework. I'm using Apache Wicket as my front-end framework for example, and when user has hit upon a bug on a page, I'd like to know about it as an error and not just a warning.
A Solution
Here's a Log4j Appender which I've written that creates JUL log statements based on original Log4j log events. A Log4j -> JUL bridge if you like, passing on the original severity level to JUL. It's pretty basic, but it works for me!
Here's the code for the Appender itself:
/** * * @author Eurig Jones */ public class GAELogAppender extends AppenderSkeleton { private static final Logger L = Logger.getLogger("log4j"); @Override protected void append(LoggingEvent event) { StringBuilder b = new StringBuilder(); Level level = event.getLevel(); String s[] = event.getThrowableStrRep(); b.append(event.getMessage().toString()); if (s != null) for (String line : s) b.append(line).append('\n'); if (level.equals(Level.FATAL)) FATAL(b.toString()); if (level.equals(Level.ERROR)) ERROR(b.toString()); if (level.equals(Level.WARN)) WARN(b.toString()); if (level.equals(Level.INFO)) INFO(b.toString()); if (level.equals(Level.DEBUG)) DEBUG(b.toString()); if (level.equals(Level.TRACE)) TRACE(b.toString()); } private void FATAL(String msg) { L.severe(msg); } private void ERROR(String msg) { L.severe(msg); } private void WARN(String msg) { L.warning(msg); } private void INFO(String msg) { L.info(msg); } private void DEBUG(String msg) { L.fine(msg); } private void TRACE(String msg) { L.finest(msg); } @Override public boolean requiresLayout() { return false; } @Override public void close() { } }Now all we need to do is setup the log4j.properties and logging.properties configurations correctly to make it work. Here is a snippet of my log4j.properties:
#log4j.appender.A1=org.apache.log4j.ConsoleAppender #log4j.appender.A1.layout=org.apache.log4j.PatternLayout #log4j.appender.A1.layout.ConversionPattern=%d ... #log4j.appender.A1=org.apache.log4j.jul.JULAppender log4j.appender.A1=chesspresso.server.log.GAELogAppender log4j.logger=ERROR, A1 ... log4j.logger.org.apache.wicket=WARN, A1 ...Above shows that I've replaced the original ConsoleAppender - the default appender which GAE uses, with the new GAELogAppender. This allowing all Log4j statements to go through this instead of the console.
Now the GAE logging.properties file...
.level = INFO chesspresso.server.log.GAELogAppender=FINEST ...As you can see, I've added the GAELogAppender here to allow all severities to be filtered through. This means that anything that hits our new appender will reach the GAE logging system, as long as our log4j.properties will allow it to filter through.
I'm still a bit unsure of how exactly GAE Maps JUL severities in it's frontend. In JUL you have FINEST, FINER, INFO, WARNING, SEVERE but the front-end seems to display these differently. It has an ERROR and CRITICAL as well as a WARNING. SEVERE seems to be flagged as an ERROR, but how can you flag something as CRITICAL? Maybe it's in some documentation I've probably scanned through :-)
This is a quick implementation of this Appender for me and so far it works, but I'd love to hear some feedback about what it doesn't do (or doesn't do properly) as I've not used it in anger.
AppEngine, JRebel and Eclipse - Getting them to work together
I recently started using JRebel for development, but initially I had a bit of trouble getting it to gel with the App Engine / Eclipse setup. After an email or two with the guys at Zero Turnaround I finally got it to work, and it's well worth the effort. Here is a guide for what you need to do...
Now enjoy a considerably quicker method of developing / deploying App Engine apps. $59 bucks is well worth it!
I'm hoping the team at Zero Turnaround turn their attention to Android's Dalvik VM next. JRebel would be of HUGE benefit to developing Android apps and I'm sure with the amount of Android developers out there at the moment it would be a big money spinner for them too.
- Install JRebel, the latest version for your platform.
- Get into Eclipse and install the JRebel Eclipse Plugin using the "Install New Software..." link under the Help menu, and add the JRebel Update site...
http://www.zeroturnaround.com/update-site
I specified the "JRebel Eclipse Debugger Integration" and "JRebel Eclipse Integration only" components.
- Specified the jrebel.jar in Eclipse / JRebel preferences. (Mine was in my C:\Program Files\ZeroTurnaround\JRebel\jrebel.jar folder.
- The installation Wizard provided with JRebel provides you a guide to get it working in Eclipse. Make sure you go through those steps. (...adding org.zeroturnaround.* etc in Eclipse preferences)
- Modified the dev_appserver.cmd, which on my PC sat in
C:\dev\eclipse\eclipse-SDK-3.6-win32\plugins\com.google.appengine.eclipse.sdkbundle.1.3.7_1.3.7.v201008311427\appengine-java-sdk-1.3.7\bin\dev_appserver.cmd
Here is it's contents:
@java -cp "%~dp0\..\lib\appengine-tools-api.jar" ^ com.google.appengine.tools.KickStart ^ --jvm_flag=-javaagent:%REBEL_HOME%\jrebel.jar --jvm_flag=-noverify ^ com.google.appengine.tools.development.DevAppServerMain %*
- Now you need to add this to the VM Arguments for your Run configuration for your App Engine application:
-javaagent:/opt/jrebel/jrebel.jar -noverify
Again, make sure this points to your jrebel.jar location.
- Once you've modified the VM Arguments in the app run config then you should see the JRebel text appear in the console.
- By the way, you don't need to execute your application in Debug mode to get JRebel to work. If you modify a file in Debug mode you'll still get the "Hot Code Replace Failed" message.
- To test I simply booted up the app, and added a method to a class and called it - You'll see a message in the console saying that JRebel is reloading the class.
Now enjoy a considerably quicker method of developing / deploying App Engine apps. $59 bucks is well worth it!
I'm hoping the team at Zero Turnaround turn their attention to Android's Dalvik VM next. JRebel would be of HUGE benefit to developing Android apps and I'm sure with the amount of Android developers out there at the moment it would be a big money spinner for them too.
Wicket and App Engine - How to avoid loading Wicket on startup
A GAE app of mine (www.chesspresso.net) which is a backend to an Android app that most requests come via other code rather than through Wicket. Wicket is my chosen framework which I use to display the front-end of the application (the website!). And most of the requests to the application does not require wicket.
As I'm sure many GAE users know, the startup time is a big problem especially for low traffic apps, and having to load a framework such as Wicket on startup is damaging to the request time for an App.
To avoid having to start the WicketFilter every time the app starts I have extended the WicketFilter to only initialize when the first Wicket page is hit:
As I'm sure many GAE users know, the startup time is a big problem especially for low traffic apps, and having to load a framework such as Wicket on startup is damaging to the request time for an App.
To avoid having to start the WicketFilter every time the app starts I have extended the WicketFilter to only initialize when the first Wicket page is hit:
public class RequestWicketFilter extends WicketFilter { private static boolean requestMade = false; private static boolean initialized = false; private static FilterConfig fConf; @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (!requestMade) requestMade = true; init(fConf); super.doFilter(req, res, chain); } @Override public void init(FilterConfig fC) throws ServletException { if (fConf == null) fConf = fC; if (requestMade && !initialized) { super.init(fConf); initialized = true; } } }
Eclipse freezing / crashing on start-up - How to get going again.
I've been suffering from freezes starting up Eclipse for quite a while now. It happens every few days for reasons I'm not sure of and it's incredibly annoying! My Eclipse Galileo installation has a number of plug-ins on top of it at the moment. Android Development Kit and the App engine plug-in are two of them, but I use lots more but I don't think its to do with any one in particular. Here is how I usually get going again...
- Go to the folder of your workspace and rename the project you were working on to something else, if you're not sure what project is that might be causing problems, rename them all or just move them to somewhere else temporarily!
- Start-up eclipse and it should start-up this time.
- Rename or move back all the folders to what they were and open them in your eclipse.
- Clean your workspace.
- Un-check "build-automatically" under "Project"
- Re-start eclipse.
- Check back "build automatically"
Working?
If you're having trouble renaming your project folder because some program has a lock on it, I would suggest downloading a neat little utility called Unlocker which just adds an option to your context menu for a file or folder.
If you're having trouble renaming your project folder because some program has a lock on it, I would suggest downloading a neat little utility called Unlocker which just adds an option to your context menu for a file or folder.
Slow initial start-up time in your Java App Engine Application?
A lot of developers have been complaining about slow start-up times of their applications in app-engine production recently on the App Engine issues list. 10-20 second start-up times is not uncommon causing long waits for app users and potential errors in some applications. This is made worse by applications that rely on frameworks that do quite a bit on start-up. With app engine your application doesn't get a permanent JVM instance like you would usualy so this means that if there's been no activity for a while the JVM goes cold so your app will have to start-up again on the next request.
The good news for people suffering with this issue (and that is all App Engine developers I would say) is that the GAE team has accepted the issue and introduced it into their roadmap - "Ability to reserve instances to reduce application loading overhead". Personally I really won't mind paying for my own JVM instance if it's not too expensive, and I would guess that this is probably the direction the GAE team will go.
My "quick-fix" (or hack) for this issue at least until the GAE team sorts it out, is to ping your app every few minutes to keep the JVM "warm". Here I've created a blank servlet that gets called by the AE background cron.xml tasks every few minutes...
web.xml:
BlankServlet.java:
cron.xml:
The good news for people suffering with this issue (and that is all App Engine developers I would say) is that the GAE team has accepted the issue and introduced it into their roadmap - "Ability to reserve instances to reduce application loading overhead". Personally I really won't mind paying for my own JVM instance if it's not too expensive, and I would guess that this is probably the direction the GAE team will go.
My "quick-fix" (or hack) for this issue at least until the GAE team sorts it out, is to ping your app every few minutes to keep the JVM "warm". Here I've created a blank servlet that gets called by the AE background cron.xml tasks every few minutes...
web.xml:
... <servlet> <display-name>BlankServlet</display-name> <servlet-name>BlankServlet</servlet-name> <servlet-class>my.package.BlankServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>BlankServlet</servlet-name> <url-pattern>/blank.html</url-pattern> </servlet-mapping> ...
BlankServlet.java:
public class BlankServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) { // Nothing needed here. } }
cron.xml:
<?xml version="1.0" encoding="UTF-8"?> <cronentries> <cron> <url>/blank.html</url> <description>Ping a blank page to keep the JVM warm</description> <schedule>every 5 minutes</schedule> </cron> </cronentries>You could of course ping your main welcome page instead but you might have some code in your pages that you might not want to be executed by an automatic process - maybe your Google Analytics javascript for example.
Wicket - Submitting a form to an anchor position
I had a non-ajax style form on the bottom of a long page. When the form is submitted, the top of the page is displayed, and not the bottom. Annoying for a user if they've submitted a comment to a page for example, and they have to scroll down right to the bottom of the page to see if it's been added. In a standard form in HTML you can just submit the form to an anchor like this...
<form method="post" action="/myPage.html#jumpToHere"> ... my form content ... </form> <div id="jumpToHere"> ... Jump back to here once submitted </div>In Wicket you can do this by overriding the onComponentTag() method in your wicket Form...
public class MyForm extends Form<T> { ... @Override protected void onComponentTag(ComponentTag tag) { super.onComponentTag(tag); StringBuilder b = new StringBuilder(tag.getString("action").toString()).append("#jumpToHere"); tag.put("action", b.toString()); } }
Wicket Locale drop-down selector
I needed a drop-down in Wicket which allowed the user to select their language to update the app/site content. I also wanted the languages to appear in their own character sets as well. Something that looks like this...
Java and the Locale class can provide us with this quite easily so here is a simple component based on the standard Wicket DropDownChoice component that does the job. You pass it a bunch of Locale objects representing the languages that you want to be supported and it will update the model you give it when it's selected...
Java and the Locale class can provide us with this quite easily so here is a simple component based on the standard Wicket DropDownChoice component that does the job. You pass it a bunch of Locale objects representing the languages that you want to be supported and it will update the model you give it when it's selected...
/** * Wicket Drop-down language / locale selector * * @author Eurig Jones */ public class DropDownLocale extends DropDownChoice<Locale> { public DropDownLocale(String id, IModel<Locale> model, List locales) { super(id, model, locales); setChoiceRenderer(new LocaleChoiceRenderer()); } @Override protected void onSelectionChanged(Locale newSelection) { getSession().setLocale(newSelection); } @Override protected boolean wantOnSelectionChangedNotifications() { return true; } private class LocaleChoiceRenderer implements IChoiceRenderer<Locale> { @Override public Object getDisplayValue(Locale locale) { // Change this to just locale.getDisplayLanguage() and it'll display the languages in the currently selected language / Locale and character set. return locale.getDisplayLanguage(locale); } @Override public String getIdValue(Locale locale, int i) { return locale.getLanguage(); } } }Some markup...
<select wicket:id="selectLanguage"></select>Here is some example calling code. We pass our new DropDownLocale component a bunch of Locale objects representing the languages we intend to support. We also pass it a model to update. Here I have chosen to directly modify the session using a PropertyModel so as soon as the drop-down is used, the app is updated with the correct language...
List<Locale> supportedLanguages = new ArrayList<Locale>(); supportedLanguages.add(Locale.ENGLISH); supportedLanguages.add(Locale.FRENCH); supportedLanguages.add(Locale.JAPANESE); PropertyModel<Locale> model = new PropertyModel<Locale>(getSession(), "locale"); DropDownLocale selectLanguage = new DropDownLocale("selectLanguage", model, supportedLanguages); add(selectLanguage);
Subscribe to:
Posts (Atom)