AUI Framework  develop
Cross-platform base for C++ UI apps
Loading...
Searching...
No Matches
Property System

Detailed Description

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:

  1. thread safe
  2. many-to-many relationships between objects
  3. optional data modification when passing values between objects (like STL projections)
  4. emitter can be either signal or property
  5. slot can be either lambda, method or property
  6. for the latter case, system must set up backward connection as well (including projection support)
  7. again, for the latter case, there's an option to make property-to-slot connection, where the "slot" is property's assignment operation
  8. 2 syntax variants: procedural (straightforward) and declarative
  9. three property variants: simple field (AProperty), custom getter/setter (APropertyDef) and custom evaluation (APropertyPrecomputed)
  10. some properties can be readonly
  11. propagating strong types' traits on views

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:

struct User {
};
auto user = aui::ptr::manage(new User { .name = "Robert" });
auto tf = _new<ATextField>();
AObject::biConnect(user->name, tf->text());
auto window = _new<AWindow>();
window->setContents(Centered { tf });
window->show();
static void biConnect(PropertySource &&propertySource, PropertyDestination &&propertyDestination)
Connects source property to the destination property and opposite (bidirectionally).
Definition AObject.h:135
Basic easy-to-use property implementation containing T.
Definition AProperty.h:156
static _< T > manage(T *raw)
Delegates memory management of the raw pointer T* raw to the shared pointer, which is returned.
Definition SharedPtrTypes.h:372

Or simpler:

// ...
window->setContents(Centered {
tf && user->name,
});
// ...

The code above generates a window with a text field:

A single call of biConnect:

// user typed "Snezhana", now let's check the value in user->name:
EXPECT_EQ(user->name, "Snezhana");

This is basic example of setting up property-to-property connection. ALogger::info("LogObserver") << "Received value: " << msg;

Declaring Properties

There are three ways to define a property in AUI:

AProperty

To declare a property inside your data model, use AProperty template:

struct User {
};

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:

User u;
u.name = "Hello";
EXPECT_EQ(u.name, "Hello");

You can even perform binary operations on it seamlessly:

User u;
u.name = "Hello";
u.name += " world!";
EXPECT_EQ(u.name, "Hello world!");
EXPECT_EQ(u.name->length(), AString("Hello world!").length());
Represents a Unicode character string.
Definition AString.h:37

In most cases, property is implicitly convertible to its underlying type:

auto doSomethingWithName = [](const AString& name) { EXPECT_EQ(name, "Hello"); };
User u;
u.name = "Hello";
doSomethingWithName(u.name);

If it doesn't, simply put an asterisk:

doSomethingWithName(*u.name);
// ^^^ HERE

Observing changes

All property types offer .changed field which is a signal reporting value changes. Let's make little observer object for demonstration:

class LogObserver : public AObject {
public:
void log(const AString& msg) {
ALogger::info("LogObserver") << "Received value: " << msg;
}
};
A base object class.
Definition AObject.h:39

Example usage:

auto observer = _new<LogObserver>();
auto u = aui::ptr::manage(new User { .name = "Chloe" });
AObject::connect(u->name.changed, slot(observer)::log);
#define slot(v)
Passes some variable and type of the variable separated by comma. It's convenient to use with the con...
Definition kAUI.h:88
static void connect(const Signal &signal, Object *object, Function &&function)
Connects signal to the slot of the specified object.
Definition AObject.h:65

At the moment, the program prints nothing. When we change the property:

u->name = "Marinette";

Code produces the following output:

[07:58:59][][LogObserver][INFO]: Received value: Marinette

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:

auto observer = _new<LogObserver>();
auto u = aui::ptr::manage(new User { .name = "Chloe" });
AObject::connect(u->name, slot(observer)::log);

Code above produces the following output:

[07:58:59][][LogObserver][INFO]: Received value: Chloe

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:

u->name = "Marinette";

Assignment operation above makes an additional line to output:

[07:58:59][][LogObserver][INFO]: Received value: Marinette

Whole program output when connecting to property directly:

[07:58:59][][LogObserver][INFO]: Received value: Chloe
[07:58:59][][LogObserver][INFO]: Received value: Marinette

