How to send messages between components?

Hi,
I have a component with several custom child components. When the user clicks in one of these child components I need to inform the other child components. How can I do that?
I need somehow to send messages from one component to other components. Can I use the broadcast/listener feature for that? If so, how?

You could use juce::ChangeBroadcaster and juce::ChangeListener. This would require each of the listeners to know about the broadcaster which will probably involve passing pointers/references around which may not be ideal.

There’s also juce::MessageListener which allows you to listen to any and all juce::Messages that are posted to the message queue. This is useful when you don’t want the broadcasters and listeners to know about each other, but has the disadvantage of not necessarilly knowing where the messages came from, and the messages are sent asynchronously which may or may not be suitable for your usecase.

My preferred method would be something like the first approach, but instead to implement a Mediator Pattern where, instead of each of of the broadcasters and listeners needing to know about eachother you instead have the broadcasters send their messages to the mediator and the listeners then receive those messages from the mediator.

2 Likes

Besides all messaging solutions, the most easiest solution is just to keep a pointer of the parent and access and call the child-components directly over it. Actually you need messages only if you want that something has to happen asynchronously. Keep it simple as possible.

2 Likes

https://docs.juce.com/master/classActionBroadcaster.html

If you need synchronous message handling, this could also work and provide less direct dependency coupling than passing pointers around JUCE: ListenerList< ListenerClass, ArrayType > Class Template Reference

Also, the answer most people don’t want to hear, is it really your components that need to be connected, or do your components operate on the same data, and you have just coupled your data too tightly into a GUI component? This isn’t always true for the question you ask, but it is often true. ie. Usually our GUI components are just views into our data, and if you use something like ValueTrees to store your data, then the ValueTree can be shared, and each component can display, modify, and be notified if the data changes using the ValueTree. I also use this as a way to share GUI state between seperate GUI components, for example, in the audio domain, maybe you have different metering types.

11 Likes

If you did need to go this route, rather than keep a pointer of the parent, you could instead call getParentComponent (or findParentComponentOfClass if you want to move farther up the parent hierarchy).

But in the bigger picture, I think @cpr2323 has the right idea here.

another possibility:
Component::post/handleCommandMessage()
https://docs.juce.com/master/classComponent.html#a005628ba100dc23d179bff98bd419eb5

Thanks all for the good advice. Didn’t expect so many solutions. I will dive into this a bit more.
@cpr2323 : It is an interface thing. Selecting something in some view should also selecet the item in some other view. So I think I really want to connect the components here.

If that’s the case then you should really consider what @cpr2323 is suggesting and have a ValueTree that stores all of class’s variables including isSelected as properties, and which objects in each one of your views shares and responds to.

The question isn’t how do you accomplish this task, but rather how do you accomplish it in a way that you won’t regret later on.

3 Likes

100% sounds like a ValueTree would solve this problem. This is a common pattern in JUCE projects. You have a root ValueTree which contains all your project data. Then each component listens to a subset of that ValueTree. You then pass a reference to the ValueTree to each component that needs it, and they are each a ValueTree::Listener.

That sounds reasonable. I once watched a talk on ‘what makes good software’, and the speaker asserted the number one item was ‘does it do what it’s supposed to do’. :slight_smile: I try to remember that, so that I never get too dogmatic. And we certainly don’t want to over engineer either. And your schedule may also dictate how much time you can spend on solving this issue. So it may be that coupling the components is the right solution.

One issue that can arise with this type of coupling is a circular dependancy, where the parent component needs to know about the child component, and the child component needs to know about the parent component. Another form of decoupling that I do is using a lambda interface, either as a parameter passed in, or as a public member variable of the child (like the onClick feature of the Button classes), which the parent can use to receive the change operation.

That’s a great way to frame it. Also applies to life choices in general. :wink:

3 Likes

