Created
November 26, 2011 16:49
-
-
Save jonnyreeves/1395965 to your computer and use it in GitHub Desktop.
PureMVC RelaxedNotificationMap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package uk.co.jonnyreeves.puremvc | |
{ | |
import flash.errors.IllegalOperationError; | |
import org.puremvc.as3.multicore.interfaces.INotification; | |
/** | |
* Maps a PureMVC Notification to a Function delegate. Add the mappings in your Mediator's Constructor and then | |
* delegate the calls to listNotificationInterests() and handleNotification() through to this class. | |
* | |
* @author Jonny Reeves | |
*/ | |
public class NotificationMap | |
{ | |
/** | |
* Constant used to indicate that a mapping is interested in all Notifications of the supplied notificationName | |
* regardless of Type. | |
*/ | |
public static const ALL_TYPES : String = "*"; | |
private const _notificationTypeByNotificationName : Object = {}; | |
/** | |
* Creates a mapping between the supplied Notification Name and a callback function. The callback function | |
* should only accept a single argument of type INotification. | |
* | |
* @param notificationName The Name of the Notification you want to map to the supplied callback function | |
* @param callback Function delegate which will be called should the PureMVC framework supply an | |
* INotification of the corrospoinding notificationName param. This delegate should | |
* expect a single argument, the INotification instance. | |
* @param notificaitonType Optional, the supplied callback will only be invoked if the `type` property of | |
* the incoming INotification matches this supplied value. | |
*/ | |
public function add(notificationName : String, callback : Function, notificationType : String = ALL_TYPES) : void | |
{ | |
// No invalid Callbacks (must accept a single argument). | |
if (callback.length != 1) { | |
throw new ArgumentError("Refusing to map supplied callback function to Notification: " + notificationName + ". The callback function must accept a single argument (the notification body)"); | |
} | |
// No Overwrites. | |
if (contains(notificationName, notificationType)) { | |
throw new IllegalOperationError("The supplied Notification: " + notificationName + " is already mapped to a callback"); | |
} | |
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName] ||= {}; | |
callbacksByNotificationType[notificationType] = callback; | |
} | |
/** | |
* Returns true if a mapping exists for the supplied Notification Name and Type combination. | |
*/ | |
public function contains(notificationName : String, notificationType : String = ALL_TYPES) : Boolean | |
{ | |
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName]; | |
if (callbacksByNotificationType == null) { | |
return false; | |
} | |
return callbacksByNotificationType[notificationType] != null; | |
} | |
/** | |
* Removes any mappings that may exist for the supplied Notificaiton Name regardless of Notification Type. | |
*/ | |
public function remove(notificationName : String) : void | |
{ | |
delete _notificationTypeByNotificationName[notificationName]; | |
} | |
/** | |
* Removes the mapping that may exist for the supplied Notification Name and Type. | |
*/ | |
public function removeByType(notificationName : String, notificationType : String) : void | |
{ | |
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName]; | |
if (callbacksByNotificationType) { | |
delete callbacksByNotificationType[notificationType]; | |
} | |
} | |
/** | |
* Clears the map, removing all mappings. | |
*/ | |
public function removeAll() : void | |
{ | |
for (var key : * in _notificationTypeByNotificationName) { | |
delete _notificationTypeByNotificationName[key]; | |
} | |
} | |
/** | |
* Returns an Array of NotificationNames which have been added to this NotificationMap instance; this can be | |
* used to satisfy the PureMVC IMediator#listNotificationInterests() method. | |
*/ | |
public function listNotificationInterests() : Array | |
{ | |
const notificationNames : Array = []; | |
for (var key : * in _notificationTypeByNotificationName) { | |
notificationNames.push(key); | |
} | |
return notificationNames; | |
} | |
/** | |
* Handles an incoming PureMVC notificaiton by invoking the callback regsitered to the supplied notificaiton | |
* name. | |
*/ | |
public function handleNotification(note : INotification) : void | |
{ | |
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[note.getName()]; | |
if (callbacksByNotificationType) | |
{ | |
// Invoke the `all types` callback, regardless of the incoming Notification's type property. | |
if (callbacksByNotificationType[ALL_TYPES] != null) { | |
callbacksByNotificationType[ALL_TYPES](note); | |
} | |
// Invoke the callback specific to this type. | |
const noteType : String = note.getType(); | |
if (noteType && callbacksByNotificationType[noteType]) { | |
callbacksByNotificationType[noteType](note); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package uk.co.jonnyreeves.puremvc | |
{ | |
import org.puremvc.as3.multicore.core.View; | |
import org.puremvc.as3.multicore.interfaces.INotification; | |
import org.puremvc.as3.multicore.patterns.observer.Observer; | |
/** | |
* Provides an extention to the default NotificationMap which caches Notifications broadcast via | |
* the Facade whilst the client Mediator is unregistered from the View's Observer Map. | |
* | |
* Map you Notifications in your Mediator's Constructor as per ususal and then tell this Map | |
* which notifications you would like cached via the RelaxedNotificationMap.rememberNotification() | |
* method. Then, when your Mediator's `onRegister` is called, invoke | |
* RelaxedNotificationMap.relayCachedNotifications() at which point the most recent 'remembered' | |
* Notification will be redispatched to the client Mediator. | |
* | |
* @author John Reeves | |
*/ | |
public class RelaxedNotificationMap extends NotificationMap | |
{ | |
private const _registeredObserverMap : Object = {}; | |
private const _cachedNotificationMap : Object = {}; | |
private var _pendingRememberanceSet : Object; | |
private var _multitonKey : String; | |
private var _mediatorName : String; | |
public function RelaxedNotificationMap(mediatorName : String) | |
{ | |
super(); | |
_mediatorName = mediatorName; | |
} | |
public function initializeNotifier(multitonKey : String) : void | |
{ | |
if (_multitonKey != null) { | |
return; | |
} | |
_multitonKey = multitonKey; | |
// Register all the mappings which were waiting for the Core Key to be supplied. | |
if (_pendingRememberanceSet != null) { | |
for (var notificationName : String in _pendingRememberanceSet) { | |
rememberNotification(notificationName); | |
} | |
_pendingRememberanceSet = null; | |
} | |
} | |
override public function removeAll() : void | |
{ | |
super.removeAll(); | |
// Clear all cached notifications. | |
clearNotificationCache(fetchCachedNotifications()); | |
// Clear all observers. | |
for (var notificationName : String in _registeredObserverMap) { | |
forgetNotification(notificationName); | |
} | |
// And any pending chaps who were left waiting... | |
_pendingRememberanceSet = null; | |
} | |
public function rememberNotification(notificationName : String) : void | |
{ | |
// This instance must be supplied with the multitonKey before we can access the | |
// relevant 'View' instance for this PureMVC Core. | |
if (_multitonKey == null) { | |
const rememberanceSet : Object = _pendingRememberanceSet ||= {}; | |
rememberanceSet[notificationName] = true; | |
return; | |
} | |
// Don't allow multiple mappings for the same notificaitonName | |
if (_registeredObserverMap[notificationName] != null) { | |
return; | |
} | |
// Register an Observer against the View Singleton which will cache notifications as they | |
// are broadcast. | |
const observer : Observer = new Observer(cacheNotification, this); | |
_registeredObserverMap[notificationName] = observer; | |
View.getInstance(_multitonKey).registerObserver(notificationName, observer); | |
} | |
public function forgetNotification(notificationName : String) : void | |
{ | |
if (_pendingRememberanceSet && _pendingRememberanceSet[notificationName]) { | |
delete _pendingRememberanceSet[notificationName]; | |
} | |
else { | |
const observer : Observer = _registeredObserverMap[notificationName]; | |
if (observer) { | |
View.getInstance(_multitonKey).removeObserver(notificationName, this); | |
delete _registeredObserverMap[notificationName]; | |
} | |
} | |
} | |
public function relayCachedNotifications() : void | |
{ | |
// Retrieve the list of Notifications that this Map recieved and then clear them. | |
// It's important to clear the Notification Cache before we supply the notifications | |
// to the Mediator in-case it decides it wants to remember other notifications :S | |
const cachedNotifications : Array = fetchCachedNotifications(); | |
clearNotificationCache(cachedNotifications); | |
// Now Relay the notification to the Mediator instance. | |
for each (var notification : INotification in cachedNotifications) { | |
handleNotification(notification); | |
} | |
} | |
private function cacheNotification(note : INotification) : void | |
{ | |
// No need to cache notifications whilst the Mediator is registered. | |
if (View.getInstance(_multitonKey).hasMediator(_mediatorName)) { | |
trace("Mediator is registered with the Facade, not caching: " + note.getName()); | |
return; | |
} | |
// Only store the last notification of this name. | |
_cachedNotificationMap[note.getName()] = note; | |
} | |
private function fetchCachedNotifications() : Array | |
{ | |
const result : Array = []; | |
for each (var notification : INotification in _cachedNotificationMap) { | |
result.push(notification); | |
} | |
return result; | |
} | |
private function clearNotificationCache(notifications : Array) : void | |
{ | |
for each (var notification : INotification in notifications) { | |
delete _cachedNotificationMap[notification.getName()]; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package uk.co.jonnyreeves.puremvc | |
{ | |
import com.iwi.util.puremvc.unitest.PureMVCTestCase; | |
import com.iwi.util.puremvc.unitest.PureMVCTestFacade; | |
import org.flexunit.asserts.assertEquals; | |
import org.flexunit.asserts.assertFalse; | |
import org.flexunit.asserts.assertTrue; | |
import org.puremvc.as3.multicore.interfaces.INotification; | |
/** | |
* @author jonny | |
*/ | |
public class TestRelaxedNotificationMap extends PureMVCTestCase | |
{ | |
private var _notificationMap : RelaxedNotificationMap; | |
private var _mediator : RelaxedNotificationMapClient; | |
private var _recievedNotificatons : Array; | |
public static const NOTE_A : String = "NOTE_A"; | |
[Before] | |
override public function setup() : void | |
{ | |
super.setup(); | |
_mediator = new RelaxedNotificationMapClient(); | |
_notificationMap = _mediator.getNotificationMap(); | |
_recievedNotificatons = []; | |
} | |
[After] | |
override public function teardown() : void | |
{ | |
super.teardown(); | |
} | |
[Test] | |
public function notificationSentWhilstMediatorIsNotRegistered_notificationIsRetrievedFromCache() : void | |
{ | |
initializeNotificationMap(); | |
// Instruct the notification map to listen for NOTE_A and then cache that notification when the | |
// client Mediator is removed. | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
// Send the notification before the Mediator is registered against the Facade. | |
facade.sendNotification(NOTE_A); | |
assertFalse(notificationCount); // Sanity check. | |
// Now re-register our hero Mediator - NOTE_A should be replayed to it. | |
facade.registerMediator(_mediator); | |
assertTrue("Notification was broadcast from the Cache", notificationCount); | |
} | |
[Test] | |
public function notificationIsRemovedFromTheCacheAfterItIsBroadcast() : void | |
{ | |
initializeNotificationMap(); | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
facade.sendNotification(NOTE_A); | |
// Assert the notification is retrieved from the cache when the Mediator is re-registered for the | |
// first time. | |
facade.registerMediator(_mediator); | |
assertEquals("Notification was broadcast from Cache", 1, notificationCount); | |
// Reset the flag and re-register the Mediator. | |
facade.removeMediator(_mediator.getMediatorName()); | |
facade.registerMediator(_mediator); | |
assertEquals("Notification was consumed form the Cache", 1, notificationCount); | |
} | |
[Test] | |
public function onlyTheLatestNotificationIsCached() : void | |
{ | |
initializeNotificationMap(); | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
facade.sendNotification(NOTE_A, "FIRST"); | |
facade.sendNotification(NOTE_A, "SECOND"); | |
facade.registerMediator(_mediator); | |
assertEquals("Notificaiton Cache only caches the most recent notification", 1, notificationCount); | |
assertEquals("Notificaiton Cache only caches the most recent notification", "SECOND", (_recievedNotificatons[0] as INotification).getBody()); | |
} | |
[Test] | |
public function notificaitonIsNotCachedWhilstMediatorIsRegistered() : void | |
{ | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
facade.registerMediator(_mediator); | |
facade.sendNotification(NOTE_A); | |
assertEquals("Notification was routed whilst the Mediator was registered", 1, notificationCount); | |
facade.removeMediator(_mediator.getMediatorName()); | |
facade.registerMediator(_mediator); | |
assertEquals("Notification was not stored in the Cache whilst the Mediator was registered", 1, notificationCount); | |
} | |
[Test] | |
public function rememberancesCanBeMappedBeforeTheMultitonKeyIsSupplied() : void | |
{ | |
// The map is being configured before it can gain access the View singleton (as no | |
// Core Key has been supplied). This is how we expect the map to be used as clients | |
// will create their mappings in the Mediator's Constructor. | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
// Curley-shuffle. | |
initializeNotificationMap(); | |
facade.sendNotification(NOTE_A); | |
facade.registerMediator(_mediator); | |
assertTrue("Notification was broadcast from the Cache", notificationCount); | |
} | |
[Test] | |
public function notificationsCanBeForgotten() : void | |
{ | |
initializeNotificationMap(); | |
_notificationMap.add(NOTE_A, recordNotification); | |
_notificationMap.rememberNotification(NOTE_A); | |
// Lest we forget... | |
_notificationMap.forgetNotification(NOTE_A); | |
facade.sendNotification(NOTE_A); | |
// Now check that the notification is not pulled from the cache. | |
facade.registerMediator(_mediator); | |
assertFalse("Notificaiton was not broadcast from the cache", notificationCount); | |
} | |
private function initializeNotificationMap() : void { | |
// Typically your Mediator will take care of this when it is first registered against the Facade. | |
_notificationMap.initializeNotifier(PureMVCTestFacade.KEY); | |
} | |
private function recordNotification(note : INotification) : void { | |
_recievedNotificatons.push(note); | |
} | |
private function get notificationCount() : int { | |
return _recievedNotificatons.length; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment