A nifty little CachedValue helper

We’ve been using CachedValue more and more recently and finding them particularly useful.

One thing we feel they’re missing is a mechanism to listen to value changes. Without this, we usually have to listen to the ValueTree they refer to in order to react to changes in the value which isn’t always ideal since there’s more boilerplate involved, and we don’t always want to hold onto a ValueTree object.

So I wrote this nifty little helper to given CachedValues callbacks:

template<typename ValueType>
class CachedValueWithCallback : private juce::Value::Listener
{
public:
    CachedValueWithCallback()
    {
        m_value.addListener (this);
    }

    void setPropertyId (const juce::Identifier& propertyId)
    {
        updateCachedValue (m_cachedValue.getValueTree(), propertyId);
    }

    void setValueTree (juce::ValueTree& tree)
    {
        updateCachedValue (tree, m_cachedValue.getPropertyID());
    }

    CachedValueWithCallback& operator= (const ValueType& newValue)
    {
        m_cachedValue = newValue;
        return *this;
    }

    operator ValueType() const noexcept
    {
        return m_cachedValue;
    }

    std::function<void (void)> onValueChanged = nullptr;

private:
    void valueChanged (juce::Value& value) override
    {
        juce::ignoreUnused (value);
        jassert (value.refersToSameSourceAs (m_value));

        if (onValueChanged != nullptr)
        {
            onValueChanged();
        }
    }

    void updateCachedValue (juce::ValueTree& tree,
                            const juce::Identifier& propertyId)
    {
        m_cachedValue.referTo (tree, propertyId, nullptr);
        m_value.referTo (m_cachedValue.getPropertyAsValue());
    }

    juce::CachedValue<ValueType> m_cachedValue;
    juce::Value m_value;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CachedValueWithCallback)
};
CachedValueWithCallback<int> m_someValue;

m_someValue.setPropertyId ("someValue");
m_someValue.setValueTree (m_valueTree);

m_someValue.onValueChanged = []() {
    DBG ("Some value changed!");
};

Though it might be useful for others. Feedback is very much welcome!
Would be interesting see if anyone else would find this useful in juce::CachedValue (or if there’s a better listener mechanism for CachedValues that we’ve missed).

2 Likes

Looks great, thanks for sharing!

Personally, I’d either like this object to be a one-liner declaration in a header that I never have to reference again (ie, make everything settable via constructor init list), or I’d like it to actually inherit from CachedValue so that it’s basically a drop-in replacement for existing CachedValue code.

My preference would be for the first way… I think it’s super convenient to write

CachedValueWithCallback<int> someValue {"someValue", valueTree, [this](){ someFunction(); }};

in a header somewhere :slightly_smiling_face:

I used to be in favour of the stick-everything-in-the-constructor pattern but more recently we’ve been trying to avoid it due to the “Convenience Trap”. But by all means customise it to your needs!

I thought about doing the inheritance approach but wasn’t sure what advantages there would be - not in our codebase anyway. If you’re already using CachedValue in a whole bunch of places then that certainly sounds useful.

We tend to keep the CachedValues pretty isolated, usually just private members in our model classes.

You might want to return CachedValueWithCallback& from setPropertyId and setValueTree so at least you can chain them together.

2 Likes

That’s a good point… I suppose the answer is to minimize the amount of things each constructor needs to be passed. Perhaps you could do something like this:

template<typename Type>
struct CachedValueCallback :    private juce::Timer
{
    CachedValueCallback (CachedValue<Type>& valueToUse,
                         std::function<void()> callback)
        : value (valueToUse), func (std::move (callback))
    { 
        startTimerHz (10);
    }

    virtual ~CachedValueCallback() { stopTimer(); }

private:
    void timerCallback() final
    {
        const auto newValue = value.getPropertyAsValue();
        if (newValue == lastValue) return;
        lastValue = newValue;
        func();
    }

    CachedValue<Type>& value;
    std::function<void()> func;
    Value lastValue;
};

then you’d be able to do simply:

CachedValue<int> someValue;
CachedValueCallback<int> callback {someValue, [this](){ someFunc(); }};

I feel like this is pretty explicit syntax to read… but either way, it’s a useful concept, so thanks for sharing :grinning:

1 Like

One other thing I realized is that the CachedValueCallback knows the type of the underlying CachedValue… meaning you can make the callback function take the latest value as its own type literal:

template<typename Type>
struct CachedValueCallback :    private juce::Timer
{
    CachedValueCallback (CachedValue<Type>& valueToUse,
                         std::function<void(Type)> callback)
        : value (valueToUse), func (std::move (callback))
    { 
        startTimerHz (10);
    }

    virtual ~CachedValueCallback() { stopTimer(); }

private:
    void timerCallback() final
    {
        const auto newValue = value.getPropertyAsValue();
        if (newValue == lastValue) return;
        lastValue = newValue;
        func (newValue);
    }

    CachedValue<Type>& value;
    std::function<void(Type)> func;
    Value lastValue;
};

and now it can be

void someFunction (int newValue);

CachedValue<int> value;
CachedValueCallback<int> cvc {value, [this](int val){ someFunction (val); }};

Yeah that’s not a bad idea. Where we’re using them, the setters are called in different places (property IDs done in the constructor, value tree passed in later) but that would certainly be a nice thing to add.

Yeah, if you’ve got existing CachedValues in places then a decorator pattern might be a nice idea.

Not a fan of that Timer usage however… What’s the advantage of that over listening to the Value? Seems like you’ll just be waiting longer to be notified of changes which could cause problems.

Good call on passing the value to the callback function - hadn’t considered that!