An important part of any kind of GUI is input validation. If you are living in the Java world, you probably know about Bean Validation and its reference implementation Hibernate Validation. Using Bean Validation you sprinkle your code with annotations in order to declare the maximum size attributes, if they may be Null and many more. It looks like this:
@NotNull
@Size(min = 2, max = 14)
private String licensePlate;
That's not bad. It's declarative and you can use the same annotation for your presentation model and your persistence model. If you use Hibernate for persistence it creates the right kind of constraint for the database as well. But there are a couple of shortcomings.
- If you want to do anything else with the information from the annotation you are forced to use ugly error prone reflection, which is especially bothersome, because a property consists of field getter and setter. So where exactly is the annotation? What if the annotation on the getter contradict the one on the field?
- You can't abstract over the annotations. If comments should always have a minimum length of 5 and a maximum length of 500 there is no way to put this information in a proper abstraction. The best you can do is to define two constants.
- It doesn't work with my Property class I introduced earlier.
The last point is obviously my own problem, but it also provides a starting point for a solution for the other shortcomings.
For those too lazy to read the referenced article, let me reintroduce the Property class. You define a Property like this:
val age = new Property(20)
This line of code is pretty much equivalent to this java code (with pcs being an instance of PropertyChangeSupport):
int age = 20;
public void setAge(int theAge){
int oldAge = age
age = theAge;
pcs.firePropertyChange("age", oldAge, age)
}
public int getAge(){
return age;
}
You can set its value (with p being an instance of what ever class contains the code above):
p := 63
You can retrieve its value:
val ageOfP = p()
And you can register a listener on it
p.age.registerListener(println(_))
Back to validation. For age I only want to allow values between 9 and 99. How would you like to check if a given age is valid? Well, I would like to ask the property!
if (p.age.valid()) doSomething()
else doSomethingElse()
If I want to know what is wrong with the age value, I'd like to ask the property for a list of validation messages like this:
val problems = p.age.validationMessages()
All you have to do for this, is to add a trait to the age Property
val age = new Property(20) with Size { min = 9; max = 99 }
You can properly abstract over constraints as well by defining you own Property classes:
class Name(value : String) extends Property(value) with Length { min = 3; max = 20 }
val firstname = new Name("")
val lastname = new Name("")
And it is easy to define your own Validations. Have look at the implementation of Length
trait Length extends Validation[String] {
self : Property[String] =>
var min : Int = -1
var max : Int = Int.MaxValue
override def validate : List[String] = minimumValidation ::: maximumValidation ::: super.validate
def minimumValidation = if (value != null && value.length < min) List("Should have minimum length " + min) else List()
def maximumValidation = if (value != null && value.length > max) List("Should have maximum length " + max) else List()
}
There are a couple of simple rules for implementing a Validation:
- It has to have a selftype of Property
- You must override the method validate
- And when doing so you have to take care to append your validation messages to the results of super.validate. By doing this you make sure you can combine different validation traits.
validationMessages and valid aren't methods but are Properties themselves. Therefor you can register listeners with them and bind your swing GUI to it. For example the following code snippet binds the validationMessages and valid to the tooltip and the visibility of label, resulting in a validation marker for a Property
def bindValidation(validation : Validation[_], component : JComponent) {
component.setVisible(!validation.valid)
validation.valid.registerListener(valid => component.setVisible(!valid))
def setToolTip(messages : List[String]) {
component.setToolTipText(
{
messages match {
case x :: xs =>
"" + (x :: xs).mkString("
") + ""
case _ => null
}
})
}
setToolTip(validation.validationMessages())
validation.validationMessages.registerListener(setToolTip)
}
If you are interested in more source code: Everything is up at github under the working title "moreThanProperties". You can also find other related articles by looking for the tag moreThanProperties.
Talks
Wan't to meet me in person to tell me how stupid I am? You can find me at the following events: