In
part 1 we discussed the basics of properties in Sgine, how to listen for events, what a delegate property is, how to filter properties, transactions with properties, and animation. In this part we will conclude our tutorial of properties in Sgine with a discussion of paths in properties, binding support, property containers, dependent property, and advanced property.
Path-based Properties
In Sgine there is a unique path system. However, this is not a typical path system because it represents object paths, not filesystem paths. In the org.sgine.path package there is a class called OPath (object path) that can represent the hierarchical path to an object. So, for example, take the following code:
import org.sgine.property.MutableProperty
class Person {
val addresses = new MutableProperty[Addresses]
}
class Addresses {
val billing = new MutableProperty[Address]
val shipping = new MutableProperty[Address]
}
class Address {
val line1 = new MutableProperty[String]
}
Now, presume I have a reference to an instance of a person object, and I want to listen for any changes to address line1 on the shipping address. This is difficult as "addresses" is mutable, "shipping" is mutable, and "line1" is mutable. So, if I do the following:
person.addresses().shipping().line1.listeners += shippingLine1Changed _
I may get a NullPointerException because something isn't initialized, or I may connect up, but when any one of the mutable chains in-between break I'm now referencing the wrong data. This is where OPath comes in. I can pass a root reference and a path as a String and it can resolve the underlying object and return Option[T]. So for example, if I want to resolve the value as above with an OPath I can invoke:
val o = OPath.resolve[String](person, "addresses().shipping().line1()")
The value of "o" will be Some[String] representing value of line1 or None if any point in the hierarchy is not able to be resolved.
Now, back to properties. The PathProperty works with OPath instances (calling resolve like above simply resolves the value, but an OPath instance is a long-term reference to that path that receives events when anything changes). So if we wanted to create a property and know when the value of line1 in the shipping address has changed we could do the following:
val shippingLine1 = new PathProperty[String](OPath(person, "addresses().shipping().line1()"))
shippingLine1.listeners += shippingLine1Changed _
Your listener would look like this:
private def shippingLine1Changed(evt: PropertyChangeEvent[String]) = {
... shipping line1 has changed ...
}
Binding
I mentioned earlier that events were probably the most used feature in properties, but binding is likely a close second. The premise of binding is fairly simple. You have two values and you want one or both to replicate the value of the other. Newer frameworks like Flex and JavaFX make heavy use of binding and do a decent job of it. However, there's a lot that Sgine provides that they have no ability to do. Alas, I'm getting ahead of myself. Let's first take a look at a simple example. Consider the standard UI usage of binding. We have a CRUD app that manages people, so we have a text field that when updated needs to modify the name of the specific person we are working with. We must first modify our Person class to support binding:
import org.sgine.property.BindingProperty
import org.sgine.property.MutableProperty
class Person {
val name = new MutableProperty[String] with BindingProperty[String]
}
Now, presuming we have a text field with signature like the following:
class TextField {
val text = new MutableProperty[String] with BindingProperty[String]
}
We can simply take our Person's name and bind it to the text field's text property:
person.name bind nameField.text
Now any changes that occur to the nameField will automatically be applied back to person.name. This is the simple case and pretty much the extent to which most binding systems function. However, as I stated before, in Sgine binding support goes well beyond that.
The first problem you often run into with standard binding functionality is that you can only bind the same type of properties together. For example, in the previous case we were binding the name of a person to the text value of a text field. These both were String representations, but what if we wanted to do something like have an "age" property that is bound to a "birthday" property? We have an Int that wants to link up to a Calendar? This brings us to translation bindings.
Translation Binding
Translation binding is very similar to standard binding except there is a translation step that must occur to get the value ready for use in the target.
Lets look at the age / birthday example:
import java.util.Calendar
import org.sgine.property.BindingProperty
import org.sgine.property.MutableProperty
class Person {
val age = new MutableProperty[Int] with BindingProperty[Int]
val birthday = new MutableProperty[Calendar] with BindingProperty[Calendar]
age bind(birthday, determineAge)
private def determineAge(c: Calendar) = {
if (c != null) {
val now = Calendar.getInstance
val age = now.get(Calendar.YEAR) - c.get(Calendar.YEAR)
now.set(Calendar.YEAR, c.get(Calendar.YEAR))
if (now before c) {
age - 1
} else {
age
}
} else {
-1
}
}
}
We simply bind birthday to age, but at the same time provide a function to convert from a Calendar to an Int. Now every time birthday changes, age gets updated.
Path-based Binding
As we've already discussed, OPath in Sgine represents an object-path to a value within a hierarchical structure. We've already seen it used to resolve values directly and with PathProperty. Now we're going to look at one of the most powerful uses of OPath, and that's for binding. Often you want to bind to a hierarchical value that may or may not exist at time of instantiation. The solution to this generally is to add a listener, to wait until it has been defined, and then add the binding. Later though, the reference structure may change or be set back to null and you find yourself with a binding to something that is no longer being used. With path-based bindings you can bind to a hierarchical structure and no longer concern yourself with the details of parts changing in-between. Every time the path changes the binding will be updated.
For example:
import org.sgine.path.OPath
import org.sgine.property.BindingProperty
import org.sgine.property.MutableProperty
class Person {
val lastName = new MutableProperty[String] with BindingProperty[String]
val spouse = new MutableProperty[Person] with BindingProperty[Person]
}
Now, consider that this person has stated they want their lastName to be bound to that of their spouse's. We could do this:
person.lastName bind person.spouse().lastName
But it's possible they are not yet married, and it's also possible that the spouse might change. If we do a path-based binding instead, no matter if they are married now or change spouses in the future, the binding will remain pointing to their spouse's lastName:
person.lastName bindPath OPath(person, "spouse().lastName()")
Though a little hard to grasp at first, the benefits of such functionality significantly reduce the amount of handling that must be added in order to deal with multi-level mutability.
Property Container
Properties by themselves are quite useful little beasts, but add PropertyContainer to this mix and the benefits multiply. One of the troubles you have with the standard bean scenario is introspecting the contents of that bean in order to find out what values it contains in order to make use of it. Inevitably Reflection is used to determine the fields or methods that make up that bean. PropertyContainer eliminates the need for manual Reflection.
The ability to know what properties exist for a specific instance can be very useful. Rather than pushing back the necessity to use Reflection onto the developer, PropertyContainer dynamically introspects the instance in order to determine what properties are available and by what name they can be referenced. For example:
import org.sgine.property.MutableProperty
import org.sgine.property.container.PropertyContainer
class Person extends PropertyContainer {
val name = new MutableProperty[String]
}
Now, I can iterate over all the properties of my "person" with:
for (prop <- person.properties) {
...
}
I can also do a lookup of a property by name:
val name = person("name")
Another very useful feature to PropertyContainers is that they are also Listenable instances, so they can receive events. This gives you the ability to listen for events on the container and receive events for the children. See the following example:
import org.sgine.property.ListenableProperty
import org.sgine.property.MutableProperty
import org.sgine.property.container.PropertyContainer
class Person extends PropertyContainer {
val name = new MutableProperty[String] with ListenableProperty[String] {
override val parent = Person.this
}
}
It's important to notice here the only revision that needed take place is that "name" provide a reference to the Person instance via the "parent" property. This is used to propagate the event up. This can allow a complete hierarchical structure to propagate all the way up to the top. Now we can add a listener with the following:
import org.sgine.event.EventHandler
import org.sgine.event.Recursion
import org.sgine.property.event.PropertyChangeEvent
val person = new Person()
person.listeners += EventHandler(propertyChanged, recursion = Recursion.Children)
private def propertyChanged(evt: PropertyChangeEvent[_]) = {
... property changed ...
}
The above example will be invoked if any child throws a PropertyChangeEvent.
Finally, beyond the static PropertyContainer we also may need to add and remove properties dynamically. This is where MutablePropertyContainer comes in. MutablePropertyContainer, as the name suggests, allows dynamically adding and removing properties on the fly rather than relying on Reflection in order to determine names and references.
Dependent Properties
DependentProperty allows for the default value to point to another property instead of the initial value of that property. For example:
import org.sgine.property.DependentProperty
import org.sgine.property.MutableProperty
class Person {
val name = new MutableProperty[String] with DependentProperty[String] {
val dependency = Person.defaultName
}
}
object Person {
val defaultName = new MutableProperty[String]("Anonymous")
}
The value of person.name is defaulted to point to whatever the value of Person.defaultName is. Even if the value of "defaultName" should change after initialization it will continue to point to it until the value of the "name" property itself has been changed. This is particularly useful for stylization as it allows default styles to exist but without removing the ability to explicitly override.
AdvancedProperty
In the examples above there is a lot of mixing of traits to achieve the desired functionality, but in most cases there is no reason not to make all of this functionality available. For simplification the AdvancedProperty combines all the standard functionality that might be desired in a property without sacrificing performance.
The traits that AdvancedProperty mixes in by default are:
- MutableProperty
- DepedentProperty
- ListenableProperty
- NamedProperty
- BindingProperty
- AnimatingProperty
- EventDelegationProperty
- CombiningProperty
Simply instantiating AdvancedProperty is much easier than concerning yourself with every possibly trait that might be needed on a given property and in general there's nothing lost by doing so. AdvancedProperty does not contain all the available traits for properties, but additional traits can be mixed in as needed.
To use AdvancedProperty is quite simple:
import org.sgine.property.AdvancedProperty
class Person {
val name = new AdvancedProperty[String]("")
}
This gives you all the functionality you are likely to need in "name". If you are taking advantage of PropertyContainer you can use the following signature instead:
import org.sgine.property.AdvancedProperty
import org.sgine.property.container.PropertyContainer
class Person extends PropertyContainer {
val name = new AdvancedProperty[String]("", this)
}
Conclusion
This has been a lengthy tutorial about the functionality that is provided in Sgine's Property system, but hopefully provides the foundation necessary for you to start taking advantage of Properties in your projects. The focus of Sgine is very much toward 3d engine development, but much of the foundation is geared toward general usage. To that end it is my desire that functionality such as the Property system may be leveraged in diverse systems that may have nothing to do with graphical programming at all.
Though this was a lengthy examination of properties it did not go over every feature and trait available in Sgine. For additional information please refer to the
ScalaDocs for Sgine.