The Parent View passes the Model property as a string. So if there's a typo, we'll only find out about it at runtime, and we have to copy-and-paste it from the Model's function definition.
The Model updates the Field by passing the property as a string, with similar drawbacks.
The value of the property can't be typed for the IDE.
As you read the requirements and my##Code:
###ILiveModel
public interface ILiveModel
{
function registerLiveListener(liveListener:ILiveListener, fields:Array):Array;
function unregisterLiveListener(liveListener:ILiveListener):void ;
function updateLiveField(fieldName:String, newValue:*):void;
function getLiveFieldValue(fieldName:String):*;
}
###ILiveListener
public interface ILiveListener
{
function updateLiveField(field:String, value:*):void;
}
###ILiveModel implementation, do any other patterns suggest themselves? Have you ever had to implement something similar?
public class KioskModel extends EventDispatcher implements ILiveModel {
private var _termsAndConditionsAgree:Boolean = false;
protected var _liveListeners:Dictionary = new Dictionary(true);
protected var _monitoredFields:Object = { };
public function get termsAndConditionsAgree():Boolean {
return _termsAndConditionsAgree;
}
public function set termsAndConditionsAgree(agree:Boolean):void {
_termsAndConditionsAgree = agree;
updateRegisteredLiveListeners("termsAndConditionsAgree");
}
public function registerLiveListener(liveListener:ILiveListener, fields:Array):Array {
var monitoredFieldListeners:Array;
var fieldValues:Array = [];
trace("<o> Registering ", liveListener, " to watch ", fields);
// Register the input in the Dictionary, so we can unregister its fields when asked to
_liveListeners[liveListener] = fields;
// For each field we've been asked to monitor, add the input, unless it's already watching that field
for each(var field:String in fields) {
if (! field in this) {
throw new ArgumentError("Cannot register Listener Field '" + field + "' on model " + this + ": property or method does not exist");
}
monitoredFieldListeners = _monitoredFields[field];
if (monitoredFieldListeners && monitoredFieldListeners.length) {
if (monitoredFieldListeners.indexOf(liveListener == -1)) {
monitoredFieldListeners.push(liveListener);
_monitoredFields[field] = monitoredFieldListeners;
}
} else {
_monitoredFields[field] = [liveListener];
}
fieldValues.push(getLiveFieldValue(field));
}
// Return an array of all the values we're watching, for convenience.
return fieldValues;
}
public function unregisterLiveListener(liveListener:ILiveListener):void {
var monitoredFields:Array = _liveListeners[liveListener];
var monitoredFieldListeners:Array;
var fieldIndex:int;
trace("<-> Unregistering ", liveListener);
if (monitoredFields) {
for each (var field:String in monitoredFields) {
monitoredFieldListeners = _monitoredFields[field];
if (monitoredFieldListeners && monitoredFieldListeners.length) {
fieldIndex = monitoredFieldListeners.indexOf(liveListener);
if (fieldIndex != -1) {
monitoredFieldListeners.splice(fieldIndex, 1);
if (monitoredFieldListeners.length){
_monitoredFields[field] = monitoredFieldListeners;
} else {
delete _monitoredFields[field];
}
}
}
}
}
delete _liveListeners[liveListener];
}
public function updateLiveField(fieldName:String, newValue:*):void {
if (fieldName in this) {
this[fieldName] = newValue;
} else {
throw new ArgumentError("Cannot update Listener Field '" + fieldName + "' on model " + this + ": property or method is not valid.");
}
}
public function getLiveFieldValue(fieldName:String):* {
if (fieldName in this) {
return (this[fieldName]);
} else {
throw new ArgumentError("Listener Field '" + fieldName + "' in " + this + " is not a valid property or method");
}
}
protected function updateRegisteredLiveListeners(fieldName:String) {
var liveListeners:Array = _monitoredFields[fieldName];
if (liveListeners) {
for each (var liveListener:ILiveListener in liveListeners) {
liveListener.updateLiveField(fieldName, this[fieldName]);
}
}
}
}
###ILiveListener implementation
public class InputRadioButtonView extends AssetView implements ILiveListener {
private const LABEL_OFF = 'off';
private const LABEL_ON = 'on';
protected var _model:ILiveModel;
protected var _modelProperty:String;
protected var _value:Boolean;
public function InputRadioButtonView(assetClip:*, model:ILiveModel, modelProperty:String, disabled:Boolean=false, localizableTextFieldName:String="label") {
super(assetClip, disabled, localizableTextFieldName);
_model = model;
_modelProperty = modelProperty;
}
public function updateLiveField(field:String, value:*):void {
updateClip();
}
/**
* Be sure the clip accurately reflects the current value, every time we switch to its screen.
*/
override public function setupBeforeLocalize():void {
updateClip();
super.setupBeforeLocalize();
}
override public function activate() {
var values:Array;
if (activatable) {
clip.addEventListener(MouseEvent.CLICK, onClick, false, 0, true);
values = _model.registerLiveListener(this, [_modelProperty]);
DebugUtils.print_r(values);
}
super.activate();
}
override public function deactivate() {
if (deactivatable) {
clip.removeEventListener(MouseEvent.CLICK, onClick);
_model.unregisterLiveListener(this);
}
super.deactivate();
}
/**
* Attempts to update the clip, and lets you know if it was successful or not.
* @return
*/
protected function updateClip():Boolean {
// Stash the value for convenience
_value = _model.getLiveFieldValue(_modelProperty);
if (clip){
if (_value) {
movieClip.gotoAndStop(LABEL_ON);
} else {
movieClip.gotoAndStop(LABEL_OFF);
}
return true;
}
return false;
}
protected function onClick(event:MouseEvent):void {
_model.updateLiveField(_modelProperty, ! _value);
}
}
###Parent View implementation
public class HowThisWorksBox extends ContentAssetView {
private var _kioskModel:KioskModel;
public function HowThisWorksBox(clip:*, disabled:Boolean = false) {
super(clip, ["heading", "body"], disabled);
}
override protected function defineFlexItems(parentItem:FlexItem = null):FlexItem {
_kioskModel = KioskModel.getInstance();
return super.defineFlexItems(parentItem)
.chainAsset(new ButtonAssetView("terms_button", ["label"], onTermsButtonClicked))
.chainAsset(new InputRadioButtonView("agree_button", _kioskModel, "termsAndConditionsAgree"));
}
private function onTermsButtonClicked():void {
screen.router.setScreen("main__terms");
}
}
Leverages the Kestrel mobile development framework - which defines the superclasses of this View.