Cosmo UI High Level Events Notification Framework Proposal
Introduction
As the Cosmo UI gets richer and richer, we have more and more independent and semi-independent UI elements (which I will call widgets - which are often but not necessarily Dojo widgets) and other code structures. By independent I mean that there is little or no dependencies on other structures at the same level of abstraction. Two examples are the calendar canvas and the item detail view. These elements are different views to the same data. If one element makes a change to that data, the other element needs to know about it.
The Problem
One way to solve this problem is for each element to let the other know when there it has made a change to the data is a simple, but not scalable approach. For two elements this is no big deal maybe. But if we add another, the original widgets have to be modified so that they can let this new widget know about item updates. And if we add three.....well you get the idea - we have situation where all
n widgets need to talk to all other
n -1 widgets. Not to mention all the dependencies between supposedly independent widgets that you are introducing.
The Solution
It would be nice if there was one place which notified all the interested parties when (for example) an item is updated. Better still, elements would register themselves as interested parties, so that the the system could accommodate new elements.
Dojo's topics is just such a system - an object subscribes to a topic its interested in, and a method calls publish whenever it wants to broadcast a message on that topic. Now whenever that topic is published, all subscribers get whatever callback they subscribed with invoked. The code might look something like this:
function CalendarCanvas(){
...
this.handleItemChanges = function(message){
updateCanvas(message.item);
}
dojo.event.topic.subscribe("ItemChanges", this, this.handleItemChanges);
...
}
function DetailForm(){
...
this.service = new ServiceCalls();
this.submitForm = function(){
validate();
service.save(this.item);
}
this.handleItemChanges = function(message){
updateDetailForm(message);
}
dojo.event.topic.subscribe("ItemChanges", this, this.handleItemChanges);
...
}
function ServiceCalls(){
...
this.save = function(item){
try {
//talk to server here
...
dojo.event.topic.publish("ItemChanges", new ItemChangedMessage("updated", item));
} catch (error e){
dojo.event.topic.publish("ItemChanges", new ItemChangedMessage("updated", item, error));
}
}
...
}
When the detail form is submitted (via a call to
DetailForm.submitForm()), it in turn calls the service method
ServiceCalls.save(). The service method publishes an
ItemChangedMessage on the "ItemChanges" topic. If the service call was successful (no exception was thrown) the updated item is put into the Message object as well as the type of change (here, "updated" - other possibilities are "created" and "deleted".) If there was an error, the error object is placed in the message object as well.
Now that the message has been published, all the subscribers to "ItemChanges" will receive this message and be able to update themselves accordingly.
Proposal
Overview
The proposal then is to use dojo's topics as the basis for our high level event notification system. High Level events include changes to data, user actions that affect other views (changing currently selected item), application-wide errors (disconnecting from the server) and others.
Well Defined
The messages that are sent via topics will be well-defined and documented, so that future elements can take advantage of them. Ad-hoc topics or messages should not be created, and existing ones should be either formalized or removed.
Independence
The ideal situation is that no widgets have any direct dependencies on each other and communicate anonymously with each other through topics. Inasmuch as is possible within the preview time-frame as many inter-widget dependencies will be removed and be replaced by this new mechanism. Also, no new inter-widget dependencies will be introduced without a good reason and discussion amongst the team.
When not to...
Topics should not be used where simple method dispatching would do just as well if not better. In other words, if your topic is very case-specific and you only expect one subscriber ever, please re-consider whether or not to use topics.
Service Layer - Backwards Compatability
For the most part, Topics will not affect the API of existing service calls. In other words "getEvents()" is still called "getEvents()" and takes the same parameters (of course, there will be API changes for other reasons, but not because of topics.)
Service Layer - When topics get published
This may be obvious, but Messages will get published on a topic AFTER a service call has been completed - successfully or not.
Whodunnit?
It's preferable that the publishing of a particular message happens at one place in the code, so there is no ambiguity as to "who" should do the calling. For some messages, this is pretty clear: since all saving of items happens at the service layer, the service layer should be publishing topics.
Loading an item(s) is not an Event
Loading items will not cause a message to be published. Travis and I agonized over this, but for performance reasons and philosophical reasons we don't think this is a good idea. The rule for model related messages is that messages only get published when something changes - the fact that something loaded is not a change.
Definitions
- High Level Event
- An event that can be defined semantically, rather than by a particular user action that initiated it. Easier to explain by example: "User clicked save" is not a high level event, but "Item was updated is". "Item was clicked on" is not a high level event but "Item was selected is".
- Message
- A message is the object which contains information about the event that occurred.
- Topic
- Topics are a way of grouping different types of messages so that objects only respond to messages that they are interested. Each topic is just an arbitrary string.
- Publish
- To publish a message is to invoke all the callbacks of all the registered subscribers on the topic of that message.
- Subscribe
- To subscribe is to register for callbacks for messages that are published on a specific topic.
Implementation
Anatomy of a Message
The base Message prototype should have the following properties:
Message
- topic - the topic on which the message should get published
Messages which invoke RPC methods have the following additional properties mixed in:
ServiceMessage
- rid - The id of the asynchronous request.
Finally, let's examine a concrete message (i.e not an abstract one.) Since changes in data are the main drivers for this functionality, let's examine the message objects that would be needed for notifying different parts of the application about data changes. First we need an
ItemChangedMessage?:
ItemChangedMessage inherits from Message,
mixins:
ServiceMessage
- topic = "ItemChanges" <-- always the same for all ItemChangedMessages?
- rid = "123124124" <-- some integer
- type = "updated" <-- either "updated", "created" or "deleted"
- item = <<object>> <-- the item in its updated form (null if deletion)
- itemUid = "asd1231241" <-- some string. Why do we need this AND item if item has the UID in it? Deletions.
We also need a message for errors that come back from the service layer:
ServiceErrorMessage inherits from Message,
mixins:
ServiceMessage
- topic = "ItemChanges" <-- always the same for all ServiceErrorMessages?
- rid = "123124124" <-- the rid of the request that failed
- error = <<error>> <-- An error object, with stack trace if it's a server side message, error message, etc.
Retrofitting with Existing Code, Widgets
The existing code works well, and has been well tested. Preview is just around the corner. How will we possibly have time to re-write all the existing code to use topics instead of their own callbacks on service methods?
The answer is...we don't. We leave the existing code for handling callbacks after service methods complete more or less in place as is. The exception is that we remove the bits in those callbacks which modify other widgets. For example, after an event has been dragged in the calendar canvas widget, the canvas (presumably) has a callback for when the object successfully saves. This callback will update it's own ui, but also others potentially like the detail form. We leave the part in which updates itself, but remove the bits which update the detail form and any other UI elements.
Now that we've done that we add handlers for the
ItemChanges? topic in each of the handlers. Since there is existing code which handles events that widget itself propagated, the first thing you do in each handler is ignore messages which are published as a result of service calls of that widget. Something like this:
function handleItemChangedMessage(message){
if (myRequestIdMap.containsKey(message.rid)){
return;
} else {
//handle stuff here
}
}
So the upshot is that we're using almost of all the existing code, rewriting very little, and only adding the functionality to handle messages from other widgets.
Note: The above might be a little tricky because I would imagine that the async registry code probably gets rid of the rId after the callback is done, so when the topic handler fires it won't find the rId in the registry and won't realize that it's widget was the propagator of that event. Hmmmm. We could keep old rid's around, and periodically get rid of them using a timer or something. Need to talk to mde about all that.
Suggested Topics and Messages