Messages and update notifications

Working in close association with the Celedev Object Framework, the Celedev Message System provides a flexible framework for notifications between objects, or between Celedev dynamic code & resources update system and the program's objects.

The messaging system

The Celedev Messaging System broadcasts asynchronous messages from a sender to all receiver objects that have registered a message handler for a particular message.

A Message in this system is a very flexible entity identified by a name, and containing any number of additional parameters that can be of any type supported by Lua.

Posting a message

A message is posted by calling the function message.post:

message.post (messageName, parameters…)

Parameters:

  • messageName: a unique identifier for this message (a string).
  • parameters…: a list of values of any type that constitute the message; these parameters will be delivered in the same order to the receivers of the message.

Example:

-- a simple message without parameter
message.post ("A very special notification")

-- a more complex message
message.post ("UserDidTouchAnAlien", touchPoint, 24.3, { life = 5, strength = 12, intelligence = 1 })

Posting a message is an asynchronous operation: it does not block the sender and the execution of the receiver's message handler methods will take place asynchronously, in a next event loop of the current Celedev Lua Context.

Receiving a message

An object subscribes to a message by calling its addMessageHandler method:

LuaObject:addMessageHandler (messageNames, handlerMethodName)

Parameters:

  • messageNames: the identifier(s) of the subscribed message(s) (a string or a list of strings in a Lua table).
  • handlerMethodName: the name of this object's method that shall be called to handle the subscribed message(s).

Example:

function MyClass:init()
    self = self[MySuperclass]:init();
    if self then
        -- Subscribe to a message
        self:addMessageHandler ("A very special notification", "doVerySpecialStuff")

        -- Subscribe to two messages at once
        self:addMessageHandler ({"UserDidTouchAnAlien", "AlienDidTouchUser"}, "handleTouchWithAlien")
    end
    return self
end

-- this is the handler for a message with no parameter
function MyClass:doVerySpecialStuff()
    -- do stuff here
    -- ...
end

-- this is the handler for the more complex message above
function MyClass:handleTouchWithAlien (messageName, touchPoint, velocity, alienInfo)
    if messageName == "UserDidTouchAnAlien" then
        -- handle this
        -- ...
    elseif messageName == "AlienDidTouchUser" then
        -- handle that
        -- ...
    end

    -- do common processing
    -- ...
end

A message handler receives in its parameters the same values that have been passed as parameters of the corresponding message.post. As such, the first parameter of a message handler method is always the message name, followed by message-specific parameters.

An object can not have several message handlers for a same message. Therefore, if addMessageHandler is called multiple time on a same object for subscribing to a same message, only the newest subscription is kept.

Unsubscribing to messages

A message handler can be removed for an object by calling its removeMessageHandler method:

LuaObject:removeMessageHandler (messageNames)

Parameters:

  • messageNames: the identifier(s) of the unsubscribed message(s) (a string or a list of strings in a Lua table or nil); if nil, all message subscriptions are removed for this object.

Example:

-- remove the message handler for a single message
self:removeMessageHandler ("A very special notification")

-- remove the message handlers for several messages at once
self:removeMessageHandler ({"UserDidTouchAnAlien", "AlienIsInBadShape"})

-- remove every remaining message handler
self:removeMessageHandler ()

After removeMessageHandler has been called, the object has the guaranty that its message handler for the specified notification(s) won't be called anymore by the Message System.

System messages

Two messages are defined at the system level. Their role is to notify your program when a code module or a resource is (re)loaded. These messages are:

  • system.did_load_module: sent each time a Lua code module is loaded or dynamically updated.
  • system.did_load_resource: sent each time a non-code resource is loaded or updated by the system.

The system.did_load_module message has the following parameters:

  • full name of the loaded module (as a .-separated path, see Using Lua Modules);
  • results of the loaded module's main chunk (zero or more results).

The system.did_load_resource message has the following parameters:

  • full name of the loaded resource (as a .-separated path, see Using Resources);
  • resource object;
  • resource type (a UTI string);
  • resource URL (a NSURL)

Using messages for handling updates

Usually, when a code module or a resource is dynamically updated, your application needs to run some code to take the change into account, for example to update its internal data structures or refresh the display. This is generally done in message handlers.

This section lists some recommended ways of using message handlers for managing code and resource updates.

Handling code update with system.did_load_module

An object subscribing to the system.did_load_module message will have its associated message handler method called for every code module loaded or updated in the current Lua Context.

Therefore, subscription to the system.did_load_module message should be reserved for objects that play a central role in the application's architecture and really need to handle update notifications concerning other modules.