APropertyDef

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:

class User: public AObject {
public:
auto name() const {
return APropertyDef {
this,
&User::getName, // this works too: &User::mName
&User::setName,
mNameChanged,
};
}
private:
AString mName;
emits<AString> mNameChanged;
void setName(AString name) {
// APropertyDef requires us to emit
// changed signal if value is actually
// changed
if (mName == name) {
return;
}
mName = std::move(name);
emit mNameChanged(mName);
}
const AString& getName() const {
return mName;
}
};
ASignal< Args... > emits
A signal declaration.
Definition ASignal.h:348
#define emit
emits the specified signal in context of this object.
Definition AObject.h:310
Property implementation to use with custom getter/setter.
Definition AProperty.h:308

APropertyDef behaves like a class/struct function member:

User u;
u.name() = "Hello";
EXPECT_EQ(u.name(), "Hello");
Note
Properties defined with APropertyDef instead of AProperty impersonate themselves by trailing braces (). 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:

User u;
u.name() = "Hello";
u.name() += " world!";
EXPECT_EQ(u.name(), "Hello world!");
EXPECT_EQ(u.name()->length(), AString("Hello world!").length());
Note
In order to honor getters/setters, APropertyDef calls getter/setter instead of using += on your property directly. Equivalent code will be:
u.setName(u.getName() + " world!")

The implicit conversions work the same way as with AProperty:

auto doSomethingWithName = [](const AString& name) { EXPECT_EQ(name, "Hello"); };
User u;
u.name() = "Hello";
doSomethingWithName(u.name());
doSomethingWithName(*u.name());

Observing changes

Close to AProperty:

auto observer = _new<LogObserver>();
auto u = _new<User>();
u->name() = "Chloe";
// ...
AObject::connect(u->name().changed, slot(observer)::log);
u->name() = "Marinette";

Code produces the following output:

[07:58:59][][LogObserver][INFO]: Received value: Marinette

Making connection to property directly instead of .changed:

auto observer = _new<LogObserver>();
auto u = _new<User>();
u->name() = "Chloe";
// ...
AObject::connect(u->name(), slot(observer)::log);

Code above produces the following output:

[07:58:59][][LogObserver][INFO]: Received value: Chloe

Subsequent changes to field would send updates as well:

u->name() = "Marinette";

Assignment operation above makes an additional line to output:

[07:58:59][][LogObserver][INFO]: Received value: Marinette

Whole program output when connecting to property directly:

[07:58:59][][LogObserver][INFO]: Received value: Chloe
[07:58:59][][LogObserver][INFO]: Received value: Marinette

APropertyPrecomputed

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.

struct User {
APropertyPrecomputed<AString> fullName = [&] { return "{} {}"_format(name, surname); };
};
auto u = aui::ptr::manage(new User {
.name = "Emma",
.surname = "Watson",
});
auto observer = _new<LogObserver>();
AObject::connect(u->fullName, slot(observer)::log);
Definition APropertyPrecomputed.h:64

The example above prints "Emma Watson". If we try to update one of dependencies of APropertyPrecomputed (i.e., name or surname), APropertyPrecomputed responds immediately:

u->surname = "Stone";

The example above prints "Emma Stone".

Observing changes

Similar to AProperty.

Valid Expressions

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.

struct User {
APropertyPrecomputed<AString> fullName = [&]() -> AString {
if (name->empty()) {
return "-";
}
if (surname->empty()) {
return "-";
}
return "{} {}"_format(name, surname);
};
};

In this expression, we have a fast path return if name is empty.

User u = {
.name = "Emma",
.surname = "Watson",
};
// trivial: we've accessed all referenced properties
EXPECT_EQ(u.fullName, "Emma Watson");

As soon as we set name to "", we don't access surname. If we try to trigger the fast path return:

u.name = "";

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.

UI data binding with let

Note
This is a comprehensive, straightforward way of setting up a connection. We are demonstrating it here so you can get deeper understanding on how connections are made and what does declarative way do under the hood. This way may be used in favour of declarative way if the latter not work for you. For declarative way, go to UI declarative data binding.

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.

Label via let

