This past week I spent a lot of time investigating problems a user was having with a production Wicket application. The problems are difficult to accurately describe and were varied:
- Various ajax components would sometimes not invoke their behaviors.
- Clicking buttons would sometimes invoke an unexpected behavior associated with a completely different button.
- Sometimes clicking a save button to submit a form would not work and would reload the page with a behavior callback URL instead of the original URL.
The user lives in a rural area and has a very slow internet connection and connects through a VPN. We thought about the possibility of network instability, packet loss, or VPN interference. But it was none of those things.
If you want the short answer, DefaultMarkupIdGenerator was generating duplicate id’s in some cases because it tries to optimize the length of the ids in deployment mode. Read on for more!
Background
So you have a better picture of the scenario, it’s necessary to describe the particular interface and action that would lead to the problems. We have a search page with filters and a table of results. Clicking the search button would apply the filters and update the results. We also have an autocomplete dropdown for searching by keyword. Upon selecting a record in the dropdown, you would be redirected to the record page.
The user would come to the search page, type a keyword, and make a selection. While the redirect was happening, they would also out of habit click the search button. It would load the new page after a few seconds, and the above-mentioned problems would start.
Cause
Wicket auto-generates DOM ids for all components added to the page so that we don’t have to worry about giving each component a unique ID. In deployment mode, DefaultMarkupIdGenerator tries to optimize the length of these ids by using a constant prefix “id” and appending a session-based sequence number.
In rare cases such as described above, when a behavior is invoked that changes the state of the page which requires more generation of ids while a redirect to a different page is happening, which itself is generating ids, some type of conflict causes duplicate ids to be generated.
With duplicate ID’s, the Wicket callbacks that are registered would not work. Sometimes it would invoke the wrong callback, or not invoke at all.
Solution
The solution I implemented was simply to modify the id generation to use the component ID and the component’s parent ID:
String markupIdPostfix = Integer.toHexString(generatedMarkupId).toLowerCase(Locale.ROOT);
String markupIdPrefix = component.getId();
if(component.getParent() != null) {
markupIdPrefix += component.getParent().getId();
}
String markupId = markupIdPrefix + markupIdPostfix;
The only downside I can see is the slightly increased page size due to the longer ids. I wasn’t able to reproduce this locally until I switched to deployment mode. In development mode, DefaultMarkupIdGenerator uses the component’s ID which makes duplicates much less likely. I went one step further to include the component’s parent ID.
Your chances of encountering this increases as the size of your page increases. If you have a small page with 100 components, it will be very unlikely. However the pages in our application regularly have 2000+ components.
Suggestion
My suggestion would be to change the behavior of DefaultMarkupIdGenerator to use a safer generation method by default and leave it up to the developer if they want to optimize the length of the ids to try to save a bit on their page size.