I noticed a Twitter poll a few weeks ago asking Java developers what is their preferred server-side UI framework. The winner was Spring MVC/Thymeleaf by a wide margin with Wicket far behind.
Here I want to take a concrete example and show how you might implement it in Wicket compared to Spring MVC/Thymeleaf. I mainly want to focus on the ease of writing the code, the amount of code, and maintainability.
DISCLAIMER: I’ve been using Wicket professionally for 8 years and have only been working with Spring MVC/Thymeleaf on side-projects for 6 months. So please correct me!
The example
For the initial code-comparison let’s keep it simple. We will create a form for users to add a car to our automobile database, specifying the year, make, and model:
Let’s start with the Wicket implementation using Wicket-UI. The code will not be 100% complete for brevity, but you can find the full source code on GitHub.
The Wicket version
<form wicket:id="carForm" class="form-horizontal">
<span wicket:id="year"></span>
<span wicket:id="make"></span>
<span wicket:id="model"></span>
<div class="clearfix">
<div class="col-sm-offset-4 col-sm-8">
<button wicket:id="saveButton" class="btn btn-primary" type="submit">Save</button>
</div>
</div>
</form>
Form<Car> carForm = new Form<Car>("carForm");
add(carForm);
feedbackPanel = new FeedbackPanel("feedback", new ContainerFeedbackMessageFilter(carForm));
add(feedbackPanel.setOutputMarkupId(true));
carForm.add(new NumberSpinnerField<Integer>(Builder.of("year", "Year", objModel(newCar::getYear, newCar::setYear)).inputType(Integer.class).min(1920).max(2020).build()));
carForm.add(new TxtField<String>(Builder.of("make", "Make", objModel(newCar::getMake, newCar::setMake)).build()));
carForm.add(new TxtField<String>(Builder.of("model", "Model", objModel(newCar::getModel, newCar::setModel)).build()));
carForm.add(new SingleClickIndicatingAjaxButton("saveButton", carForm, true, null) {
@Override protected void onSubmit(AjaxRequestTarget target) {
String errMsg = NewCarValidator.validate(newCar);
if(errMsg != null) {
error(errMsg);
target.add(feedbackPanel);
return;
}
carRepository.save(newCar);
}});
The Spring MVC version
<script type="text/javascript">
$(document).ready(function() {
$("#carForm").submit(function (event) {
$.ajax({
type: "POST", url: '/cars/add', data: $('#carForm').serialize(),
beforeSend: function (xhr) {
var token = $('#_csrf').attr('content');
var header = $('#_csrf_header').attr('content');
xhr.setRequestHeader(header, token);
},
success: function (data) {
}
});
return false;
});
});
</script>
<form role="form" id="carForm" th:action="@{/cars/add}" th:object="${car}" method="post">
<div class="form-group row">
<label for="year" class="col-sm-2 col-form-label">Year</label>
<div class="col-sm-3">
<input type="number" class="form-control" th:field="*{year}">
</div>
</div>
<div class="form-group row">
<label for="year" class="col-sm-2 col-form-label">Make</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{make}">
</div>
</div>
<div class="form-group row">
<label for="year" class="col-sm-2 col-form-label">Model</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{model}">
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
@Controller
public class CarController {
@GetMapping("/cars/add")
public ModelAndView carForm(Model model) {
return new ModelAndView("add_car", Map.of("car", new Car()));
}
@PostMapping("/cars/add")
public ModelAndView addCar(@ModelAttribute("car") @Valid Car car, BindingResult bindingResult) {
NewCarValidator.validate(car, bindingResult);
if (bindingResult.hasErrors()) {
return new ModelAndView("add_car", Map.of("car", car));
}
carRepository.save(newCar);
return new ModelAndView(new RedirectView("/cars/add"));
}
}
Let’s compare
We can see that in this trivial example, the two versions are similar. I still prefer the Wicket version because it’s much more Java-centric. I don’t have to write any client-side Javascript and the HTML is much simpler.
Another disadvantage in the Spring version we are binding the field values to the Car object using strings which is error-prone.
Let’s add complexity
Real-world applications are rarely this simple, so let’s add a bit of complexity to our app. You will see this is where Wicket really shines!
Our automobile database now stores a list of cars that are considered rare. For example the 1942 Chevrolet Special DeLuxe Fleetline. When the user is adding their car, if we determine that it’s rare, we want to collect additional information. We want the mileage and condition. If the condition is really bad, we also want to know if the engine starts.
You might be surprised how little the Wicket version changes.
The Wicket Version
<form wicket:id="carForm" class="form-horizontal">
<span wicket:id="year"></span>
<span wicket:id="make"></span>
<span wicket:id="model"></span>
<span wicket:id="rareCar">
<span wicket:id="mileage"></span>
<span wicket:id="condition"></span>
<span wicket:id="engineStarts"></span>
</span>
<div class="clearfix">
<div class="col-sm-offset-4 col-sm-8">
<button wicket:id="saveButton" class="btn btn-primary" type="submit">Save</button>
</div>
</div>
</form>
rareCar = new WebMarkupContainer("rareCar") {
@Override
public void onConfigure() {
super.onConfigure();
setVisible(isRareCar(newCar));
}
};
carForm.add(rareCar.setOutputMarkupPlaceholderTag(true));
carForm.add(new AjaxNumberSpinnerField<Integer>(Builder.of("year", "Year", objModel(newCar::getYear, newCar::setYear)).inputType(Integer.class).min(1920).max(2020).build()) {
public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});
carForm.add(new AjaxTxtField<String>(Builder.of("make", "Make", objModel(newCar::getMake, newCar::setMake)).build()) {
public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});
carForm.add(new AjaxTxtField<String>(Builder.of("model", "Model", objModel(newCar::getModel, newCar::setModel)).build()) {
public void onFieldChanged(AjaxRequestTarget target) { target.add(rareCar); }
});
rareCar.add(new TxtField<Integer>(Builder.of("mileage", "Mileage", objModel(newCar::getMileage, newCar::setMileage)).build()));
rareCar.add(new AjaxDropdownField<CarCondition>(Builder.of("condition", "Condition", objModel(newCar::getCondition, newCar::setCondition))
.choiceList(new ListModel<CarCondition>(CarCondition.VALUES)).cr(UIHelpers.getEnumChoiceRenderer()).build()) {
public void onFieldChanged(AjaxRequestTarget target) { target.add(engineStartsField); }
});
rareCar.add(engineStartsField = new CheckBoxField(Builder.of("engineStarts", "Engine Starts?", objModel(newCar::getEngineStarts, newCar::setEngineStarts)).build()) {
@Override
public void onConfigure() {
super.onConfigure();
setVisible(newCar.getCondition() == CarCondition.BAD);
}
});
As you can see the main difference is we added a new container for storing the new conditional fields in case the car is rare. Some of the fields have also become ‘Ajax-enabled’. It’s important to note, we still haven’t written any JavaScript and the HTML remains virtually the same!
The Spring MVC version
Ok, we have a lot of work to do 馃檪 We’re going to need to write a bunch of boilerplate Javascript code. On the Java side, it’s not too bad. It only changes a bit.
<script type="text/javascript">
$(document).ready(function() {
$("#carForm").submit(function (event) {
$.ajax({
type: "POST", url: '/cars/add', data: $('#carForm').serialize(),
beforeSend: function (xhr) {
var token = $('#_csrf').attr('content');
var header = $('#_csrf_header').attr('content');
xhr.setRequestHeader(header, token);
},
success: function (data) {
}
});
return false;
});
});
$("#year").change(function() {
carFuncs.checkRareCar();
});
$("#make").change(function() {
carFuncs.checkRareCar();
});
$("#model").change(function() {
carFuncs.checkRareCar();
});
carFuncs.checkRareCar = function () {
$.ajax({
type: "POST", url: '/cars/check_rare', data: $('#carForm').serialize(),
success: function (data) {
if(data.status == "rare") {
$('#rareCar').show();
} else {
$('#rareCar').hide();
}
}
});
};
$( "#condition" ).change(function() {
if(this.value == 'bad') {
$('#engineStartsField').show();
} else {
$('#engineStartsField').hide();
}
});
</script>
<form role="form" id="carForm" th:action="@{/cars/add}" th:object="${car}" method="post">
<div class="form-group row">
<label for="year" class="col-sm-2 col-form-label">Year</label>
<div class="col-sm-3">
<input type="number" class="form-control" th:field="*{year}">
</div>
</div>
<div class="form-group row">
<label for="make" class="col-sm-2 col-form-label">Make</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{make}">
</div>
</div>
<div class="form-group row">
<label for="model" class="col-sm-2 col-form-label">Model</label>
<div class="col-sm-3">
<input type="text" class="form-control" th:field="*{model}">
</div>
</div>
<span id="rareCar">
<div class="form-group row">
<label for="mileage" class="col-sm-2 col-form-label">Milage</label>
<div class="col-sm-3">
<input type="number" class="form-control" th:field="*{mileage}">
</div>
</div>
<div class="form-group row">
<label for="condition" class="col-sm-2 col-form-label">Condition</label>
<div class="col-sm-3">
<select class="form-control" th:field="*{condition}" required>
<option value="0">Select</option>
<option th:each="c : ${conditions}" th:value="${c.id}" th:text="${c.value}"></option>
</select>
</div>
</div>
<div class="form-row" id="engineStartsField">
<div class="form-group col-md-6">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" th:field="*{engineStarts}">
<label class="custom-control-label" for="engineStarts">Engine starts</label>
</div>
</div>
</div>
</span>
<div class="form-group row">
<div class="col-sm-3">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
@Controller
public class CarComplexController {
@GetMapping("/cars/add")
public ModelAndView carForm(Model model) {
return new ModelAndView("add_car", Map.of("car", new Car()));
}
@PostMapping("/cars/add")
public ModelAndView addCar(@ModelAttribute("car") @Valid Car car, BindingResult bindingResult) {
NewCarValidator.validate(car, bindingResult);
if (bindingResult.hasErrors()) {
return new ModelAndView("add_car", Map.of("car", car));
}
carRepository.save(newCar);
return new ModelAndView(new RedirectView("/cars/add"));
}
@GetMapping("/cars/check_rare")
public @ResponseBody AjaxResponse checkIsCarRare(@ModelAttribute("car") Car car) {
boolean isRare = carService.isRare(car);
return AjaxResponse.success("Success", isRare ? "rare" : "not_rare");
}
}
Let’s compare
So what we can see is that as the UI and business logic/requirements become more complex, the size of the client code in the Spring version explodes. There is also a lot of reliance on string mappings which becomes very error-prone.
In the Wicket version, as the complexity increases the amount of client code remains almost the same. We can also easily write Java unit tests for the Wicket version. This is because in Wicket everything is object-based which leads to easy refactorings, easy mocking, and so on. This would be very difficult to do in the Spring version.
Let’s also imagine we will be selling our software to many different dealerships and car collectors. We may have to customize it for each one. We’ve already seen how simple this is using Wicket. I haven’t had to do this with Spring MVC, but I would imagine it will be difficult.
Conclusion
Even our ‘complex’ example is fairly simple compared to real-world applications. I believe the more complex the UI and business requirements become, the more productivity gains you will realize from using Wicket. Keep in mind that you can still use the full power of Spring framework when developing Wicket apps.
Spring MVC may be a better choice in cases where the UI might not be that complex and requiring the maximum speed and performance is critical.