AUI Framework
develop
Cross-platform base for C++ UI apps
|
Property System is a data binding mechanism based on signal-slot system.
AUI property system, a compiler-agnostic alternative to __property or [property]. Based on signal-slot system for platform-independent C++ development. Unlike Qt, AUI's properties don't involve external tools (like moc
). They are written in pure C++.
AUI property system is relatively complex, as it involves a lot of features in a single place:
Learning curve is relatively flat, so be sure to ask questions and open issues on our GitHub page.
Main difference between basic value lying somewhere inside your class and a property is that the latter explicitly ties getter, setter and a signal reporting value changes. Property acts almost transparently, as if there's no extra wrapper around your data. This allows to work with properties in the same way as with their underlying values. You can read the intermediate value of a property and subscribe to its changes via a single connect
call. Also, when connecting property to property, it is possible to make them observe changes of each other bia biConnect
call:
Or simpler:
The code above generates a window with a text field:
A single call of biConnect
:
user->name
value (pre fire): user->named.changed
to tf
to notify the text field about changes of user->name
:
tf->text().changed
to notify the user->name
property about changes in text field (i.e., if the user typed another value to the text field): This is basic example of setting up property-to-property connection. ALogger::info("LogObserver") << "Received value: " << msg;
There are three ways to define a property in AUI:
To declare a property inside your data model, use AProperty template:
AProperty<T>
is a container holding an instance of T
. You can assign a value to it with operator=
and read value with value()
method or implicit conversion operator T()
. AProperty behaves like a class/struct data member:
You can even perform binary operations on it seamlessly:
In most cases, property is implicitly convertible to its underlying type:
If it doesn't, simply put an asterisk:
All property types offer .changed
field which is a signal reporting value changes. Let's make little observer object for demonstration:
Example usage:
At the moment, the program prints nothing. When we change the property:
Code produces the following output:
As you can see, observer received the update. But, for example, if we would like to display the value via label, the label wouldn't display the current value until the next update. We want the label to display current value without requiring an update. To do this, connect to the property directly, without explicitly asking for changed
:
Code above produces the following output:
As you can see, observer receives the value without making updates to the value. The call of LogObserver::log
is made by AObject::connect
itself. In this document, we will call this behaviour as "pre-fire".
Subsequent changes to field would send updates as well:
Assignment operation above makes an additional line to output:
Whole program output when connecting to property directly:
You can use this way if you are required to define custom behaviour on getter/setter. As a downside, you have to write extra boilerplate code: define property, data field, signal, getter and setter checking equality. Also, APropertyDef requires the class to derive AObject
. Most of AView's properties are defined this way.
To declare a property with custom getter/setter, use APropertyDef template. APropertyDef-based property is defined by const member function as follows:
APropertyDef behaves like a class/struct function member:
()
. We can't get rid of them, as APropertyDef is defined thanks to member function. In comparison to user->name
, think of user->name()
as the same kind of property except defining custom behaviour via function, hence the braces ()
.For the rest, APropertyDef is identical to AProperty including seamless interaction:
APropertyDef
calls getter/setter instead of using +=
on your property directly. Equivalent code will be: The implicit conversions work the same way as with AProperty:
Close to AProperty
:
Code produces the following output:
Making connection to property directly instead of .changed
:
Code above produces the following output:
Subsequent changes to field would send updates as well:
Assignment operation above makes an additional line to output:
Whole program output when connecting to property directly:
Despite properties offer projection methods, you might want to track and process values of several properties.
APropertyPrecomputed<T>
is a readonly property similar to AProperty<T>
. It holds an instance of T
as well. Its value is determined by the C++ function specified in its constructor, typically a C++ lambda expression.
It's convenient to access values from another properties inside the expression. The properties accessed during invocation of the expression are tracked behind the scenes so they become dependencies of APropertyPrecomputed
automatically. If one of the tracked properties fires changed
signal, APropertyPrecomputed
invalidates its T
. APropertyPrecomputed
follows lazy semantics so the expression is re-evaluated and the new result is applied to APropertyPrecomputed
as soon as the latter is accessed for the next time.
In other words, it allows to specify relationships between different object properties and reactively update APropertyPrecomputed
value whenever its dependencies change. APropertyPrecomputed<T>
is somewhat similar to Qt Bindable Properties.
APropertyPrecomputed
is a readonly property, hence you can't update its value with assignment. You can get its value with value()
method or implicit conversion operator T()
as with other properties.
The example above prints "Emma Watson". If we try to update one of dependencies of APropertyPrecomputed
(i.e., name
or surname
), APropertyPrecomputed
responds immediately:
The example above prints "Emma Stone".
Similar to AProperty
.
Any C++ callable evaluating to T
can be used as an expression for APropertyPrecomputed<T>
. However, to formulate correct expression, some rules must be satisfied.
Dependency tracking only works on other properties. It is the developer's responsibility to ensure all values referenced in the expression are properties, or, at least, non-property values that wouldn't change or whose changes are not interesting. You definitely can use branching inside the expression, but you must be confident about what are you doing. Generally speaking, use as trivial expressions as possible.
In this expression, we have a fast path return if name
is empty.
As soon as we set name
to ""
, we don't access surname
. If we try to trigger the fast path return:
surname
can't trigger re-evaluation anyhow. Re-evaluation can be triggered by name
only. So, at the moment, we are interested in name
changes only.
APropertyPrecomputed
might evaluate its expression several times during its lifetime. The developer must make sure that all objects referenced in the expression live longer than APropertyPrecomputed
.
The expression should not read from the property it's a binding for. Otherwise, there's an infinite evaluation loop.
This approach allows more control over the binding process by using AObject::connect
/AObject::biConnect
which is a procedural way of setting up connections. As a downside, it requires "let" syntax clause which may seem as overkill for such a simple operation.
Use let
expression to connect the model's username property to the label's text() property.
This gives the following result:
Note that label already displays the value stored in User.
Let's change the name:
By simply performing assignment on user
we changed ALabel display text. Magic, huh?
It's fairly easy to define a projection because one-sided connection requires exactly one projection, obviously.
This gives the following result:
Note that the label already displays the projected value stored in User.
Let's change the name:
This way, we've set up data binding with projection.
In previous examples, we've used AObject::connect
to make one directional (one sided) connection. This is perfectly enough for ALabel because it cannot be changed by user.
In some cases, you might want to use property-to-property as it's bidirectional. It's used for populating view from model and obtaining data from view back to the model.
For this example, let's use ATextField instead of ALabel as it's an editable view. In this case, we'd want to use AObject::biConnect
because we do want user->name
to be aware of changes of the view.
This gives the following result:
Let's change the name programmatically:
ATextField will respond:
If the user changes the value from UI, these changes will reflect on user->model
as well:
This way we've set up bidirectional projection via AObject::biConnect
which makes user->name
aware of UI changes.
Bidirectional connection updates values in both directions, hence it requires the projection to work in both sides as well.
It is the case for ADropdownList with enums. ADropdownList works with string list model and indices. It does not know anything about underlying values.
For example, define enum with AUI_ENUM_VALUES and model:
Now, let's get a mapping for our Gender
enum:
The compile-time constant above is equivalent to:
We just using aui::enumerate::ALL_VALUES
because it was provided conveniently by AUI_ENUM_VALUES
for us.
It's not hard to guess that we'll use indices of this array to uniquely identify Gender
associated with this index:
To perform opposite operation (i.e., Gender
to int), we can use aui::indexOf
:
To bring these conversions together, let's use overloaded lambda:
... -> int
, ... -> Gender
) to make it obvious what do transformations do and how one type is transformed to another.The function-like object above detects the direction of transformation and performs as follows:
It is all what we need to set up bidirectional transformations. Inside AUI_ENTRY:
user->gender
programmatically, ADropdownList will respond: As said earlier, let
syntax is a little bit clunky and requires extra boilerplate code to set up.
Here's where declarative syntax comes into play. The logic behind the syntax is the same as in AObject::connect
/AObject::biConnect
(for ease of replacement/understanding).
Declarative syntax uses &
and &&
operators to set up connections. These were chosen intentionally: &&
resembles chain, so we "chaining view and property up".
&
sets up one-directional connection (AObject::connect
).&&
sets up bidirectional connection (AObject::biConnect
).Also, >
operator (resembles arrow) is used to specify the destination slot.
The example below is essentially the same as Label via let but uses declarative connection set up syntax.
Use &
and >
expression to connect the model's username property to the label's text property.
Note that the label already displays the value stored in User.
Let's change the name:
In this example, we've achieved the same intuitive behaviour of data binding of user->name
(like in Label via let example) but using declarative syntax. The logic behind &
is almost the same as with let
and AObject::connect
so projection use cases can be adapted in a similar manner.
In previous example we have explicitly specified ALabel's property to connect with.
One of notable features of declarative way (in comparison to procedural let
way) is that we can omit the view's property to connect with if such ADataBindingDefault
specialization exist for the target view and the property type. Some views have already predefined such specialization for their underlying types. For instance, ALabel has such specialization:
We can use this predefined specialization to omit the destination property:
Behaviour of such connection is equal to Label via declarative:
Note that the label already displays the value stored in User.
Let's change the name:
In this example, we've omitted the destination property of the connection while maintaining the same behaviour as in Label via declarative.
Think of ADataBindingDefault
as we're not only connecting properties to properties, but also creating a "property to view" relationship. This philosophy covers the following scenario.
In AUI, there's aui::ranged_number template which stores valid value range right inside the type:
These strong types can be used to propagate their traits on views, i.e., ANumberPicker. When using declarative syntax, the property system calls ADataBindingDefault::setup
to apply some extra traits of the bound value on the view. Here's an abstract on how ANumberPicker
defines specialization of ADataBingingDefault
with aui::ranged_number
:
As you can see, this specialization pulls the min and max values from aui::ranged_number
type and sets them to ANumberPicker
. This way ANumberPicker
finds out the valid range of values by simply being bound to value that has constraints encoded inside its type.
operator&&
here to set up bidirectional connection. For more info, go to Declarative bidirectional connection.By creating this connection, we've done a little bit more. We've set ANumberPicker::setMin and ANumberPicker::setMax as well:
This example demonstrates how to use declarative binding to propagate strong types. aui::ranged_number
propagates its constraints on ANumberPicker
thanks to ADataBindingDefault
specialization.
We can use projections in the same way as with let
.
Note that the label already displays the projected value stored in User.
Projection applies to value changes as well. Let's change the name:
In previous examples, we've used &
to make one directional (one sided) connection. This is perfectly enough for ALabel because it cannot be changed by user.
In some cases, you might want to use property-to-property as it's bidirectional. It's used for populating view from model and obtaining data from view back to the model.
For this example, let's use ATextField instead of ALabel as it's an editable view. In this case, we'd want to use &&
because we do want user->name
to be aware of changes of the view.
This gives the following result:
Let's change the name programmatically:
ATextField will respond:
If the user changes the value from UI, these changes will reflect on user->model
as well:
This way we've set up bidirectional projection via &&
which makes user->name
aware of UI changes.
We can use projections in the same way as with let
.
Let's repeat the Bidirectional projection sample in declarative way:
&&
operator here instead of &
because we want the connection work in both directions: user.gender -> ADropdownList
and ADropdownList -> user.gender
.user->gender
programmatically, ADropdownList will respond: Classes | |
struct | AProperty< T > |
Basic easy-to-use property implementation containing T. More... | |
struct | APropertyDef< M, Getter, Setter, SignalArg > |
Property implementation to use with custom getter/setter. More... | |
|
inlinestatic |
Connects source property to the destination property and opposite (bidirectionally).
Connects propertySource.changed
to the setter of propertyDestination
. Additionally, sets the propertyDestination
with the current value of the propertySource
(pre-fire). Hence, initial dataflow is from left argument to the right argument.
After pre-fire, connects propertyDestination.changed
to the setter of propertySource
. This way, when propertyDestination
changes (i.e, propertyDestination
belongs to some view and it's value is changed due to user action) it immediately reflects on propertySource
. So, propertySource
is typically a property of some view model with prefilled interesting data, and propertyDestination is a property of some view whose value is unimportant at the moment of connection creation.
biConnect pulls AObject from propertySource
and propertyDestination
to maintain the connection.
See signal-slot system for more info.
propertySource | source property, whose value is preserved on connection creation. |
propertyDestination | destination property, whose value is overwritten on connection creation. |
|
inlinestatic |
Connects signal or property to the slot of the specified non-AObject type.
See signal-slot system for more info.
property | source property. |
object | instance of AObject . |
function | slot. Can be lambda. |
|
inlinestatic |
Connects property to the slot of the specified object.
Connects to "changed" signal of the property. Additionally, calls specified function with the current value of the property (pre-fire).
See signal-slot system for more info.
property | property |
object | instance of AObject |
function | slot. Can be lambda |
|
inlinestatic |
Connects source property to the destination property.
Connects propertySource.changed
to the setter of propertyDestination
. Additionally, sets the propertyDestination
with the current value of the propertySource
(pre-fire). Hence, dataflow is from left argument to the right argument.
connect pulls AObject from propertyDestination
to maintain the connection.
See signal-slot system for more info.
propertySource | source property, whose value is preserved on connection creation. |
propertyDestination | destination property, whose value is overwritten on connection creation. |