CachedValue not updated - how to use correctly?

Hi all,

I am trying to wrap my head around how to use CachedValues. I am trying to introduce CachedValues into my codebase because they give me type safety and the ability to define a default value (which raw ValueTree properties don’t give me). However, I can’t get them to work.

Let’s say I have a Component, and I want its name to be controllable from a ValueTree property. I have a tree with a “name” property. Whenever that changes, I want the Component to automatically update its name to match.

Here’s my first attempt:

    struct MyComponent : public Component, private ValueTree::Listener
    {
        MyComponent (const ValueTree& _tree)
            : tree (_tree), name (tree, Ids::name, nullptr)
        {
            tree.addListener (this);
            updateName();
        }

    private:
        void updateName()
        {
            setName (name);
        }

        void valueTreePropertyChanged (ValueTree& _tree, const Identifier& _property) override
        {
            if (_tree == tree && _property == Ids::name)
                updateName();
        }

        ValueTree tree;
        CachedValue<String> name;
    };

    ValueTree tree {"Test", {{Ids::name, "foo"}}};
    MyComponent comp (tree);
    CHECK (comp.getName() == "foo");  // succeeds :)

    tree.setProperty (Ids::name, "bar", nullptr);
    CHECK (comp.getName() == "bar");  // fails :(

However this does not work. The check on the last line of code fails. If I step into the debugger, I see that even though I set the name in the tree to "bar", afterwards the name cached value inside updateName() still has the old value "foo".

What am I doing wrong, and how do I do this correctly? I am tearing my hear out…

Is the valueTreePropertyChanged callback happening?

I suspect this is because there is no guarantee that the CachedValue is updated before this valueTreePropertyChanged callback is called. So I think you’ll need to call forceUpdateOfCachedValue() on name.

1 Like

Yes, what @alibarker1 said.

And in this simple case, it doesn’t really make sense to use both the ValueTree::Listener AND the CachedValue. If you have the ValueTree::Listener, then set the member variable there.

The reason why I want to use wrappers like CachedValue is because I want type safety, a way to define a default value, and then later also a way to force values to always fall inside a range (numbers), always be non-empty (strings), and all kinds of similar constraints. Typing out those checks each time in the valueTreePropertyChanged() callback is too much boilerplate. But what is the idiomatic way to do this?

Perhaps I need to implement something similar to a CachedValue, but such that I can assign a valueChanged lambda to it? And in that lambda, capture this and do the work of updating the name etc.? And then the outer class no longer has to be a ValueTree::Listener.

I really struggle to understand how to do this correctly. Basically, the use case is:

  • I need to react to ValueTree property changes
  • At the same time I need to ensure that those properties are always of a certain type, inside a certain range, etc.
  • I want to avoid boilerplate because this pattern repeats hundreds of times in my code.
1 Like

Yes.

Yes, indeed, that is what the debugger tells me as well.

So here is an idea. I could write a MagicValue class such that this works (pseudocode):

    struct MyComponent : public Component
    {
        MyComponent (const ValueTree& tree)
            : name (tree, Ids::name, nullptr)
        {
           name.valueChanged = [this]{ setName (name); };
        }

        MagicValue<String> name;
    };

MagicValue's implementation would be similar to CachedValue, except it would call its valueChanged every time it internally receives a valueTreePropertyChanged callback.

Does that sound like the right approach for this problem, or does anyone have a better idea?

I have a similar approach in my usage of ValueTrees. But instead of approaching it at the individual property level, I create a wrapper class for each data model, with setters, getters, and lambda callbacks for each of the properties. It will either wrap an existing data model, or initialize all of the properties to default values if passed an empty ValueTree. Here is a small example of using one:

    presetEntry.wrap (presetEntryVT);
    presetEntry.onNameChange = [this] (String name) { updateNameLabel (name); };
    presetEntry.onTagListChange = [this] (String tagList) { updateTagListLabel (tagList); };

    updateNameLabel (presetEntry.getName ());
    updateTagListLabel (presetEntry.getTagList ());