Use let expression to connect the model's username property to the label's text() property.

using namespace declarative;
struct User {
};
auto user = aui::ptr::manage(new User { .name = "Roza" });
class MyWindow: public AWindow {
public:
MyWindow(const _<User>& user) {
setContents(Centered {
_new<ALabel>() let {
// Data goes from left to right:
// current value (pre fire) or changed event
// goes to assignment operation of it->text()
AObject::connect(user->name, it->text());
// -> -> -> -> ->
// in other words, this connection is essentially the
// same as
// AObject::connect(user->name, slot(it)::setText);
//
// if you want user->name to be aware or it->text()
// changes (i.e., if it were an editable view
// like ATextField) use AObject::biConnect instead
// (see "Bidirectional connection" sample).
},
});
}
};
_new<MyWindow>(user)->show();
void setContents(const _< AViewContainer > &container)
Moves (like via std::move) all children and layout of the specified container to this container.
Definition AViewContainerBase.cpp:515
Represents a window in the underlying windowing system.
Definition AWindow.h:45
void show()
Definition AWindowsImpl.cpp:126
#define let
Performs multiple operations on a single object without repeating its name (in place) This function c...
Definition kAUI.h:261

This gives the following result:

Note that label already displays the value stored in User.

Let's change the name:

user->name = "Vasil";
text

By simply performing assignment on user we changed ALabel display text. Magic, huh?

Label via let projection

It's fairly easy to define a projection because one-sided connection requires exactly one projection, obviously.

_new<ALabel>() let {
// Data goes from left to right:
// current value (pre fire) or changed event
// goes through projection (&AString::uppercase) first
// then it goes to assignment operation of it->text()
// property.
AObject::connect(user->name.readProjected(&AString::uppercase), it->text());
// -> -> -> -> -> -> -> -> -> -> -> ->
// in other words, this connection is essentially the same as
// AObject::connect(user->name.projected(&AString::uppercase), slot(it)::setText);
// if view's property gets changed (i.e., by user or by occasional
// ALabel::setText), these changes DO NOT reflect on model
// as we requested connect() here instead of biConnect().
},

This gives the following result:

Note that the label already displays the projected value stored in User.

Let's change the name:

user->name = "Vasil";
text

This way, we've set up data binding with projection.

Bidirectional connection

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.

_new<ATextField>() let {
// Data goes from left to right in the first place
// (i.e., user->name current value overrides it->text())
// if view's property gets changed (i.e., by user),
// these changes reflect on model
// as we requested biConnect here
// -> value + changes ->
AObject::biConnect(user->name, it->text());
// <- changes only <-
},

This gives the following result:

Let's change the name programmatically:

user->name = "Vasil";

ATextField will respond:

If the user changes the value from UI, these changes will reflect on user->model as well:

EXPECT_EQ(user->name, "Changed from UI");

This way we've set up bidirectional projection via AObject::biConnect which makes user->name aware of UI changes.

Bidirectional projection

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:

enum class Gender {
MALE,
FEMALE,
OTHER,
};
Gender::MALE,
Gender::FEMALE,
Gender::OTHER)
#define AUI_ENUM_VALUES(enum_t,...)
Defines all enum values for AEnumerate.
Definition AEnumerate.h:208
struct User {
// we've omitted other fields for sake of simplicity
};

Now, let's get a mapping for our Gender enum:

static constexpr auto GENDERS = aui::enumerate::ALL_VALUES<Gender>;
constexpr auto ALL_VALUES
constexpr std::array of all possible enum values is the order they've been passed to AUI_ENUM_VALUES.
Definition AEnumerate.h:176

The compile-time constant above is equivalent to:

/* pseudocode */
GENDERS = std::array { Gender::MALE, Gender::FEMALE, GENDER::OTHER };

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:

/* pseudocode */
GENDERS[0]; // -> MALE
GENDERS[1]; // -> FEMALE
GENDERS[2]; // -> OTHER

To perform opposite operation (i.e., Gender to int), we can use aui::indexOf:

