Scoped-Listeners / Modernize listeners


#1

I’ve made this little class, and it really revolutionised my programming style.

Its a scoped listener, and very easily combines the listener (old-school) functionality with the ability to attach std::function lambdas to it (a more modern approach). It’s not a very sophisticated, its very plain and simple.

Example:

class TestComponent : public Component
{
public:
	TestComponent( ColourSelector* selector, MidiKeyboardComponent* keyboard )
	{
		// attach updateUI to the ChangeBroadcaster of ColourSelector and run it once, to update the UI to its current state
		scopedListener.addAndRunOnce(selector, [this,selector]() { updateUI(selector->getCurrentColour()); });

		// attach handleKeyboardChange if the MidiKeyboardComponent changes
		scopedListener.add(keyboard, [this]() { handleKeyboardChange(); });
	};

	~TestComponent()
	{

	}

	void updateUI(Colour c)
	{
		// do stuff
	};
		
	void handleKeyboardChange()
	{
		// do stuff
	};

	ScopedChangeListener scopedListener;
};

And here is the ScopedChangeListener

class ScopedChangeListener : public ChangeListener
{
public:
	ScopedChangeListener()
	{

	};

	virtual ~ScopedChangeListener()
	{
		for (auto& i : items)
		{
			i.broadCaster->removeChangeListener(this);
		};
	};

	void add(ChangeBroadcaster* broadcaster, std::function<void()> func)
	{
		Item i;
		i.broadCaster = broadcaster;
		i.func = func;

		{
			ScopedLock lock(cSection);
			items.push_back(i);
		};
		broadcaster->addChangeListener(this);
	};

	void addAndRunOnce(ChangeBroadcaster* broadcaster, std::function<void()> func)
	{
		add(broadcaster, func);

		if (MessageManager::getInstanceWithoutCreating() != nullptr && MessageManager::getInstanceWithoutCreating()->isThisTheMessageThread())	
		{
			func();
		}
		else
		{
			jassertfalse;
		};
	};
	
	void changeListenerCallback(ChangeBroadcaster* source) override
	{
		ScopedLock lock(cSection);
		for (auto& i : items)
		{
			if (source == i.broadCaster)
			{
				i.func();
			};
		}
	}

private:
	CriticalSection cSection;

	struct Item
	{
		std::function<void()> func;
		ChangeBroadcaster* broadCaster;
	};

	std::vector<Item> items;
};

Button::onClick
Common pattern with Listeners
Button::onClick
#2

Neat little class! I have used a very similar approach myself, but It would be nice to see a class like this included in the JUCE library.


#3

Yes, maybe something which could be used for any kind of listeners.


#4

This can be dangerous in case it is called before the GUI is initialised which can for example happen (if I remember correctly) in the AudioProcessor class (prepareToPlay, setStateInformation etc.) in AAX plugins. Instead of simply failing this can have side effects like timers not running correctly since the MessageManager isn’t initialised correctly.

It’s saver to use this:

if (MessageManager::getInstanceWithoutCreating() != nullptr && MessageManager::getInstanceWithoutCreating()->isThisTheMessageThread())

#5

Thanks for the tip, i fixed that.


#6

Very nice. Thanks for sharing.


#7

Cute!

Just one very subtle and tricky problem is that you run the risk of edge-case order-of-destruction problems…

i.e. these listeners will be unregistered during the ScopedChangeListener’s destructor, which will be called after its parent’s destructor has been run, and somewhere in the sequence of destruction of the other sibling member variables. So… if there was an indirect callback during one of the other member destructors, then it could trigger a call to the half-destructed object. That’d be unlucky, admittedly, and isn’t an argument against this pattern. A simple workaround would be to give the ScopedChangeListener a method you can call to clear all its listeners, and to manually call that in the parent’s destructor.


#8

Thanks, i currently used it only in classes which were destructed on the message-thread, where i think this can’t happen, but yes this is a potential problem.

I added a check, which will assert if the object will be destructed on a non message-thread, while the listeners are not removed.

Fell free to to jucify this class, i also made similar classes for other kind of listeners (like Value::Listener), which needs a little different implementation because it references to the Value Source and not to pointers.

I think goal would be, to have some-kind of master ScopedListener which could be used for any kind of Listener, but this would require that all Listeners are somehow will inherited from one master-Listener/Broadcaster class.

Otherwise it could be implemented using a template (maybe)

class ScopedChangeListener : public ChangeListener
{
public:
	ScopedChangeListener()
	{

	};

	virtual ~ScopedChangeListener()
	{
		// If you delete the ScopedChangeListener on a non-Message-Thread
		// be sure to call clear(), before any destruction happens
		// so that no pending message will arrive, while or after partial destruction of any
		// involved objects.

		jassert(!(  MessageManager::getInstanceWithoutCreating() != nullptr
			    && !MessageManager::getInstanceWithoutCreating()->isThisTheMessageThread()
				&& items.size() > 0));
	
		clear();
	};

	void add(ChangeBroadcaster* broadcaster, std::function<void()> func)
	{
		Item i;
		i.broadCaster = broadcaster;
		i.func = func;

		{
			ScopedLock lock(cSection);
			items.push_back(i);
		};
		broadcaster->addChangeListener(this);
	};

	void addAndRunOnce(ChangeBroadcaster* broadcaster, std::function<void()> func)
	{
		add(broadcaster, func);

		if (MessageManager::getInstanceWithoutCreating() != nullptr && MessageManager::getInstanceWithoutCreating()->isThisTheMessageThread())
		{
			func();
		}
		else
		{
			jassertfalse;
		};
	};
	
	void changeListenerCallback(ChangeBroadcaster* source) override
	{
		ScopedLock lock(cSection);
		for (auto& i : items)
		{
			if (source == i.broadCaster)
			{
				i.func();
			};
		}
	}

	void clear()
	{
		ScopedLock lock(cSection);
		for (auto& i : items)
		{
			i.broadCaster->removeChangeListener(this);
		};
		items.clear();
	}

private:
	CriticalSection cSection;

	struct Item
	{
		std::function<void()> func;
		ChangeBroadcaster* broadCaster;
	};

	std::vector<Item> items;
};

#9

No, you’ve misunderstood… I wasn’t talking about thread-safety (it’s definitely not thread safe and just calling clear() in the destructor certainly won’t fix that!).

I was talking about plain old single-threaded callbacks that could be invoked indirectly as member variable destructors are being executed. Like I said, it’s an rare edge-case, but would need to be taken into account.


#10

I have to admit i not sure what the problem is. The problem i can think of is, that there is a reference to an (destructed) object in an lambda function, which may be called when the lambda is destructed.