1 Like

Interesting. This sounds similar – I also need to maintain a particular data model.

I also looked into @dave96’s constrainer approach to maintain values within a certain range, but then I discovered that it actually only constrains the “cached value” that the class in question sees, it doesn’t actually enforce the constraint on the actual ValueTree property that the rest of the world sees. So it looks like I need to come up with my own solution for that as well.

One problem I see is that, as @alibarker1 noted above, the order in which the different ValueTree::Listeners are called is not guaranteed, so I can either have individual properties listening to the tree, or the outer class listening to the tree, but never mix and match both (which was the mistake I did in the original post).

This is indeed not easy to figure out :frowning:

I always use my wrappers, so there is never a conflict with callback order. It also means I can implement constraints that are always honored, because they always go through the setters.

Sounds great, but…

Could you please elaborate what “setters” you mean?

I don’t update any of the ValueTree properties directly from any code, I use the setters in my wrapper. Here is a stripped down declaration of the PresetEntry class I used in my previous example. So, in client code I would call presetEntry.setName("adad"); to set the property.

class PresetEntry : public ValueTreeWrapper
{
public:
    PresetEntry () : ValueTreeWrapper (PresetEntryId) {}

    static ValueTree create (String name, String tagList = {});

    void setName (String name, bool includeSelfCallback = true);
    void setDescription (String description, bool includeSelfCallback = true);
    void setTagList (String tagList, bool includeSelfCallback = true);

    String getName ();
    String getDescription ();
    String getTagList ();

    std::function<void (String name)> onNameChange;
    std::function<void (String description)> onDescriptionChange;
    std::function<void (String tagList)> onTagListChange;

    static inline const Identifier PresetEntryId { "PresetEntry" };
    static inline const Identifier NamePropertyId        { "name" };
    static inline const Identifier DescriptionPropertyId { "description" };
    static inline const Identifier TagListPropertyId     { "tagList" };

private:
    void initValueTree () override;
    void processValueTree () override;

    void valueTreePropertyChanged (ValueTree& treeWhosePropertyHasChanged, const Identifier& property) override;

}
1 Like

Thanks! This is very useful. Really interesting to see how other people approach this problem.

I’d actually be happy to share the ValueTreeWrapper class with you.

2 Likes

So this is fairly complex and your final use case will probably depend on what trade-offs you’re willing to make.

A few things to verify before we dig in to this:

MyComponent example and CachedValues:
As you’ve found out, the order of ValueTree callbacks is fairly indeterminate (technically I think they’re called in the order they’re added in but I don’t think this is specified or to be relied upon). This means that in your first example, MyComponent::valueTreePropertyChanged is called before the CachedValue's internal valueTreePropertyChanged callback so it’s cachedValue member still has the old value.

As has already been pointed out, you can work around that by calling name .forceUpdateOfCachedValue() in updateName().
This is generally fine for UI stuff like this but probably not suitable for performance code as it kind of defeats the caching aspect of CachedValues.

For UI code, one solution to this problem is to simply update your components asynchronously. E.g. make MyComponent inherit from juce::AsyncUpdater and then in MyComponent::valueTreePropertyChanged, you’d call triggerAsyncUpdate(). Then in handleAsyncUpdate() you’d call updateName(). You now know at this point all the valueTreePropertyChanged will have been made so you can safely use the cached version of name (by effectively calling get() or using the operator overload as in the example).