/* pseudocode */
aui::indexOf(GENDERS, Gender::MALE); // -> 0
aui::indexOf(GENDERS, Gender::FEMALE); // -> 1
aui::indexOf(GENDERS, Gender::OTHER); // -> 2
AOptional< size_t > indexOf(const Container &c, const typename Container::const_reference value) noexcept
Finds the index of the first occurrence of the value.
Definition containers.h:257

To bring these conversions together, let's use overloaded lambda:

static constexpr auto GENDER_INDEX_PROJECTION = aui::lambda_overloaded {
[](Gender g) -> int { return aui::indexOf(GENDERS, g).valueOr(0); },
[](int i) -> Gender { return GENDERS[i]; },
};
Definition callables.h:36
Note
It's convenient to use lambda trailing return type syntax (i.e., ... -> 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:

GENDER_INDEX_PROJECTION(0); // -> MALE
GENDER_INDEX_PROJECTION(Gender::MALE); // -> 0

It is all what we need to set up bidirectional transformations. Inside AUI_ENTRY:

auto user = aui::ptr::manage(new User { .gender = Gender::MALE });
class MyWindow: public AWindow {
public:
MyWindow(const _<User>& user) {
// generate a string list model for genders from GENDERS array defined earlier
GENDERS
| ranges::views::transform(AEnumerate<Gender>::toName)
| ranges::to_vector);
// equivalent:
// gendersStr = { "MALE", "FEMALE", "OTHER" }
// you can customize the displayed strings by playing with
// ranges::views::transform argument.
setContents(Centered {
_new<ADropdownList>(gendersStr) let {
// AObject::connect(user->gender, it->selectionId());
//
// The code above would break, because Gender and int
// (selectionId() type) are incompatible.
//
// Instead, define bidirectional projection:
user->gender.biProjected(GENDER_INDEX_PROJECTION),
it->selectionId());
},
});
}
};
_new<MyWindow>(user)->show();
static const AString & toName(enum_t value)
Map runtime enum value to name. Throws an exception if no such value.
Definition AEnumerate.h:134
static _< AListModel< StoredType > > fromVector(AVector< V > t)
Definition AListModel.h:244
An std::weak_ptr with AUI extensions.
Definition SharedPtrTypes.h:178
dropdownlist
dropdownlist
dropdownlist

UI declarative data binding

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".

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.

Label via declarative

Use & and > expression to connect the model's username property to the label's text property.

using namespace declarative;
struct User {
};
auto user = aui::ptr::manage(new User { .name = "Roza" });
class MyWindow: public AWindow {
public:
MyWindow(const _<User>& user) {
setContents(Centered {
_new<ALabel>() & user->name > &ALabel::text
});
}
};
auto window = _new<MyWindow>(user);
window->show();
auto text() const
Label's text property.
Definition AAbstractLabel.h:37

Note that the label already displays the value stored in User.

Let's change the name:

user->name = "Vasil";

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.

ADataBindingDefault for omitting view property

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:

/* PREDEFINED! You don't need to define it! This listing is an example */
template<>
public:
static auto property(const _<ALabel>& view) { return view->text(); }
};
Represents a simple single-line text display view.
Definition ALabel.h:23
Defines how View handles properties of FieldType type.
Definition ADataBinding.h:37
static auto property(const _< View > &view)
Returns property definition for FieldType.
Definition ADataBinding.h:49

We can use this predefined specialization to omit the destination property:

_new<ALabel>() & user->name

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:

user->name = "Vasil";

In this example, we've omitted the destination property of the connection while maintaining the same behaviour as in Label via declarative.

ADataBindingDefault strong type propagation

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:

/* PREDEFINED! You don't need to define it! This listing is an example */
template <aui::arithmetic UnderlyingType, auto min, auto max>
struct ADataBindingDefault<ANumberPicker, aui::ranged_number<UnderlyingType, min, max>> {
public:
static auto property(const _<ANumberPicker>& view) {
return view->value();
}
static void setup(const _<ANumberPicker>& view) {
view->setMin(aui::ranged_number<UnderlyingType, min, max>::MIN);
view->setMax(aui::ranged_number<UnderlyingType, min, max>::MAX);
}
// ...
};
A text field for numbers with increase/decrease buttons.
Definition ANumberPicker.h:24
auto value() const
Value property.
Definition ANumberPicker.h:52
static void setup(const _< View > &view)
Called then view linked with field.
Definition ADataBinding.h:43

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.

