Adding "metadata" to QML properties
-
I'm using QML to expose an object model to the end-user. In my object model (just like in Qt), there are objects (and children) and properties. However, properties may have other metadata associated with them. For example, a string property may have an associated regular expression for validation.
Right now, my system looks like this:
@MyObject {
property var input
property string foo: "foo sez hello"MetaData { name: "input" isInput: true } MetaData { name: "foo" regex: "^foo.*" } ChildObject { # ... etc }
}@
This project is my first exposure to QML, so I'm not sure if this is idiomatically correct. I find my current approach a bit hacky, as it's easy for the MetaData and properties to get out of sync. For example, if I add a new property, I can easily forget to add a new instance of "MetaData". There's no easy way to iterate over all properties in C++, so I depend on having a metadata entry to find (for example) all isInput properties.
I can think of a few ways of expressing the same information, but I'm not sure which is the "correct" way to do so.
One way is to follow how states work in QQmlItem and make metadata explicitly a list:
@MyObject {
property var input
property string foo: "foo sez hello"metadata: [ MetaData { name: "input" isInput: true }, MetaData { name: "foo" regex: "^foo.*" } ] ChildObject { # ... etc }
}@
This feels a bit better, since the metadata is no longer floating around with other child instances, but metadata and properties are still separate, so the sync issue still applies.
Another way would be to abuse QQmlValueSource and the "on" syntax:
@MyObject {
property var input
MetaData on input {
isInput: true
}property string foo: "foo sez hello" MetaData on foo { regex: "^foo.*" } ChildObject { # ... etc }
}@
Just typing this out felt like kicking a puppy, so let's move on.
Yet another way would be to use an object instance as the property, with a ".value" property that holds the actual value:
@MyObject {
property var input: Input {
value: null
}property string foo: String { value: "foo sez hello" regex: "^foo.*" } ChildObject { # ... etc }
}@
This would litter qml with lots of repeated ".value"s, and "property string foo: String" seems hacky. And also, I think this requires that any property change handlers be on "value" (inside the scope of the object instance), since onFoo won't be called when foo.value changes). On the other hand, the property metadata is now clearly part of the property itself.
Opinions?
-
Hi,
It's an interesting problem. Your solutions are interesting; I'll deal with them in reverse order:
If each property value is represented by an object which contains the actual value + the metadata values, then (depending on how many properties you require metadata for) you will be instantiating large numbers of QObjects. This is expensive. You can connect up the change signals appropriately by doing something like: bar.onValueChanged.connect(root.onBarChanged); -- but this is a manual step which must be done in imperative code, so isn't too clean. Finally, the real problem with this approach (in my opinion) is that you lose the type information for every property (as, basically, they all become QtObject typed properties) -- even though type safety is enforced for the internal "value" property, it isn't for the external (eg, you could accidentally assign a StringWithMetadata property to an IntWithMetadata property, and no warnings would be given by default).
Property value source objects - this is pretty hideous, and I'm surprised that it works as you expect. If it does work, check how many times the expressions are run using QML analyzer, I think you might be in for a shock (although I don't remember whether the implementation is clever enough to avoid emitting signals if no value change occurs).
The metadata list property is the best, in my opinion. It has strict coupling with properties (via the "name" metadata property), you don't require one for each property in existence, you keep type safety and automatic change signal hookup for properties, and so forth. Keeping them in sync is an issue, I agree, but probably one that can be checked via an automatic tool (if all properties+metadata must be declared at compile time).
Finally, the child objects -- these objects (assuming they're non-visual) actually get added to a list property internally (called resources, iirc). I don't like this solution as much as the explicit metadata list property solution, syntactically, but in practice it's almost identical.
I don't know which version of QtQuick you're using, but another possibility (if you're using QtQuick2, and I don't know whether this solution is better or worse tbh) is to use a singleton type that stores a hash of qobject+propertyIndex pair, to metadata structure... That would avoid the need to instantiate so many QObjects in order to store your metadata, but I don't know how simple it would prove to be to hook it up properly / automatically.
Cheers,
Chris. -
Thanks Chris, that mirrors my opinions pretty closely (right down to "hideous"). This sounds interesting:
bq. I don’t know which version of QtQuick you’re using, but another possibility (if you’re using QtQuick2, and I don’t know whether this solution is better or worse tbh) is to use a singleton type that stores a hash of qobject+propertyIndex pair, to metadata structure… That would avoid the need to instantiate so many QObjects in order to store your metadata, but I don’t know how simple it would prove to be to hook it up properly / automatically.
I am using QtQuick 2.0, so a singleton is possible. I'm not sure how this would look in QML, though. How would you register metadata for a property?
cheers,
-Mark -
It might end up being worse (because of the QVariantMap to JS Object dictionary conversion at both points (param + retn) is really slow) but something like:
@
MySingleton::storeMetaData(QObject *obj, int propertyIdx, const QVariantMap &md)
{
int hashValue = hashFunction(obj, propertyIdx);
m_metaData.insert(hashValue, md);
}QVariantMap MySingleton::metaData(QObject *obj, int propertyIdx) const
{
int hashValue = hashFunction(obj, propertyIdx);
return m_metaData.value(hashValue);
}
@Etc. You might have to have some C++ functions to extract the property index using QMetaObject etc, too, and pass in strings (property names) instead of int property index directly, but that's all just details.
Definitely benchmark it though, because it might end up being slower.
Also, you won't get any change signals automatically, so if you bind to any metadata, you'll need to do something manually.Cheers,
Chris.