Another alternative here would be to use a juce::Value to handle the callback. These can handle the async update automatically for you so your example would keep a Value member and listen to changes in that, calling in the constructor (where nameValue is a member “Value nameValue;”:

nameValue = name.getPropertyAsValue()
nameValue.addListener (this);

Constraining CachedValues
The RangedWrapper stuff I wrote a few years ago was primarily to work with the existing CachedValue APIs and to get people thinking about ways to (ab)use the type system.
Since then I’ve started using my own more flexible ConstrainedCachedValue class that is the same as CachedValue but has a void setConstrainer (std::function<Type (Type)> constrainerToUse); method that call this when returning the returning the value.

Additionally, it also constrains the current value when setting the constrainer (or setting the value) which means that it will actually update the underlying ValueTree when one is used.
This seems to be closer to your requirements. (This is here: https://github.com/Tracktion/tracktion_engine/blob/master/modules/tracktion_engine/utilities/tracktion_ConstrainedCachedValue.h)


CachedValue Callbacks
There’s been some talk here of using std::function objects as callbacks. Generally I’m all for this and use it a lot, especially for GUI components (like Slider, Button etc). However, I think the fundamental difference there is that you usually have only one thing that needs to listen to changes in the Slider/Button and that’s usually to update your model.

With CachedValues, these are your model so are usually “listened” to in at least two places (usually one control to set the value and another to respond to the change in UI, maybe another to respond trigger audio changes etc. An example of this would be a button to control the visibility of a panel). If you’re using a CachedValue from multiple places because it caches, having a single callback probably wouldn’t cut it. Maybe a CachedValue with ListenerList would be more appropriate here…

The other thing you have to remember when using shared CachedValues (e.g. non-small scoped members such as in your original example) is that you’d have to clear std::functions or remove listeners to avoid dangling listeners or captured this pointers.

I do agree in your MyExample however that a single std::function callback is probably the best approach.


Bringing it all Together
To be honest, I use ValueTree and all the ancillary classes a variety of ways depending on the use case. I still haven’t found the “perfect” way of doing all tasks.

What I would say though is that I’m definitely moving towards a more binding style rather than creating wrappers for everything. This often involved making a shim class that inherits from juce::ReferenceCountedPointer so it can be stashed in a Component::getProperties() NamedValueSet. Given your name example I think the ideal would be to be able to write something like:

CachedValueCallback::bind (myComponent, name,
                           [this] (auto& comp, auto n) { comp.setName (n); });

This would probably use something like your MagicValue internally but could be implemented in a variety of ways.

The main things you need to decide are how visible you want these to be i.e. does the rest of codebase have raw access to the underling ValueTree or does all access go through CachedValues? The thing that’s always bugged me about dynamic types like this is that there’s nothing really stopping another part of the code from setting a nonsense value. This can happen in any dynamically typed language though and there are plenty of those around…

7 Likes

Follow-up: I experimented with different approaches, including the ideas by @cpr and @dave96 . The biggest question is whether there should be a class wrapping each individual value, or a class wrapping the whole tree.

I ended up not doing the whole tree approach, because I found that there is too much boilerplate that I’d have to write over and over again for every use of it (there are lots of these in my project).

So I went with the value wrapping approach, and ended up with a ConstrainedValue class that is very similar to JUCE’s CachedValue, except that you can:

  • add a std::function to get a synchronous value changed callback
  • add a constrainer as a template parameter (similar to how std::map has a comparator as a template parameter). This works really well for then implementing e.g. a constrainer that keeps a value within a certain range
  • have that constrainer actually modify the value in the ValueTree.

This works beautifully in my project, because the values tend to be numbers and strings, and they tend to be private members of some other class that doesn’t necessarily has to be aware of the ValueTree, and the value changed callbacks tend to not do too much work.

I found out that you can even wrap the same ValueTree property with multiple of these ConstrainedValues in different places, and it just works. If you use different ranges for the constrainer, then the value will be constrained to the overlap of those ranges. Only gotcha is if you wrap the same ValueTree property with different constrained values with mutually exclusive constraints, you get undefined behaviour (unsurprisingly).

I’m happy to share my implementation of ConstrainedValue here for you folks to review:

ConstrainedValue.h (6.1 KB)

9 Likes

Hi,
I just stumbled across this. Thank you very much for your code, very useful. But I think there is a mistake in the referTo-method. Shouldn’t you add the ConstrainedValue as a listener of the tree you want it to refer to? So at the end of referTo: tree.addListener (this); Or am I misunderstanding something. Right now it only works if constructed with the constructor that takes the ValueTree reference.