...and wish I had known right from the start!
Even if you instantly like Wicket's Object-oriented modular approach, you have quite some learning ahead of you before you can use it's powers well.
There are plenty of good books, example website and tutorials out there, and the code is well-written. So there's nothing to worry about. Eventually, you will grasp it.
But if you're like me, you read fast, and thus miss some points. I learnt a lot by reading answers by smart people in discussion forums. Here's me giving back something to the community: My "must-know list" I wish I had had when I started learning.
Nothing new or exciting for seasoned Wicket developers. But maybe something for newbies. And definitely
a must-read for anyone starting in our new company.
This is something I learned about way too late! You have to use Models for just about everything, or your session size will simply explode.
This may not be a problem for a rarely used application on a powerful server, but it can become a nightmare if you have thousands of users in parallel,
or if you are using Google App Engine like us, which enforces a strict 1MB session size maximum.
Read our separate post about optimising session size.
I rarely read documentation. So it took me several months of dead-ugly code to figure out some of the most basic cool feature of Wicket. It's the enclosure.
Consider this problem: You want to display an optional value. To display it, you need additional context. What do do with the context when the optional value should get hidden?
Maybe you have a user object, and each user has a first name, and a second name. And then some users have a nickname too. You display them in a table, each one gets a row.
| Last name | Smith |
| First name | John |
| Nickname | Johnny |
Or you could take the shortcut: Set the visibility of the nickname-Label to invisible, and let an Enclosure handle the rest:
<table>
<tr><td>Last name</td><td><span wicket:id="lastName"/></td></tr>
<tr><td>First name</td><td><span wicket:id="firstName"/></td></tr>
<wicket:enclosure child="nickName">
<tr><td>Nickname name</td><td><span wicket:id="nickName"/></td></tr>
</wicket:enclosure>
</table>
It seems to be good practice to call session.replaceSession() to prevent session fixation attacks. That's fine, and it works on my local machine. However, if run on Google App Engine (and presumably other clustered web servers, since GAE just uses Jetty) then there's a problem, which took me months to realise.
When people want to get to a certain password-protected page, and they have to log in, then they get redirected to our login page. Once logged in, they continue to their desired page. Or do they? Well, not anymore after we added the replaceSession().
The replaceSession kills the redirection, but only on the actual webserver. What we ended up doing was gather the continuationURL, store it, replace the session, and then force th continuationURL onto Wicket again. I bet there are more elegant ways, but Googling around didn't find me a better solution yet, so here is the code to get and set the destination:
private String getOriginalDestinationUrl(Page page) {
PageMap p = (PageMap) page.getPageMap();
Field field;
try {
field = PageMap.class.getDeclaredField("interceptContinuationURL");
field.setAccessible(true);
String inter = (String) field.get(p);
log.info("got " + inter + " from field access");
return inter;
} catch (Exception e) {
throw new RuntimeException("error getting the continuation.", e);
}
}
private void setOriginalDestinationUrl(String value, Page page) {
PageMap p = (PageMap) page.getPageMap();
log.info("Setting intercept to " + value);
Field field;
try {
field = PageMap.class.getDeclaredField("interceptContinuationURL");
field.setAccessible(true);
field.set(p, value);
} catch (Exception e) {
throw new RuntimeException("error setting the continuation", e);
}
}
Thanks to Nico and Stephan from tasqade for this hint!
You have to love AttributeAppenders. A simple, foolproof way to dynamically add a CSS class to your Label, or to add a title to your Image. Dynamically, that is. How else would you get those nifty tooltips when hovering on an image, or a different CSS class depending on whether we're currently dealing with positive votes (green colour!) or negative votes (red color!). You can even pass your Components around through your app, add multiple Appenders, and Wicket ensures there's no duplication in the resulting code. I used AttributeAppenders a lot because they are so fun!
userLogo.add(new AttributeAppender("title", new Model(userName), ""));
userLogo.add(new AttributeAppender("class", new Model("userIcon"), ""));
Until this week.
App Engine doesn't like big sessions, and neither do I. There's a hard cap of 1MB when App Engine refuses to store your session, and even in the 200K region I get uneasy, since even these 200K incur serialisation and deserialisation costs. My main gripe with Wicket is that it stores all its components in the session for the last n pages you visited. This does add up. Now, the problem is that AttributeAppenders also end up in the page tree and thus in the session. And if you were as happy-go-lucky as me, then chances are you have many AttributeAppenders in there. Each one may just cost 40 to 120 bytes or so. But if you display 30 rows of data, and each row is heavily decorated and linked (author! recipient! project! parent message! votes! etc) these hundreds of Appenders do add up too!
Fortunately, I was able to cut the number in half by analysing each Appender and checking if there's some other way. Often I had just added Appenders because I was lazy. Or because I thought some tooltips might add value (but they didn't really). Sometimes I had used Appenders because I thought it was a nice way to factor out the classes into ONE spot, even if the links were built from 20 different pages. Sometimes I had one class for case A, and one for case B (e.g. red negative votes vs green positive votes), and I could make one the default, and have a class only for the less likely case (negative votes in my example).
So there's no need to panic or stop using AttributeAppenders, and premature optimisation is evil anyway. Use AttributeAppenders for 99% of the cases. Only if you just know that a certain link will be pervasive through the whole app, it may make sense to think about memory and performance.
So in the above example, I keep using the title to display the username since there's no other way. But the class now is wired into the component in the HTML.
Surprisingly, some people don't know about the mount()-methods. These methods turn the default ugly URLs into something nice. It works like this
mountBookmarkablePage("/authenticate", PleaseAuthenticatePage.class);
setVisible(false) is great to hide a component based on some on-the-fly calculation, for example during the initial page rendering But to show that component later on during a Wicket Ajax request, you'll have to mark that component. Otherwise setVisible(true) simply won't to anything. Call
mycomponent.setOutputMarkupPlaceholderTag(true), in addition to setOutputMarkupId(true). Of course, don't forget to add that component to the AjaxRequestTarget.
Admittedly, this is very basic knowledge. But it's a bug I kept running into a few times until I remembered.
Important addition: Override onConfigure()!It's pretty obvious that there is a setVisible() method on each component. It's not quite as visible that you shouldn't use this directly while you're processing an AJAX request. Instead, override the onConfigure() method, and inside it use setVisible(), based on the objects' models' state. The AJAX request merely states that the said component is dirty and needs to be re-rendered. ("addComponent()")
It is seriously easy to make panels, components and pages incorporate JavaScript and CSS files. Just put this into a panel, for example, and you don't have to worry about imports in the HTML file anymore.
add(CSSPackageResource.getHeaderContribution(IndexPage.class, "global.css"));
add(JavascriptPackageResource.getHeaderContribution(IndexPage.class, "jquery.tools.min.js"));
Then set the browser cache to something appropriate. Once your code is stable and in production, you probably want really long page expiry settings. But during development, alpha- and beta-testing, 2 hours seems to be a quite good compromise. Beta-testes will reload resources every day they try out your features (and won't have to shift-refresh to get your latest css changes), but there won't be any reloading during a testing session.
getResourceSettings().setDefaultCacheDuration(7200);
I'd still advise not to split your resources up per component (e.g. to have a JS and a CSS for eachpanel)
The more resource files you have, the more requests the browser has to make, slowing your pages down significantly. I find it perfectly fine to develop a medium-complex application with just one master-js and one master-css file for the things shared across multiple pages, sprinkling in one js and one css file per wicket page for the very specific things (many pages doing just fine without those additional files though).
As long as you clean your css and js frequently, the additional overhead for the browser to parce css and js that is not used in every page is barely noticeable.
Careful!
Being the lazy guy I am, I had put all my key Resources into my base component. This worked like a charm, each page included the right JS and CSS files, and only once, just as desired. Only when I checked in the profiler, it became obvious that each component now had the same behaviours, which resulted in a bloated session. I quickly moved those resource-methods into the base page instead, which works as well, but without the huge session size penalty.
Wicket is perfect at modularising your code. I just love it. However, it is so simple that you really don't have to do it as the first thing. It is perfectly fine to delay it a bit, until your architecture becomes more stable, and until you know how your components are really going to look and interact.
A typical pattern when dealing with lists of objects was to have a page class (PerformanceReviewListPage), which contained a specific panel to handle the actual list (PerformanceReviewListPanel). Turns out that it's so easy to extract the list panel later on, you may as well do it later on. If you really need it, that is. In my case, I didn't really often end up reusing the panel, and most of our pages are not that terribly long either. But it blows up the number of files when using autocomplete, so why componentise too early.
Say you are rendering a list of item titles, and for each item you want to be able to load a detail view right into the list. Or you have some tree-like structure, showing a list of items, and want to ajax-load sublists into an element.
There is an almost trivial way to do this. It is called replaceWith(). All you need is to add an empty placeholder component underneath each item in your list. Now, if someone clicks that ajax-link, you create a new panel with the same ID, and call placeholderPanel.replaceWith(newPanel). THAT'S IT! I love Wicket!
The main problem is finding this part of the documentation, or to realise this is what you need. I searched for plenty of keywords, didn't find anyhting, and ended up using JQuery's AJAX functionality to load a specific sub-list-page into the current one. Which is a major PITA with Wicket, because then you suddenly run into problems with Wicket pagemaps, and the dreaded Page Expired errors.
So, for all the poor souls looking for this solution, here are the keywords I used to search for:
I hope this helped. Again, replaceWith() is what you're looking for!
Say, an event list. Event A has three properties, Event B has only 2, and Event C should always be displayed with some additional icons and colours. You could have one panel for each event type, but that's a bit of an overkill in most cases. On the other end, having a single panel render different types of events would result in nasty IF statements and overly complex code. Fragments to the rescue. (I found them a bit hard to understand at first, but it is really worth the effort. You'll have one ling but simple IF statement like this
...
else if (type==Event.COMMENT_CHANGE_PROJECT) {
listItem.add(new FragmentCommentChangeProject("eventDetails",
"fragmentCommentChangeProject", eventCreator,comment, project));
}
else if (type==Event.USER_LOG_IN) {
listItem.add(new FragmentUserLogIn("eventDetails","fragmentUserLogIn", eventCreator));
}
...
And then inner classes for each fragment, and neat simple HTML fragments like this
<wicket:fragment wicket:id="fragmentUserLogIn">
<a wicket:id="executingUserName"> <span wicket:id="userName">Andrew Lynch</span></a>
logged in.
</wicket:fragment>
There is a very simple autocomplete component in Wicket Extensions. You've seen it, it works fine for basic use cases. But it fails miserably for duplicates, and if you want to display additional text in the dropdown.
But let's assume you write a user-picker. Since the component is String-based only, it cannot tell duplicate names from each other. You have two "Joe Smith" in your list (and you can tell them apart by their avatar), and select the second one? Whoops, the component only knows you selected a certain Joe Smith. Which could be either the first, or the second. Duplicates just don't work.
Even if you don't have any duplicates, you get into trouble once you try to spice up the data that's displayed. You can override public String getTextValue(Object o) { } for example to display the position of the employee in small grey letters, hacking together some html there. But then the position ends up in the search field. And then you're in trouble, because your database doesn't know about a user called "Joe Smith Developer".
Fortunately, there is a not-so-well-known solution in Wicket-Stuff, which solves both your problems. Steve Swinsburg posted a great article about it The component works just fine, and allows you to add arbitrary text to the display, without confusing the selection. Like this:
Some HTML/CSS fiddling was necessary, but not very hard.
Note: I was not able to get a maven-compilable checkout from wicket-stuff, and the documentation was a bit sparse. What worked well for me though: I copy-pasted the autocomplete-related code into a separate project of my own. This compiled just fine. Also, this means less class-files in my classpath, which is essential on Google App Engine.
Note 2: If you are as confused as me about the component replacing the searchbox by th selected item It seems that appending clearInputOnSelection(); does the trick. As in
ObjectAutoCompleteBuilder<User,String> builder =
new ObjectAutoCompleteBuilder<User,String>(
new AutoCompletionChoicesProvider<User>() {
public Iterator<User> getChoices(String s) {
if (allUsers == null) {
Key companyKey = currentUser.getCompanyKey();
allUsers = Service.getServices().getCompanyService()
.getUsers(companyKey, false);
}
ArrayList results = Util.filter(s, allUsers);
if (results.size() == 0)
results.add(User.NOUSER);
return results.iterator();
}
}
).autoCompleteResponseRenderer(getAutoCompleteResponseRenderer()).clearInputOnSelection();
The problem I struggled with is that you cannot remove an appender that's not there, Wikcet throws an exception. So if you want to use an appender depending on certain circumstances, and get rid of it after an AJAX action, it can be tricky to remember which appender you added, and only remove it if it had been added. The quick and dirty way is easier, simply override getBehaviors, and add your appender only on demand:
protected <M extends IBehavior> List<M> getBehaviors(Class<M> type) {
List<M> mList = new ArrayList<M>(super.getBehaviors(type));
if (!feedbackRequestDetachableModel.getRequest().isCompleted()) {
mList.add((M)greenAppender);
}
return mList;
}
But yeah, it is dirty, since this method gets called quite a few times, so don't do this in inner loops. If there's a better way, please
mail us!
If you read this far, and if you're a student or a junior developer looking for a job on a really neat and modern application, we should talk! We use a whole stack of cool technology, there's no hierarchy or beauraucracy, and we're a nice bunch too. We're based in Berlin (with a satellite office in Sydney), and most of our customers are in the US or in Australia: Atlassian, Quiksilver, Opera, Klout, Modcloth, SugarInc just to mention a few.
We love working remotely, so as long you're able to make it to Berlin every now and then for a week. We do have some requirements of course, so do check out our Java and JQuery Developer Jobs in Berlin recruiting page.
We frequently offer a referral bonus, so if you know someone else, do check out the careers page as well.