Any suggestion in this thread suggests solutions as different forms of message passing. Now they don’t mean they should be used for ui to ui communication (althought they could). It seems like you are putting too much logic in your ui layer, while it should be your model layer that would control how the ui updates and react. Dispatching the selection between components and ignore a model layer (or in case of current selection, user session state) is an easy win now, and a complete mess later on.

Maybe this is an edge case.
It is more like the user sets a cursor position in one view and I want the cursor to be updated in other views too.
So no data is involved. I do not think a cursor position should be part of the data object.

What is the ‘cursor position’ related to? does this position control something? Most of the given advice has been about encouraging you to see the underlying data model. When I read “updated in other views too” it really feels like the Components are viewing some sort of shared ‘data’. Would you be willing to describe what it does more clearly, so I can either confirm your design, or modify my advice?

Again, this may actually be an instance where simply coupling the Components makes sense, or even that it is a decent solution (based on engineering effort of doing it differently) to get you moving forward in development. But I think it is a good exercise to at least continue the discussion with a deeper understanding of the problem.

I have two time line views. When I pace the cursor at some time point in one view, I want the cursor in the other time view to be adapted too. No data is selected in this case.
I realize I did say that I select something. But that is not accurate. I select a time.

But thinking some more about it: when I would select a data item in one view, and I want to select it on the other view too, I still doubt if this is the responsibility of the module that handles the data. It i still an interface thing. Isn’t it?

A rule of thumb I use to determine if some value/aspect should be stored in the data model is: when I reload a saved state, do I want this aspect to go back at the way it was when I saved it?

Same thing if I am working with undoable stuff: if I undo my past actions, should I also undo the changes to this particular value/aspect?

If the answer to one of the above is “yes”, then it’s very likely that the value should go in the data model and that the various UI components that display it should all use refer to the data model as their source of truth, with no need of knowing each other

I encourage you to expand your thinking of ‘what is data’. The ‘current timeline cursor’ is data. It’s seperate from the ‘processor’ data, yes, but it is still data, and your desire to have it used in more than one place indicates that it is shared data. There isn’t only one data model, often times that are many data models. I often have a UI data model, which might contain things like window positions and sizes, etc. And as soon as I see that a piece of data is available to multiple components, it becomes part of the data model.

Also, the next question is, does the changing of the timeline cursor control something else? Does it control something like ‘start of playback’? If so, then it isn’t really the cursor position being shared, it is the value of what it is controlling that is being shared. The UI is just a view into the underlying data.

I am still not trying to convince you that you shouldn’t couple the components, but I want to ensure you understand these concepts clearly, so you can have confidence that you are choosing the right methods to achieve your desired result.

To get around where the messages come from, we created a wrapper around MessageBroadcaster and created custom messages with an ID that you can trace back to who is broadcasting it. You can also specify if you want the delivery to be sync/async. Since you can post value trees as part of the message it can come in handy also.

void WaveformView::notifyZoomUpdate()
{
    auto m = createValueTree (IDs::MESSAGE,
                              IDs::messageID, "ZoomLevelUpdated",
                              IDs::startDrawPos, startDrawPosSeconds,
                              IDs::endDrawPos, endDrawPosSeconds,
                              IDs::zoomCoeff, zoomCoeff,
                              IDs::sampleLength, sampleLengthSeconds);

    faw::MessageBroadcaster::postMessage (new Message (m),  faw::NotificationType::Sync);
}

Then any listeners do something like


void SampleEditorOverlay::handleMessage (const faw::Message& message)
{
    if (message.getID() == "ZoomLevelUpdated")
    {
        //we could also pack in the index...
        zoomedSampleStartPos = message.getProperty (IDs::startDrawPos);
        zoomedSampleEndPos = message.getProperty (IDs::endDrawPos);
        zoomCoeff = message.getProperty (IDs::zoomCoeff);
        sampleLengthSeconds = message.getProperty (IDs::sampleLength);
        repaint();
    }
}