Skip to main content
Added remaining code
Source Link
  1. 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.

  2. The Model updates the Field by passing the property as a string, with similar drawbacks.

  3. 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.

  1. 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.

  2. The Model updates the Field by passing the property as a string, with similar drawbacks.

As you read the requirements and my implementation, do any other patterns suggest themselves? Have you ever had to implement something similar?

  1. 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.

  2. The Model updates the Field by passing the property as a string, with similar drawbacks.

  3. The value of the property can't be typed for the IDE.

##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

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.

Source Link

Input field that dynamically monitors model value

This question is regarding my approach to the following implementation requirement. Can you suggest a better alternative that doesn't suffer from the issues I'll point out in my implementation?

##REQUIREMENT: Create an input field (View) that updates itself automatically whenever a specific property on the Model changes.

  • The input field should be generic (ie: it should work for any Model, and any appropriately typed property)
  • The Parent View that holds the Field should not be responsible for updating the field when the Model changes
  • The programming language is ActionScript, which is a strongly typed EMCAScript variant. I'm not sure whether this is relevant, since this question doesn't deal with code directly.

##MY SOLUTION:

Model implements an Interface that defines the following methods:

  • registerInput(): the Field registers itself with the model and indicates which properties it wants to monitor
  • unregisterInput(): for cleanup /gc purposes
  • updateInputField(): the Field can update the model when the user interacts with it

Input Field implements an Interface that defines the following method:

  • updateField(): the Model can call this on any registered Field, when a monitored property changes

The Input Field is instantiated by the Parent View (that has a reference to the Model), by providing it with the Model instance (typed to the Interface) and the NAME (String) of the property on the Model we're want to monitor.

ISSUES:

This solution works well, but there are two issues, both dealing with the fact that the Model's property is passed as a string, which means the IDE can't use autosuggest or type checking.

  1. 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.

  2. The Model updates the Field by passing the property as a string, with similar drawbacks.

As you read the requirements and my implementation, do any other patterns suggest themselves? Have you ever had to implement something similar?