Handling code update with a custom message

Many objects are only interested in being notified of the updates of a single module, and generally of the module defining their own class (or class extension). In such case, the use of a custom message is the simplest and most efficient solution.

A custom message is simply a message defined by the current code module and posted at the end of the module's main chunk. This way, the custom message is sent each time the current module is loaded or reloaded. By making the instances of the class declared in the module subscribe to this common message (for example in their init method), every instance of the class will have its handler called when the code module is reloaded.

The example below illustrate this pattern:

local MyClass = class.createClass ("MyClass", aSuperclass)

-- define the custom message
local myClassUpdatedMessage = "MyClass is updated" -- or any unique string

function MyClass:init()
    self = self[aSuperclass]:init();
    if self then
        -- Subscribe to the custom message
        self:addMessageHandler (myClassUpdatedMessage, "handleCodeUpdate")
    end
    return self
end

function MyClass:handleCodeUpdate()
    -- Handle the update: e.g. for a view, signal that it needs to be displayed
    self.view:setNeedsDisplay()
end

-- posting the update message shall be done at the end of the module (before returning!)
message.post (myClassUpdatedMessage)

return MyClass

Handling resource updates with system.did_load_resource

For individual resources, you will generally use for this the resource handler function or the object field associated to a ressource in the corresponding getResource call (see Using Resources).

However, if you want to be notified of every resource update occurring in the current Lua Context, you can subscribe to the system.did_load_resource message.

For example, if you want to do some specific processing when images located in a directory named "Animals" are loaded or updated, you could write:

function MyClass:init()
    self = self[MySuperclass]:init();
    if self then
        -- Subscribe to the "system.did_load_resource" message
        self:addMessageHandler ("system.did_load_resource", "handleResourceUpdate")
    end
    return self
end

function MyClass:handleResourceUpdate(messageName, resourceName, resourceObject)
    if resourceName:hasPrefix("Animals.") and resourceObject:isKindOfClass(objc.UIImage) then
        -- process the resource
        -- ...
    end
end

Notifying resource update with a custom message

In many cases, an object is not directly interested in the updates of a resource, but rather needs to be notified when the resource update has been taken into account by some other object in the program. In such case, a custom message will provide a very simple way of propagating the resource update between objects.

As an example of this update propagation pattern, let's suppose that we have two classes in our program: a model class subscribing to a resource of type plist for updating its own data and a controller class that displays a subset of the data provided by the model class.

The model class can be defined as:

local ModelClass = class.createClass ("ModelClass")

local superclass = ModelClass.superclass

-- define the custom message
local modelUpdatedMessage = "ModelClass did update model"

function ModelClass:initWithResourceNamed(modelResourceName)
    self = self[superclass]:init();
    if self then
        -- Subscribe to the model resource
        getResource (modelResourceName, "plist", self, "modelResource")
    end
    return self
end

function ModelClass:handleModelResourceUpdate (modelResourceObject)
    -- Update the internal model data based on modelResourceObject
    -- ...

    -- Post a model-updated message
    message.post (modelUpdatedMessage, self)
end

-- declare a setter for the resource handler field
ModelClass:declareSetters { modelResource = ModelClass.handleModelResourceUpdate }

-- Other methods
-- ...

-- return modelUpdatedMessage in addition of the ModelClass, 
-- so other modules can get it by simply calling "require"
return ModelClass, modelUpdatedMessage

And the controller class can be defined as:

-- get the model update message (provided in the model class module's result)
local ModelClass, modelUpdatedMessage = require "ModelClass"

local UIViewController = objc.UIViewController

local ControllerClass = class.createClass ("ControllerClass", UIViewController)

function ControllerClass:initWithModelObject(modelObject)
    self = self[UIViewController]:init();
    if self then
        self.model = modelObject
        -- Subscribe to the model updated message
        self:addMessageHandler (modelUpdatedMessage, "handleModelUpdate")
    end
    return self
end

function ControllerClass:handleModelUpdate (messageName, modelObject)
    -- check that this message comes from our model
    if modelObject == self.model then
        -- Handle the model update
        -- ...
    end
end

return ControllerClass

When the resource named modelResourceName is updated:

  1. method ModelClass:handleModelResourceUpdate is called (via the modelResource setter) and it gets the new modelResourceObject;
  2. after updating the internal model, ModelClass:handleModelResourceUpdate posts a modelUpdatedMessage with the current model (self) as parameter;
  3. as a result of the message, method ControllerClass:handleModelUpdate is called;
  4. if the model object in the message is the expected model object (self.model), the controller object reads the new state from the model object.

Post a Comment