_new<ANumberPicker>() && user->age,
Note
We're using 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:

EXPECT_EQ(numberPicker->getMin(), 1);
EXPECT_EQ(numberPicker->getMax(), 99);

This example demonstrates how to use declarative binding to propagate strong types. aui::ranged_number propagates its constraints on ANumberPicker thanks to ADataBindingDefault specialization.

Label via declarative projection

We can use projections in the same way as with let.

using namespace declarative;
struct User {
};
auto user = aui::ptr::manage(new User { .name = "Roza" });
class MyWindow: public AWindow {
public:
MyWindow(const _<User>& user) {
_<ALabel> label;
setContents(Centered {
_new<ALabel>() & user->name.readProjected(&AString::uppercase)
});
}
};
auto window = _new<MyWindow>(user);
window->show();
text

Note that the label already displays the projected value stored in User.

Projection applies to value changes as well. Let's change the name:

user->name = "Vasil";
EXPECT_EQ(user->name, "Vasil");
EXPECT_EQ(label->text(), "VASIL"); // projected
text

Declarative bidirectional connection

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.

_new<ATextField>() && user->name

This gives the following result:

Let's change the name programmatically:

user->name = "Vasil";

ATextField will respond:

If the user changes the value from UI, these changes will reflect on user->model as well:

EXPECT_EQ(user->name, "Changed from UI");

This way we've set up bidirectional projection via && which makes user->name aware of UI changes.

Declarative bidirectional projection

We can use projections in the same way as with let.

Let's repeat the Bidirectional projection sample in declarative way:

_new<ADropdownList>(gendersStr) && user->gender.biProjected(GENDER_INDEX_PROJECTION) > &ADropdownList::selectionId
auto selectionId() const
Selected id property.
Definition ADropdownList.h:50

Note
We used the && operator here instead of & because we want the connection work in both directions: user.gender -> ADropdownList and ADropdownList -> user.gender.
dropdownlist
dropdownlist

Functions

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...
 

Function Documentation

◆ biConnect()

template<APropertyWritable PropertySource, APropertyWritable PropertyDestination>
requires requires { { *propertySource } -> aui::convertible_to<std::decay_t<decltype(*propertyDestination)>>; { *propertyDestination } -> aui::convertible_to<std::decay_t<decltype(*propertySource)>>; }
static void AObject::biConnect ( PropertySource && propertySource,
PropertyDestination && propertyDestination )
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.

Parameters
propertySourcesource property, whose value is preserved on connection creation.
propertyDestinationdestination property, whose value is overwritten on connection creation.

◆ connect() [1/3]

template<AAnyProperty Property, typename Object, ACompatibleSlotFor< Property > Function>
requires (!aui::derived_from<Object, AObject>)
static void AObject::connect ( const Property & property,
_< Object > object,
Function && function )
inlinestatic

Connects signal or property to the slot of the specified non-AObject type.

See signal-slot system for more info.

struct User { AProperty<AString> name }; // user.name here is non-AObject type
connect(textField->text(), user->name.assignment());
Parameters
propertysource property.
objectinstance of AObject.
functionslot. Can be lambda.

◆ connect() [2/3]

template<AAnyProperty Property, aui::derived_from< AObjectBase > Object, typename Function>
static void AObject::connect ( const Property & property,
Object * object,
Function && function )
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.

connect(textField->text(), slot(otherObjectRawPtr)::handleText);
Parameters
propertyproperty
objectinstance of AObject
functionslot. Can be lambda

◆ connect() [3/3]

template<APropertyReadable PropertySource, APropertyWritable PropertyDestination>
requires requires { { *propertySource } -> aui::convertible_to<std::decay_t<decltype(*propertyDestination)>>; }
static void AObject::connect ( PropertySource && propertySource,
PropertyDestination && propertyDestination )
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.

Parameters
propertySourcesource property, whose value is preserved on connection creation.
propertyDestinationdestination property, whose value is overwritten on connection creation.
Collaboration diagram for Property System: