Overview | File | Class | Tree | Index | Help
| The Gravey Framework and RATS RIA
PREV NEXT | FRAMES NO FRAMES

## grvMVC.js

#### Summary

Collection of model-view-controller support classes. This library supports rich-internet-application code in the browser using the Model-View-Controller design pattern (supported by Observer, Command, Bean, Composite, etc. design patterns). A detailed explanation of the functionality of this library can be found in the AJAX from Scratch series of articles.
Version: 2.0
Requires:

- grvUtils.js
- grvValidate.js
- grvClass.js

Author: Bruce Wallace (PolyGlotInc.com)

Class Summary
MVCAJAXReplyCmd | This class implements the "process AJAX reply" command.
MVCAttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object.
MVCBoolEditController | This class manages a scalar editor with a set of yes,no,unknown radio buttons attached to the specified Boolean (i.e.
MVCBoolModel | This class encapsulates a Boolean scalar data model.
MVCButtonController | This class is a Controller that
      (1) watches a MVCBoolModel from which the "enable" state can
          be deduced (actually only uses MVCBoolModel.isTrue()),
      (2) manages a button view using the enable state,
      (3) defines a "pushed" event.
MVCChkBoxEditController | This class manages a form field "checkbox" editor of the specified MVCScalarModel.
MVCCmdButtonController | This class is a Button Controller for "undo/redo" buttons.
MVCCmdImgButtonController | This class is an Image Button Controller for "undo/redo" buttons.
MVCCollection | the MVCCollection "interface" has no code, only an API followed by convention (ie DUCK-TYPING)
      The required "interface" for "collections":
      (1) getCount() - returns number of elements in collection
      (2) getItem( itemKey ) - return specified element
      (3) iterate( function(itemKey,itemObject,itemIndex) ) - calls
          specified function on each element in collection;
          If the function returns a true then the iteration
          stops right there instead of continuing thru rest
          of the elements in the collection
      (4) getMemento() - return a "memento" that can be accepted later
      (5) setMemento(m) - update the current state based on given "memento"
      (6) reset() - empties collection of elements
      (7) dump() - returns debug string with contents of collection
      (8) iterate(func) - invokes func(itemKey,itemObject,itemIndex)
          on each item in collection
See Memento design pattern.
MVCCommand | This class acts as the abstract base class for each Command (ala Command design pattern).
MVCContext | This class encapsulates the criteria for building a MVCView such that if it changes, the view needs rebuilding.
MVCController | This class acts as the base class for each controller (that supports an underlying MVCView).
MVCDateEditController | This class manages a Date form field editor of the specified MVCScalarModel.
MVCDecode | This class encapsulates a code/decode (aka picklist) item.
MVCDequeModel | This class encapsulates a data model for Stacks and Queues.
MVCDollarEditController | This class manages an editor of the specified Dollar MVCScalarModel.
MVCDualBoolController | This class manages dual views (viewer and editor) of the specified (Boolean i.e.
MVCDualChkBoxController | This class manages dual views (viewer and editor) of the specified (boolean) Scalar Model.
MVCDualController | This abstract class manages dual views (viewer and editor) of a MVCScalarModel.
MVCDualDateController | This class manages dual views (viewer and editor) of the specified Date Scalar Model.
MVCDualDollarController | This class manages dual views (viewer and editor) of the specified Dollar Scalar Model.
MVCDualMenuController | This class manages dual views (viewer and popup menu) of the specified Models.
MVCDualStringController | This class manages dual views (viewer and editor) of the specified String Scalar Model.
MVCDualTextController | This class manages dual views (viewer and editor) of the specified "Text" Scalar Model.
MVCEditRule | This class encapsulates edit rules.
MVCFieldEditController | This class manages a form field editor of the specified MVCScalarModel.
MVCFormButtonController | This class is a Button Controller that manages a form button
MVCImgButtonController | This class is a ButtonController that manages an image button
MVCListModel | This class encapsulates a data model for a List of objects.
MVCListView | This class is a MVCView that expects to subscribe to a MVCListModel and will invoke itemHTMLstr() on each member of the list when buildHTMLstr() is called and itemPaint() on each member when paintHTML() is called.
MVCMap | This class encapsulates a Map of object/key pairs and implements the MVCCollection interface
MVCMapModel | This class encapsulates a data model for a Map of object/key pairs.
MVCMenuItem | the MVCMenuItem "interface" has no code, only an API followed by convention (ie DUCK-TYPING)
      The required "interface" for "menu items":
      (1) getDescription() - return "this" formatted for menu item
MVCModel | This abstract class acts as the base class for each MVC data model; Since models can subscribe to other models, they can act as both MVCObserver and MVCObservable.
MVCMutex | This class encapsulates a Map of mutual exclusion data; It self-registers instantiations into a static Map; This class implements Lamport's bakery algorithm for mutual exclusion; It is used to execute Command objects while making sure that no other Command objects (that are using Mutex) are executed at the same time. NOTE: our main use for this is to keep background AJAX processing from confusing foreground UI processing, which can otherwise occur because both are making data model changes simultaneously.
MVCObservable | the MVCObservable "interface" has no code, only an API followed by convention (ie DUCK-TYPING)
      The required "interface" for "observables":
      (1) addObserver( observer ) - add given MVCObserver to your list
MVCObserver | This class acts as the abstract base class for each "observer class" (ala Observer design pattern).
MVCPadScalarView | This class produces a blank-padded view of the specified MVCScalarModel.
MVCPopupMenuController | This class manages a popup menu which watches a MVCListModel (specifying the menu items) embedded within a MVCSelectionModel that reflects which item is/should-be currently selected.
MVCROAttributeModel | This class encapsulates a "wrapper" data model for a specified attribute of a specified base object.
MVCScalarEditCmd | This class implements a scalar edit command.
MVCScalarEditController | This abstract class manages an editor of the specified MVCScalarModel.
MVCScalarModel | This class encapsulates a Scalar data model with a default implementation of the scalar being implemented via a (bean-like) property.
MVCScalarView | This class produces a view of the specified MVCScalarModel.
MVCSelectionModel | This class encapsulates the data model for a selector which indicates the currently selected item in a specified MVCCollection data model.
MVCTextEditController | This class manages a form field "textarea" editor of the specified MVCScalarModel.
MVCTxtButtonController | This class is a Button Controller that manages a button that can display text based on a data model's current value
MVCUndoRedoModel | This class encapsulates the data model for the Command Dequeue which supports deep undo and redo.
MVCView | This class acts as the base class for each (MVC) View.

Method Summary
`static Object` | `handleEvent(aEvent) * { * var myEvent = aEvent ? aEvent : window.event; * } * * @author Bruce Wallace (PolyGlotInc.com) * @requires grvUtils.js * @requires grvValidate.js * @requires grvClass.js * @version 2.0 */ var kMVCImgPath = "/images/"; var kMVCDim_ = "DIM_"; var kMVCImgSpacer = 'spacer.gif'; var kMVCImgRedSpacer = 'redspacer.gif'; var kMVCImgCalendar = 'calendar.gif'; function mvcSPACER()`
`static boolean` | `mvcDoCmd( command )` Do the given command in "synchonized" mode (meaning that it will wait until commands that are already running/queued have finished).
`static String` | `mvcEmbedAttributeViewer( <MVCView> parentView, <String> attribute, <Function> optFormatter, <Object> optParam )` Create and embed, as a subview, a MVCScalarView of the specified data model attribute.
`static String` | `mvcEmbedBoolDualEditor( <MVCView> parentView, <String> attribute, <int> min, <String> className )` Create and embed, as a subview, a MVCDualBoolController of the specified data model attribute which is expected to be a (boolean) data element.
`static String` | `mvcEmbedChkBoxDualEditor( <MVCView> parentView, <String> attribute, <int> min, <String> className )` Create and embed, as a subview, a MVCDualChkBoxController of the specified data model attribute which is expected to be a (boolean) data element.
`static String` | `mvcEmbedDateDualEditor( <MVCView> parentView, <String> attribute, <String> className )` Create and embed, as a subview, a MVCDualDateController of the specified data model attribute which is expected to be a String data element containing a Date.
`static String` | `mvcEmbedDollarDualEditor( <MVCView> parentView, <String> attribute, <String> className, <Function> optFormatFunction )` Create and embed, as a subview, a MVCDualDollarController of the specified data model attribute which is expected to be a dollar amount data element.
`static String` | `mvcEmbedDualMenu( <MVCView> parentView, <String> attribute, <MVCSelectionModel> menuModel, <String> className, <String> optEvtHndlrName, <String> optInitDesc )` Create and embed, as a subview, a MVCDualMenuController of the specified data model attribute
`static String` | `mvcEmbedStringDualEditor( <MVCView> parentView, <String> attribute, <int> min, <int> max, <String> className, <Function> optKeyFilter )` Create and embed, as a subview, a MVCDualStringController of the specified data model attribute which is expected to be a String data element.
`static String` | `mvcEmbedTextDualEditor( <MVCView> parentView, <String> attribute, <int> rows, <int> cols, <int> max, <String> className )` Create and embed, as a subview, a MVCDualTextController of the specified data model attribute which is expected to be a String data element.
`static Object` | `mvcREDSPACER600()`
`static Object` | `mvcREDSPACER768()`
`static Object` | `mvcSECTIONBREAK()`
`static void` | `onMVCFieldEditFocus( <Element> field )` handle "entering a text field" event per webreference tip)
`static boolean` | `onMVCFieldEditKey( <String> viewID )` handle "key pressed" events in text fields
`static Object` | `onMVCGlobalKeyPress()` pre-screen all keypress events for entire page This implementation handles ctrl-z and ctrl-y to invoke Undo and Redo respectively.
`static void` | `onMVCRedoBtnPressed()` redo-button-pressed event handler
`static boolean` | `onMVCScalarEditUpdate( <String> viewID )` ScalarEditController update-event handler
`static void` | `onMVCUndoBtnPressed()` undo-button-pressed event handler

    ///////////////////////////////////////////////////////////////////////////
    // This file uses JSDoc-friendly comments [ http://jsdoc.sourceforge.net/ ]
    // (JSDoc tutorial in book: "Foundations of AJAX", Chap 5)
    // TO BUILD DOCS: If ActivePerl and HTML::Templates are installed,
    // and JSDoc is installed at c:\JSDoc-1.9.8.1\jsdoc.pl
    // then execute buildDoc.bat and view jsdoc\index.html
    ///////////////////////////////////////////////////////////////////////////

    /**
     * @file         grvMVC.js
     * @fileoverview Collection of model-view-controller support classes.
     * This library supports rich-internet-application code in the browser
     * using the Model-View-Controller design pattern (supported by Observer,
     * Command, Bean, Composite, etc. design patterns).  A detailed explanation
     * of the functionality of this library can be found in the <a target="_blank"
     * href="http://www.polyglotinc.com/AJAXscratch/">AJAX from Scratch</a>
     * series of articles.
     *
     * @todo convert event handling to be netscape safe:
     * <div onclick="handleEvent(event)">Click me!</div>
     * function handleEvent(aEvent)
     * { // if aEvent is null, means the Internet Explorer event model, so get window.event.
     *   var myEvent = aEvent ? aEvent : window.event;
     * }
     *
     * @author       Bruce Wallace  (PolyGlotInc.com)
     * @requires     grvUtils.js
     * @requires     grvValidate.js
     * @requires     grvClass.js
     * @version      2.0
     */

    //////////////////////////////////////////////////////////////////
    // GRAVEY LEXICAL CODING CONVENTIONS:
    // (*) All private variables or functions start with "_"
    // (*) All variables and functions start with a lowercase letter
    // (*) All Classes start with an uppercase letter
    // (*) All Class methods and instance variables start with lowercase
    // (*) All Class "static" methods and variables start with uppercase
    // (*) All constants start with "k"
    // (*) All global variables start with "g"
    // (*) All event handler functions start with "on"
    //
    // (*) All Gravey utility global variables start with "gGrv"
    // (*) All Gravey MVC     global variables start with "gMVC"
    // (*) All Gravey EDO     global variables start with "gEDO"
    // (*) All Gravey utility functions start with "grv"
    // (*) All Gravey MVC     functions start with "mvc"
    // (*) All Gravey MVC event handler functions start with "onMVC"
    // (*) All Gravey EDO event handler functions start with "onEDO"
    // (*) All Gravey MVC classes start with "MVC"
    // (*) All Gravey EDO classes start with "EDO"
    //////////////////////////////////////////////////////////////////

    // Override these constants as needed
    var kMVCImgPath			= "/images/";		//path to Gravey graphic files
    var kMVCDim_			= "DIM_";			//prefix to "dimmed" version of graphics
    var kMVCImgSpacer		= 'spacer.gif';		//one pixel transparent
    var kMVCImgRedSpacer	= 'redspacer.gif';	//one pixel red
    var kMVCImgCalendar		= 'calendar.gif';	//calendar icon

    function       mvcSPACER(){ return '<img src="'+kMVCImgPath+kMVCImgSpacer
    		+'" height="1" width="5" alt="" align="middle"/>';
    }
    function mvcSECTIONBREAK(){ return '<img src="'+kMVCImgPath+kMVCImgSpacer
    		+'" height="30" width="600" alt="" align="middle"/>';
    }
    function mvcREDSPACER600(){ return '<img src="'+kMVCImgPath+kMVCImgRedSpacer
    		+'" height="20" width="600" alt="600 pixels wide" align="middle"/>';
    }
    function mvcREDSPACER768(){ return '<img src="'+kMVCImgPath+kMVCImgRedSpacer
    		+'" height="20" width="768" alt="768 pixels wide" align="middle"/>';
    }

    /////////////////// GLOBAL VARIABLES /////////////////////

    /** Global Undo Command Queue */
    var gMVCUndoCmds;

    /** Global Root Container View */
    var gMVCRootView;

    ////////////////////////////////////////////
    //////// PATTERN FOUNDATION CLASSES ////////
    ////////////////////////////////////////////

    /**
     * @class the MVCCollection "interface" has no code, only
     * an API followed by convention (ie <a target="_blank"
     * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>)
     *<pre>
     *  The required "interface" for "collections":
     *  (1) getCount() - returns number of elements in collection
     *  (2) getItem( itemKey ) - return specified element
     *  (3) iterate( function(itemKey,itemObject,itemIndex) ) - calls
     *      specified function on each element in collection;
     *      If the function returns a true then the iteration
     *      stops right there instead of continuing thru rest
     *      of the elements in the collection
     *  (4) getMemento() - return a "memento" that can be accepted later
     *  (5) setMemento(m) - update the current state based on given "memento"
     *  (6) reset() - empties collection of elements
     *  (7) dump() - returns debug string with contents of collection
     *  (8) iterate(func) - invokes func(itemKey,itemObject,itemIndex)
     *      on each item in collection
     *</pre>
     * See <a target="_blank" href="http://en.wikipedia.org/wiki/Mementto_pattern">
     * Memento design pattern</a>.
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCCollection(){/* this function exists only for JsDoc purposes.*/}

    /**
     * @class the MVCMenuItem "interface" has no code, only
     * an API followed by convention (ie <a target="_blank"
     * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>)
     *<pre>
     *  The required "interface" for "menu items":
     *  (1) getDescription() - return "this" formatted for menu item
     *</pre>
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCMenuItem(){/* this function exists only for JsDoc purposes.*/}

    Class(MVCDecode,["Code Type","Code #","Code Description"]);
    /**
     * @class This class encapsulates a code/decode (aka picklist) item.
     * This class implements the {@link MVCMenuItem} interface.
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDecode()
    {
    	/** @param {String} type type of code
    	 *  @param {String} code value of code
    	 *  @param {String} desc description of value
    	 *  @param {String} aux  auxiliary text
    	 */
    	this.konstructor = function( type, code, desc, aux )
    	{
    		// define instance variables
    		this.type = type;
    		this.code = code;
    		this.desc = desc;
    		this.aux  = aux;
    	}

    	/** return the description of "this" @type String */
    	this.getDescription = function() {
    		return /*this.code + "-" + */ this.desc;
    	}

    	/** return the debug details of "this" @type String */
    	this.dump = function()
    	{
    		var		dStr = new Array();
    				dStr.push( "Decode>>>[" );
    				dStr.push( "type=" +(this.type?this.type:"null") );
    				dStr.push( "code=" +(this.code?this.code:"null") );
    				dStr.push( "desc=" +(this.desc?this.desc:"null") );
    				dStr.push( " aux=" +(this.aix ?this.aux :"null") );
    				dStr.push( "]" );
    		return	dStr.join(" ");
    	}
    }

    Class(MVCMap);
    /**
     * @class This class encapsulates a Map of object/key pairs
     * and implements the {@link MVCCollection} interface
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCMap()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		// init instance variables
    		this.name = optName || "unnamed Map";
    		this.map  = new Object();
    		this.N    = 0;
    	}

    	/** return a (deep) clone of "this" object. @type Map */
    	this.clone = function()
    	{
    		var m = new MVCMap( this.name );
    		this.iterate( function(K,o,i){ m.addItem(K,o); } );
    		return m;
    	}

    	/** return how many items are in map @type int */
    	this.getCount   = function(   ){ return this.N; }

    	/** return item as string associated with given key @type String */
    	this.getItemStr = function( k ){ return this.map[k].toString(); }

    	/** return item associated with given key @type Object */
    	this.getItem    = function( k ){ return this.map[k]; }

    	/** delete item associated with given key */
    	this.delItem    = function( k ){ delete this.map[k];    --this.N;     }

    	/** add given object and associate with given key */
    	this.addItem    = function(k,o){ this.map[k] = o;       ++this.N;     }

    	/** reset map to empty */
    	this.reset      = function(   ){ this.map = new Object(); this.N = 0; }

    	/** iterate thru items in map calling specified function
    	 * @param {Function} f function that takes key and object as params
    	 * and returns true if the iteration should be stopped before all
    	 * items in map are processed.
    	 */
    	this.iterate = function( f )
    	{
    		var i = 0;
    		for (k in this.map)
    		  if ( f( k, this.getItem(k), i++ ) )
    		    return; //early
    	}

    	/** debug method to return this list as a string @type String */
    	this.dump = function()
    	{
    		var s = this.name + "=Map==>";
    		this.iterate(
    		  function( i, ithItem ){ s += "["+i+"]="+ ((ithItem.dump?ithItem.dump():ithItem.toString()) +"; "); }
    		);
    		return s;
    	}

    	/** Return the key that comes after the given key.
    	 * If no key is specified, return the first key.
    	 * If no key matches the specs above, return null.
    	 */
    	this.nextKey = function( k )
    	{
    		var nextKey = null;
    		this.iterate(
    			function(K,o,i){
    				if (!k) return nextKey = K;/*TRICKY!*/
       				if (k==K) k=null;
       			}
    		);
    		return nextKey;
    	}

    	/** Return the object that comes AFTER the given key
    	 * or, if no key specified, return the first object.
    	 * If no object fits the specs above, return null.
    	 */
    	this.next = function( k )
    	{
    		var    n = this.nextKey(k);
    		return n ? this.getItem(n) : null;
    	}

    	/** return the first object in this map or null if empty @type Object */
    	this.first = function(){ return this.next(); }

    	/** return the first key in this map or null if empty @type Object */
    	this.firstKey = function(){ return this.nextKey(); }

    	/** return a memento of the current state of "this" @type Object */
    	this.getMemento = function(){ return this.clone(); }

    	/** update "this" based on the given memento
    	 *  @param {Object} m the memento
    	 */
    	this.setMemento = function(m)
    	{
    		this.reset();
    		var self = this;
    		m.iterate( function(K,o,i){ self.addItem(K,o); } );
    	}
    }

    Class(MVCContext);
    /**
     * @class This class encapsulates the criteria for building
     * a {@link MVCView} such that if it changes, the view needs
     * rebuilding. This class is meant to be subclassed, however,
     * the default implementation implements a single-value context
     * where the value must be comparable via the "=" operator.
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCContext()
    {
    	/** @param {anyComparableType} optContext optional value to save as view context */
    	this.konstructor = function( optContext )
    	{
    		// init instance variables
    		this.context = optContext;
    	}

    	/**
    	 * @param {MVCContext} lastBuildContext context to compare to THIS
    	 * @return whether the current context is the same as the given one
    	 * @type boolean
    	 */
    	this.sameAs = function( lastBuildContext )
    	{
    		if (lastBuildContext==null) return false;
    		return this.context == lastBuildContext.context;
    	}

    	/** return THIS formatted as string @type String */
    	this.toString = function(){ return "{"+this.context+"}"; }
    }

    // ------------------------
    // --- OBSERVER PATTERN ---
    // ------------------------

    /**
     * @class the MVCObservable "interface" has no code, only
     * an API followed by convention (ie <a target="_blank"
     * href="http://en.wikipedia.org/wiki/Duck_typing">DUCK-TYPING</a>)
     *<pre>
     *  The required "interface" for "observables":
     *  (1) addObserver( observer ) - add given {@link MVCObserver} to your list
     *</pre>
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     * @see MVCModel
     */
    function MVCObservable(){/* this function exists only for JsDoc purposes.*/}

    Class(MVCObserver);
    /**
     * @class This class acts as the abstract base class for each
     * "observer class" (ala Observer design pattern).
     *<pre>
     * Subclasses of MVCObserver should define/override:
     *   (1) the {@link #update} method which accepts update events
     *       and takes one parameter which is the "observable"
     *       plus one optional adhoc parameter.
     *</pre>
     * @extends GrvObject
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCObserver()
    {
    	this.konstructor = function()
    	{
    		// init instance variables
    		this.model = null;	//what do I watch (mostly, but not exclusively)
    	}

    	/**
    	 * override this method with your logic to respond to an update
    	 * event from one of the Observables you are subscribed to.
    	 * @param {MVCObservable} observable the generator of this update event
    	 * @param {Object} optAdhocObj optional adhoc object passed by sender
    	 */
    	this.update = function( observable, optAdhocObj ){ return; /*override me*/ }

    	/**
    	 * subscribe to (aka watch/monitor/observe) the given observable
    	 * @param {MVCObservable} observable object to monitor
    	 */
    	this.subscribe = function( observable ) {
    		this.model = observable;
    		observable.addObserver( this );
    	}
    }

    // -----------------------
    // --- COMMAND PATTERN ---
    // -----------------------

    Class(MVCCommand);
    /**
     * @class This class acts as the abstract base class for each
     * Command (ala <a target="_blank"
     * href="http://en.wikipedia.org/wiki/Command_pattern">
     * Command design pattern</a>).
     *<pre>
     * Subclasses of MVCCommand should define/override:
     *  (1) the constructor to load the do/undo/redo context data
     *      which should set the "valid" attribute to a negative
     *      number if the command cant properly be initiated.
     *  (2) the {@link #doit} method which executes the command
     *  (3) the {@link #undo} method which "rolls back" the command
     *  (4) the {@link #redo} method which "un-rolls-back" the command
     *  (5) canUndo property if this cmd is only meant for mutex
     *
     * FYI, REDO would be different than DO, for example, in the case
     * that DO had to do a database search to get a value, but REDO
     * could simply use that saved value without re-searching for it.
     *</pre>
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCCommand()
    {
    	// init static member variables
    	if (grvIsUndefined(MVCCommand.NextID)) MVCCommand.NextID = 1;

    	/** @param {String} cmdDesc general description of this command
    	 *  @param {String} optErrMsg if not null then cancel this cmd
    	 */
    	this.konstructor = function( optName, optErrMsg )
    	{
    		// init instance variables
    		if (optErrMsg){ this.state = -1; alert(optErrMsg); return; }

    		if (optName) this.name = optName;
    		this.state   = 0;	//-1=invalid,0=notDone,1=done,2=undone,3=redone
    		this.canUndo = true;
    		this.id      = MVCCommand.NextID++
    	}

    	/** "Do command" logic */
    	this.doit      = function(){ alert("DOIT:"+this); /*override me*/ }

    	/** "command UNDO" logic */
    	this.undo      = function(){ alert("UNDO:"+this); /*override me*/ }

    	/** "command REDO" logic. Default is to just call {@link #doit} */
    	this.redo      = function(){ this.doit();         /*override me*/ }

    	/** return details of this command for user viewing @type String */
    	this.details   = function(){ return "";           /*override me*/ }

    	/** return THIS formatted as string @type String */
    	this.toString  = function(){ return this.name + ": " + this.details(); }

    	/** return true iff this command should not even be started @type boolean */
    	this.isInvalid = function(){ return this.state<0; }

    	///////////////// "synchronized" API //////////////////

    	/** synchronized call with global view blocking */
    	this.syncDo = function( methodName )
    	{
    		gMVCRootView.block();//grvBreak("blocked to ["+View.Depth+"] for "+this);
    		new MVCMutex( this, methodName );
    		gMVCRootView.unblock();//grvBreak("unblocked to ["+View.Depth+"] after "+this);
    	}

    	/** synchronized this.DOIT() */
    	this.syncDoIt = function(){ this.syncDo("DOIT"); }

    	/** synchronized this.UNDO() */
    	this.syncUnDo = function(){ this.syncDo("UNDO"); }

    	/** synchronized this.REDO() */
    	this.syncReDo = function(){ this.syncDo("REDO"); }

    	//////////////// "unsynchronized" API /////////////////

    	/** "DO" this command if in the proper state. @throw error if in wrong state */
    	this.DOIT = function()
    	{
    		if (this.state!=0) throw "Cant DO an invalid or already started command.";
    		gMVCRootView.block();
    		this.doit();
    		this.state = 1;
    		gMVCRootView.unblock();
    	}

    	/** "REDO" this command if in the proper state. @throw error if in wrong state */
    	this.REDO = function()
    	{
    		if (this.state!=2) throw "Cant REDO a command that isnt undone.";
    		gMVCRootView.block();
    		this.redo();
    		this.state = 3;
    		gMVCRootView.unblock();
    	}

    	/** "UNDO" this command if in the proper state. @throw error if in wrong state */
    	this.UNDO = function()
    	{
    		if (!this.canUndo) throw "UNDO not supported for this command.";
    		switch( this.state )
    		{
    			case 1:
    			case 3:
    					gMVCRootView.block();
    					this.undo();
    					this.state = 2;
    					gMVCRootView.unblock();
    					break;
    			default:
    				throw "Cant UNDO a command that isnt done.";
    		}
    	}
    }

    /** Do the given command in "synchonized" mode (meaning that
     * it will wait until commands that are already running/queued
     * have finished).  NOTE: BECAUSE OF THIS, YOU WILL DEADLOCK
     * IF ANY COMMAND INVOKES ANOTHER COMMAND!!!!!
     * @return false if the command was invalid and not run
     * @type boolean
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcDoCmd( command )
    {
    	if ( command.isInvalid() ) return false;
    	if (!command.canUndo)
    	{
    	     gMVCUndoCmds.reset();
    	     command.syncDoIt();
    	}
    	else gMVCUndoCmds.newDo( command );
    	return true;
    }

    Class(MVCMutex,["command object","method name"]);
    /**
     * @class This class encapsulates a Map of mutual exclusion data;
     * It self-registers instantiations into a static Map;
     * This class implements <a target="_blank"
     * href="http://wikipedia.org/wiki/Lamport's_bakery_algorithm">
     * Lamport's bakery algorithm</a> for mutual exclusion;
     * It is used to execute Command objects while making sure
     * that no other Command objects (that are using Mutex)
     * are executed at the same time.<p>
     * NOTE: our main use for this is to keep background AJAX
     * processing from confusing foreground UI processing,
     * which can otherwise occur because both are making data
     * model changes simultaneously.
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCMutex()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCMutex.CpuSlice)){//keep this line above jsdoc comments!

    		/** Static method to give a slice of CPU to mutex with given ID
    		 * @param {int} cmdID ID of command to resume
    		 * @param {int} optStartID optional ID of command on which we are waiting;
    		 * if not specified then start at top of list of all pending commands.
    		 */
    		MVCMutex.CpuSlice = function( cmdID, optStartID )
    		{
    		//	grvBreak("slice id="+cmdID+" start="+optStartID);
    			MVCMutex.Map.getItem(cmdID).cpuSlice(optStartID);
    		}

    		// init static member variables
    		MVCMutex.Map = new MVCMap("global mutex map");
    	}

    	/** @param {Command} cmdObj Command object to be wrapped in Mutex
    	 *  @param {String} methodName name of method being run on cmdObj
    	 *  @param {boolean} optInhibitInvoke optional flag that if true
    	 * will inhibit immediate launching of cmdObj by this constructor
    	 */
    	this.konstructor = function( cmdObj, methodName, optInhibitInvoke )
    	{
    		// init instance variables
    		this.cmd      = cmdObj;
    		this.id       = cmdObj.id;
    		this.name     = "Mutex for " + this.id;
    		this.choosing = false;
    		this.number   = 0;
    		this.methodID = methodName;

    		// auto-register "this"
    		MVCMutex.Map.addItem( this.id, this );

    		// auto start processing unless inhibited
    		optInhibitInvoke || this.invoke();
    	}

    	/** launch the processing of "this" mutex/command object */
    	this.invoke = function()
    	{
    		this.choosing = true;
    		this.number   = grvTimestamp();
    		this.choosing = false;
    		MVCMutex.CpuSlice( this.id );
    	}

    	/** continue the processing of "this" mutex/command object
    	 * @param {int} optStartID optional ID of last command we were waiting on;
    	 * if not specified then start at top of list of all pending mutex/commands.
    	 * @see #CpuSlice
    	 */
    	this.cpuSlice = function( optStartID )
    	{
    		var startID = optStartID ? optStartID : MVCMutex.Map.firstKey();
    		for (var j=MVCMutex.Map.getItem(startID); j; j=MVCMutex.Map.next(j.id))
    		{
    		    if (
    		    	// delay if thread j still receiving its #
    		        j.choosing
    			    // delay if threads with smaller numbers (or with same #,
    			    // but with higher priority) still finishing their work
    			    || (j.number
    			    && (j.number <  this.number ||
    			       (j.number == this.number && j.id < this.id) ) )
    		    ){
    				grvBusyDo( "MVCMutex.CpuSlice", '('+ this.id +','+ j.id +')', 10 );
    				return;//run away to fight another day (or millisecond)
    			}
    		}

    		//by this point, we have exclusive access, so...

    			// BEGIN CRITICAL SECTION...
    			this.cmd[ this.methodID ]();
    			//...END CRITICAL SECTION

    		//end exclusive access
    		this.number = 0;

    		//since we are using cmd IDs instead of static thread numbers
    		//(as is used in original bakery algorithm), we delete this
    		//mutex to free memory.
    		MVCMutex.Map.delItem( this.id );
    	}
    }

    Class(MVCEditRule);
    /**
     * @class This class encapsulates edit rules. STD edit rules:<pre>
     * MVCEditRule.kZero '0'  Read-Only; zero
     * MVCEditRule.kCopy 'X'  Read-Only; copy of legal balance (HACK!)
     * MVCEditRule.kRO   'R'  Read-Only; (current value)
     *
     * MVCEditRule.kEdit '?'  Read-Write; no validation
     * MVCEditRule.kReqd '*'  Read-Write; non-empty required
     * MVCEditRule.kNonZ '#'  Read-Write; non-zero required
     *
     * MVCEditRule.kPos  '+'  Read-Write; positive or zero
     * MVCEditRule.kPOS  '{'  Read-Write; positive or zero (but not empty)
     * MVCEditRule.kNeg  '-'  Read-Write; negative or zero
     * MVCEditRule.kNEG  '}'  Read-Write; negative or zero (but not empty)
     * MVCEditRule.kFone '('  Read-Write; phone number (or empty)
     * MVCEditRule.kFONE ')'  Read-Write; phone number (required)
     * MVCEditRule.kZip  'z'  Read-Write; zipcode (or empty)
     * MVCEditRule.kZIP  'Z'  Read-Write; zipcode (required)
     * MVCEditRule.kAlfa 'a'  Read-Write; letters[space] (or empty)
     * MVCEditRule.kALFA 'A'  Read-Write; letters[space] (required)
     * MVCEditRule.kDec  '.'  Read-Write; decimal number (or empty)
     * MVCEditRule.kDEC  ':'  Read-Write; decimal number (required)
     * MVCEditRule.kInt  '8'  Read-Write; 0-9 (or empty)
     * MVCEditRule.kINT  '9'  Read-Write; 0-9 (required)
     * MVCEditRule.kBool 'y'  Read-Write; Y,N,y,n,true,false (or empty)
     * MVCEditRule.kBOOL 'Y'  Read-Write; Y,N,y,n,true,false (required)
     * MVCEditRule.kDate 'd'  Read-Write; date (or empty)
     * MVCEditRule.kDATE 'D'  Read-Write; date (required)
     * MVCEditRule.kAlNo ';'  Read-Write; A-Za-z0-9 (or empty)
     * MVCEditRule.kALNO '|'  Read-Write; A-Za-z0-9 (required)
     *</pre>
     * @extends GrvObject
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCEditRule()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCEditRule.kEdit)){//keep this line above jsdoc comments!

    		// init static member constants
    		// NOTE: Dont EVER use blank as a rule! you are warned!
    		// NOTE: Some of these constants must sync with GRAVE database!
    		MVCEditRule.kZero = '0';	//R/O zero
    		MVCEditRule.kCopy = 'X';	//R/O copy of legal balance
    		MVCEditRule.kRO   = 'R';	//R/O (current value)

    		MVCEditRule.kEdit = '?';	//editable - no validation
    		MVCEditRule.kReqd = '*';	//non-empty required
    		MVCEditRule.kNonZ = '#';	//non-zero required

    		MVCEditRule.kPos  = '+';	//positive or zero
    		MVCEditRule.kPOS  = '{';	//positive or zero (but not empty)
    		MVCEditRule.kNeg  = '-';	//negative or zero
    		MVCEditRule.kNEG  = '}';	//negative or zero (but not empty)
    		MVCEditRule.kFone = '(';	//phone number (or empty)
    		MVCEditRule.kFONE = ')';	//phone number (required)
    		MVCEditRule.kZip  = 'z';	//zipcode (or empty)
    		MVCEditRule.kZIP  = 'Z';	//zipcode (required)
    		MVCEditRule.kInt  = '8';	//0-9 (or empty)
    		MVCEditRule.kINT  = '9';	//0-9 (required)
    		MVCEditRule.kDec  = '.';	//decimal number (or empty)
    		MVCEditRule.kDEC  = ':';	//decimal number (required)
    		MVCEditRule.kAlfa = 'a';	//A-Za-z[space] (or empty)
    		MVCEditRule.kALFA = 'A';	//A-Za-z[space] (required)
    		MVCEditRule.kBool = 'y';	//Y,N,y,n,true,false (or empty)
    		MVCEditRule.kBOOL = 'Y';	//Y,N,y,n,true,false (required)
    		MVCEditRule.kDate = 'd';	//date (or empty)
    		MVCEditRule.kDATE = 'D';	//date (required)
    		MVCEditRule.kAlNo = ';';	//A-Za-z0-9 (or empty)
    		MVCEditRule.kALNO = '|';	//A-Za-z0-9 (required)

    		/** Static method to return whether specified edit rule is readonly.
    		 * @param {char} rule rule to use
    		 * @type boolean
    		 */
    		MVCEditRule.IsReadOnly = function(rule)
    		{
    			switch( rule )
    			{
    				case MVCEditRule.kZero:
    				case MVCEditRule.kCopy:
    				case MVCEditRule.kRO:
    					return true;
    			}
    			return false;
    		}

    		/** Static method to validate value with STD edit rule.
    		 * @param {MVCScalarModel} model model whose current value to validate
    		 * @param {char} rule rule to apply
    		 * @return null if valid else error msg
    		 * @type String
    		 * @throw error if unknown rule is specified
    		 */
    		MVCEditRule.Validate = function( model, rule )
    		{
    			var value = model.getValue();
    			if (value && value.toString().indexOf("~")>=0)
    				return "The tilde (~) character is always illegal.";

    			//pre-check required fields
    			switch( rule )
    			{
    				case MVCEditRule.kReqd:
    				case MVCEditRule.kDATE:
    				case MVCEditRule.kFONE:
    				case MVCEditRule.kALNO:
    				case MVCEditRule.kALFA:
    				case MVCEditRule.kBOOL:
    				case MVCEditRule.kINT:
    				case MVCEditRule.kDEC:
    				case MVCEditRule.kPOS:
    				case MVCEditRule.kNEG:
    				case MVCEditRule.kZIP:
    					if (grvIsEmpty(value))
    					  return "Value ["+value+"] must not be empty";
    			}

    			//we assume at this point that missing required values are handled
    			switch( rule )
    			{
    				case MVCEditRule.kCopy:
    					//no validation here--caller must handle copying value
    				case MVCEditRule.kReqd:
    				case MVCEditRule.kRO:
    				case MVCEditRule.kEdit:
    					//no extra validation at this point
    					break;

    				case MVCEditRule.kFONE:
    				case MVCEditRule.kFone:
    					if (grvIsPhoneNum(value)) break;
    					return "Value ["+value+"] must be a phone number.";

    				case MVCEditRule.kBOOL:
    				case MVCEditRule.kBool:
    					if (grvIsEmpty(value) || grvBoolValue(value)!=null) break;
    					return "Value ["+value+"] must be 'Y/N' or 'true/false'.";

    				case MVCEditRule.kDATE:
    				case MVCEditRule.kDate:
    					var errMsg = grvDateParseErrorMsg(value);
    					if (errMsg==null) break;
    					return "Value ["+value+"] must be a date..."+errMsg;

    				case MVCEditRule.kZIP:
    				case MVCEditRule.kZip:
    					if (grvIsZipCode(value)) break;
    					return "Value ["+value+"] must be a zipcode.";

    				case MVCEditRule.kALNO:
    				case MVCEditRule.kAlNo:
    					if (grvIsAlphaNum(value)) break;
    					return "Value ["+value+"] must contain only letters/digits.";

    				case MVCEditRule.kALFA:
    				case MVCEditRule.kAlfa:
    					if (grvIsAlphaBlank(value)) break;
    					return "Value ["+value+"] must contain only letters.";

    				case MVCEditRule.kZero:
    					//initial validation only--caller must handle setting value
    					if (value==0) break;
    					return "Value ["+value+"] must be Zero.";

    				case MVCEditRule.kINT:
    				case MVCEditRule.kInt:
    					if (grvIsInteger(value)) break;
    					return "Value ["+value+"] must be an integer.";

    				case MVCEditRule.kDEC:
    				case MVCEditRule.kDec:
    					if (grvIsSignedFloat(value)) break;
    					return "Value ["+value+"] must be a number.";

    				case MVCEditRule.kPOS:
    				case MVCEditRule.kPos:
    					if (value>=0) break;
    					return "Value ["+value+"] must be Positive.";

    				case MVCEditRule.kNEG:
    				case MVCEditRule.kNeg:
    					if (value<=0) break;
    					return "Value ["+value+"] must be Negative.";

    				case MVCEditRule.kNonZ:
    					if (value!=0) break;
    					return "Must select a known (non-zero) value.";

    				default:
    					grvLoudThrow("Unknown MVCEditRule:["+rule+"]");
    			}

    			//do this last! Give custom validation a shot
    			if (model instanceof MVCAttributeModel) return model.validate();

    			//no errors found after all tests
    			return null;
    		}
    	}

    	/** no-op constructor...not used */
    	this.konstructor = function(){}
    }

    ////////////////////////////////////////////
    ///////////////// MODELS ///////////////////
    ////////////////////////////////////////////

    Class(MVCModel).Extends(MVCObserver);
    /**
     * @class This abstract class acts as the base class for each
     * MVC data model; Since models can subscribe to other models,
     * they can act as both {@link MVCObserver} and {@link MVCObservable}.
     * This class implements the {@link MVCObservable} interface.
     * @extends MVCObserver
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCModel()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		this.souper();

    		// init instance variables
    		if (optName) this.name = optName;
    		this.hasChanged  = false; //only since last notifyObservers
    		this.everChanged = false; //since last "neverChanged()"
    //grvTraceMrk("everChanged false leaving constructor for "+this.name);
    		this.subscribers = new Array();
    		this.autoCommit  = true;
    		this.updateStamp();
    	}

    	/** debug method generating alert with subscriber list */
    	this.dumpSubscribers = function()
    	{
    		var N = this.subscribers.length;
    		var s = this.name + " observed by["+N+"]: ";
    		for (var i=0; i<N; ++i)
    		  s += (this.subscribers[i].name + "; ");
    		grvBreak(s);
    	}

    	/** return "this" formatted as string @type String */
    	this.toString = function(){ return grvObjectToShortInitializer(this); }

    	/** set the "dirty" and "everChanged" flags to true */
    	this.dirty = function(){
    		this.hasChanged = this.everChanged = true;
    		this.updateStamp();
    	}

    	/** clear the "dirty" flag that says "there has been a change to
    	 * this model since the last update event broadcase via publish()"
    	 */
    	this.clean = function(){ this.hasChanged  = false; }

    	/** return current value of funky "everChanged" flag */
    	this.neverChanged = function(){ this.everChanged = false; }

    	/** tell observers that we have changed
    	 * @param {boolean} resetMark if true clear funky "everChanged" flag
    	 * @param {Object} optAdhocObj optional adhoc object to pass to observers
    	 */
    	this.publish = function( resetMark, optAdhocObj )
    	{
        	this.dirty();
        	if (resetMark) this.neverChanged();
        	if (this.autoCommit) this.notifyObservers( optAdhocObj );
    	}

    	/** generic "bean" property GETTER */
    	this.GET  = function(property      ){ return this[property];  }

    	/** generic "bean" property SETTER (w/o publish) */
    	this._SET = function(property,value){ this[property] = value; }

    	/** generic "bean" property SETTER (w/publish)
    	 * @return flag saying if we published
    	 * @type boolean
    	 */
    	this.SET  = function(property,value)
    	{
    		if (this.GET(property)==value) return false;
    		this._SET(property,value);
    		this.publish(false,property);
    		return true;
    	}

    	/** update the timestamp on this model */
    	this.updateStamp = function(){ this.timestamp = new Date().valueOf(); }

    	/** inhibit publishing until matching EndTransaction() called */
    	this.BeginTransaction = function()
    	{
    		gMVCRootView.block();
    		if (!this.autoCommit) grvError("already started transaction");
    		this.autoCommit = false;
    	}

    	/** publish a "batch" of updates
    	 * @param {boolean} totalReload clears "everChanged" flag iff true
    	 */
    	this.EndTransaction = function(totalReload)
    	{
    		if (this.autoCommit) grvError("Not in transaction!");
    		this.autoCommit  = true;
    		if (totalReload) this.neverChanged();
    		this.updateStamp();
    		this.notifyObservers();
    		this.reloaded();
    		gMVCRootView.unblock();
    	}

    	/** handle "Model reloaded" event. */
    	this.reloaded = function(){ /* override me if needed */ }

    	/** {@link MVCObservable} API */
    	this.addObserver = function( observer )
    	{
    		grvValidateArgs("MVCModel.addObserver",["observer"],arguments);
        	this.subscribers.push( observer );
    	}

    	/** @deprecated @throws not implemented exception */
    	this.delObserver = function( observer )
    	{
    		grvValidateArgs  ( "MVCModel.delObserver", ["observer"], arguments );
    		grvNotImplemented( "MVCModel.delObserver" );
    	    //delete this.subscribers[observer.getID()];
    	}

    	/** if "dirty" flag is set, Notify all subscribers of change
    	 * to our state; When done, if we were the initiator of the
    	 * cascade of update events (ie if global transaction depth
    	 * is back to zero when we are done), then the views will be redrawn.
    	 * @param {Object} optAdhocObj optional adhoc object passed to observers
    	 */
    	this.notifyObservers = function( optAdhocObj )
    	{
    		if (this.hasChanged)
    		{
    			gMVCRootView.block();
    			var N = this.subscribers.length;
    			for (var i=0; i<N; ++i)
    			{
    //grvTraceMVC(this.name+"["+optAdhocObj+"] updates["+i+"] "+this.subscribers[i].name);
    				this.subscribers[i].update( this, optAdhocObj );
    			}
    			gMVCRootView.unblock();
    		}
    		this.clean();
    	}
    }

    Class(MVCListModel).Extends(MVCModel);
    /**
     * @class This class encapsulates a data model for a List of objects.
     * This class implements the {@link MVCCollection} interface.
     * @extends MVCModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCListModel()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		this.souper( optName );

    		// init instance variables
    		this._reset();
    	}

    	/** return a (deep) clone of "this" object. @type MVCListModel */
    	this.clone = function()
    	{
    		var m = new MVCMap( this.name );
    		this.iterate( function(K,o,i){ m.addItem(K,o); } );
    		return m;
    	}

    	/** debug method to return this list as a string @type String */
    	this.dump = function()
    	{
    		var s = this.name + "=ListModel==>";
    		this.list.iterate(
    		  function( i, ithItem ){ s += ((ithItem.dump?ithItem.dump():ithItem.toString()) + "; "); }
    		);
    		return s;
    	}

    	/** provide a "unique" number based on the state of this list
    	 * NOTE: the quality of this hash code depends on the quality
    	 * of the hash code provided by each list item.
    	 * @deprecated
    	 * @type int
    	 */
    	this.hash = function()
    	{	//this is a Q&D poor excuse for a hash algorithm
    		var N = this.list.length;
    		var h = 0;
    		for (var i=0; i<N; ++i)
    			h += (this.list[i].hash() * (2*(i+1)));
    		return h;
    	}

    	/** pop top item off list but dont publish @return item @type Object */
    	this._pop       = function( ){ return this.list.pop(); }

    	/** push item onto list but dont publish @return index @type int */
    	this._push      = function(o){ this.list.push(o); return this.getCount()-1; }

    	/** delete given index from list @return deleted item @type Object */
    	this._del       = function(i){ var o=this.list[i]; this.list.splice(i,1); return o; }

    	/** clear list and update timestamp but dont publish */
    	this._reset     = function( ){ this.list = new Array(); this.updateStamp(); return this; }

    	/** return count of items in list @type int */
    	this.getCount   = function( ){ return this.list.length; }

    	/** return item in list with given index @type Object */
    	this.getItem    = function(i){ return this.list[i]; }

    	/** return item in list with given index as formatted string @type String */
    	this.getItemStr = function(i){ return this.list[i].toString(); }

    	/** push given item onto list and publish @return index @type int */
    	this.addItem    = function(o){ var i=this._push(o); this.publish(); return i; }

    	/** delete given index from list and publish @return deleted item @type Object */
    	this.delItem    = function(i){ var o=this._del(i); this.publish(); return o; }

    	/** clear list and publish */
    	this.reset      = function( ){ this._reset(); this.publish(true); return this; }

    	/** iterate thru items in list calling specified function
    	 * @param {Function} f function that takes index and object as params
    	 * and returns true if the iteration should be stopped before all
    	 * items in list are processed.
    	 */
    	this.iterate = function(f)
    	{
    		var N = this.getCount();
    		for (var i=0; i<N; ++i)
    		  if ( f( i, this.getItem(i), i ) )
    		    return; //early
    	}

    	/** add the given object into the list just before the given zero-based-index */
    	this.addBefore = function(i,o){ this.list.splice(i,0,o); this.publish(); }

    	/** return a memento of the current state of "this" @type Object */
    	this.getMemento = function()
    	{
    		var m = new Array();
    		for (var i=0; i<this.list.length; ++i) m[i] = this.list[i];
    		return m;
    	}

    	/** update "this" based on the given memento
    	 *  @param {Object} m the memento
    	 */
    	this.setMemento = function(m)
    	{
    		this._reset();
    		for (var i=0; i<m.length; ++i) this.list[i] = m[i];
    		this.publish();
    	}
    }

    Class(MVCDequeModel).Extends(MVCListModel);
    /**
     * @class This class encapsulates a data model for Stacks
     * and Queues. The "stack" is built within a dequeue
     * such that up/down do not change the queue itself.
     * @extends MVCListModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDequeModel()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName ){
    		this.souper( optName );
    	}

    	/** clear the queue and reset the pseudo-stack but dont publish */
    	this._reset = function( ){
    		this.list = new Array();
    		this.tos  = -1; //position in deque of top of pseudo-stack
    		this.updateStamp();
    	}

    	/** DEQUE API: add given object to back of the line @return item */
    	this.addFirst = function(o){         this.list.unshift(o); /*this.publish();*/ return o; }
    	/** DEQUE API: add given object to front of the line @return item */
    	this.addLast  = function(o){         this._push       (o); /*this.publish();*/ return o; }
    	/** DEQUE API: remove object from back of the line @return item */
    	this.delFirst = function( ){ var o = this.list.shift  ( ); /*this.publish();*/ return o; }
    	/** DEQUE API: remove object from front of the line @return item */
    	this.delLast  = function( ){ var o = this._pop        ( ); /*this.publish();*/ return o; }

    	/** QUEUE API: add given object to back of the line @return item */
    	this.enqueue  = function(o){ return this.addFirst(o); }
    	/** QUEUE API: remove object from front of the line @return item */
    	this.dequeue  = function( ){ return this.delLast ( ); }

    	/** STACK API: push given object to front of the line aka top of the stack @return item */
    	this.push = function(o){ this.tos = this.getCount()  ; return this.addLast(o); }
    	/** STACK API: remove given object from front of the line aka top of the stack @return item */
    	this.pop  = function( ){ this.tos = this.getCount()-1; return this.delLast( ); }

    	/** pseudo-Stack API: return the top of the pseudo-stack */
    	this.top     = function(offset){
    		if (offset) offset = parseInt(offset); else offset = 0;
    		return this.getItem( offset+this.tos );
    	}

    	/** pseudo-Stack API: return index of next up iff we can go up */
    	this.upIndex = function(){
    		return ((this.getCount()-this.tos)<=1) ? null : this.tos+1;
    	}
    	/** pseudo-Stack API: non-destructive push/get */
    	this._up     = function(){
    		++this.tos;
    		return this.top();
    	}
    	/** pseudo-Stack API: return index of next down iff we can go down */
    	this.downIndex = function(){
    		return (this.tos<0) ? null : this.tos;
    	}
    	/** pseudo-Stack API: non-destructive pop */
    	this._down   = function(){
    		--this.tos;
    		return this.top(1);	// return what we "popped"
    	}
    	/** pseudo-Stack API: throw away items above top of pseudo-stack */
    	this._cutback = function(){
    	  while (this.getCount()>(this.tos+1)) this._pop();
    	}
    }

    Class(MVCMapModel).Extends(MVCModel);
    /**
     * @class This class encapsulates a data model for a Map of
     * object/key pairs. Its API is a wrapper for the {@link MVCMap} API.
     * This class implements the {@link MVCCollection} interface.
     * @extends MVCModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCMapModel()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		this.souper( optName );

    		// init instance variables
    		this.map = new MVCMap("Map for :"+this.name);
    	}

    	this.getCount   = function(   ){ return this.map.getCount(); }
    	this.getItemStr = function(k  ){ return this.map.getItemStr(k); }
    	this.getItem    = function(k  ){ return this.map.getItem(k); }
    	this.delItem    = function(k)  { this.map.delItem(k);   this.publish(); }
    	this.addItem    = function(k,o){ this.map.addItem(k,o); this.publish(); }
    	this.reset      = function(   ){ this.map.reset();      this.publish(true); }
    	this.iterate    = function(f)  { this.map.iterate(f); }

    	/** debug method to return this list as a string @type String */
    	this.dump = function() {
    		return this.name + "=MapModel==>" + this.map.dump();
    	}

    	/** return a memento of the current state of "this" @type Object */
    	this.getMemento = function(){ return this.map.getMemento(); }

    	/** update "this" based on the given memento
    	 *  @param {Object} m the memento
    	 */
    	this.setMemento = function(m){ this.map.setMemento(m); this.publish(); }
    }

    Class(MVCScalarModel).Extends(MVCModel);
    /**
     * @class This class encapsulates a Scalar data model with a
     * default implementation of the scalar being implemented via
     * a (bean-like) property. The property name can optionally
     * be specified.<p>
     * Scalar models assume that a validity attribute of the
     * basic model value can also be set and it's member name
     * is based on the name of the basic value's member name.
     * By convention, the validity attribute consists of an error
     * message string if invalid or null if valid.
     * @extends MVCModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCScalarModel()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCScalarModel.kValiditySuffix)){//keep this line above jsdoc comments!

    		/** The suffix added to the basic name to get the validity attribute name. */
    		MVCScalarModel.kValiditySuffix = ".err";
    	}

    	/** @param {String} optPropName optional property name to use instead of default
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( optPropName, optName  )
    	{
    		this.souper( optName );

    		// init instance variables
    		this.pname = optPropName || "scalarvalue";
    	}

    	/** set scalar to given value but dont publish */
    	this._setValue = function(x){        this._SET(this.pname,x); }

    	/** set scalar to given value and publish */
    	this.setValue  = function(x){ return this. SET(this.pname,x); }

    	/** get scalar value */
    	this.getValue  = function( ){ return this. GET(this.pname  ); }

    	/** set the validity attribute of this scalar value and dont publish
    	 * @param {String} errMsg the validity attribute (as an error message)
    	 */
    	this._setValidity = function( errMsg ){
    		       this._SET( this.pname+MVCScalarModel.kValiditySuffix, errMsg );
    	}

    	/** return the validity attribute of this scalar value @type String */
    	this.getValidity  = function(){
    		return this. GET( this.pname+MVCScalarModel.kValiditySuffix );
    	}
    }

    Class(MVCBoolModel).Extends(MVCScalarModel);
    /**
     * @class This class encapsulates a Boolean scalar data model.
     * @extends MVCScalarModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCBoolModel()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName ){
    		this.souper( "boolflag", optName );
    	}

    	/** return the current value of this model */
    	this.isTrue  = function(){ return this.getValue(); }

    	/** set model to given value and publish
    	 * @param {boolean} b value to set model to
    	 */
    	this.setFlag = function(b){ this.setValue(b); }
    }

    Class(MVCAttributeModel,["base object","attribute"]).Extends(MVCScalarModel);
    /**
     * @class This class encapsulates a "wrapper" data model for a
     * specified attribute of a specified base object.
     * The specified attribute can NOT be a method call.
     * @extends MVCScalarModel
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCAttributeModel()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCAttributeModel.kValidateSuffix)){//keep this line above jsdoc comments!

    		/** The suffix added to the basic name to get the validate function name. */
    		MVCAttributeModel.kValidateSuffix = ".vfunc";
    	}

    	/** @param {Object} baseObject object whose property we are wrappering
    	 *  @param {String} attribute name of baseObject's member we are wrappering
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( baseObject, attribute, optName )
    	{
    		this.souper( undefined, optName?optName:attribute );

    		// init instance variables
    		this.attribute = attribute;
    		this.base      = baseObject;
    	}

    	/** set the value of base object attribute and publish */
    	this.setValue = function(x)
    	{
    		this._setValue( x );
    		this.publish();
    	}

    	/** set the value of base object attribute but dont publish */
    	this._setValue = function(x){        this.base[ this.attribute ] = x; }

    	/** return the value of base object attribute */
    	this.getValue  = function( ){ return this.base[ this.attribute ];     }

    	/** set the validity attribute of this attribute but dont publish
    	 * @param {String} errMsg the validity attribute (as an error message)
    	 */
    	this._setValidity = function( errMsg ){
    		       this.base[ this.attribute+MVCScalarModel.kValiditySuffix ] = errMsg;
    	}

    	/** return the validity attribute of this attribute @type String */
    	this.getValidity = function(){
    		return this.base[ this.attribute+MVCScalarModel.kValiditySuffix ];
    	}

    	/** return error message from custom validation method or null for no error */
    	this.validate = function()
    	{
    		if (this.base && this.base[ this.attribute+MVCAttributeModel.kValidateSuffix ])
    		{
    			 return this.base[ this.attribute+MVCAttributeModel.kValidateSuffix ]( this.attribute );
    		}
    		else return null;
    	}
    }

    Class(MVCROAttributeModel,["base object","attribute"]).Extends(MVCAttributeModel);
    /**
     * @class This class encapsulates a "wrapper" data model for a
     * specified attribute of a specified base object.
     * The specified attribute can in fact be a method name and
     * it will be called as needed. This model is READ-ONLY.
     * @extends MVCAttributeModel
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCROAttributeModel()
    {
    	/** @param {Object} baseObject object whose property we are wrappering
    	 *  @param {String} attribute name of baseObject's member we are wrappering
    	 *  @param {String} optParam optional parameter to pass to attribute if
    	 *                  it is a method
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( baseObject, attribute, optParam, optName )
    	{
    		this.souper( baseObject, attribute, optName );

    		// init instance variables
    		this.optParam = optParam;
    	}

    	/** invoke our attribute as a method call and return result */
    	this.invoke = function(f,optParam){ return f.call(this.base,optParam); }

    	this._setValue    = function(){ grvError("attempt to set a read-only model"); }
    	this._setValidity = function(){ this._setValue(); }
    	this.setValue     = function(){ this._setValue(); }

    	/** return current value of base object attribute (even if it is a method). */
    	this.getValue = function(){
    		var x = this.base[ this.attribute ];
    		if (x && x instanceof Function) return this.invoke(x,this.optParam);
    		return x;
    	}
    }

    Class(MVCSelectionModel,["Collection Model"]).Extends(MVCScalarModel);
    /**
     * @class This class encapsulates the data model for a selector
     * which indicates the currently selected item in a specified
     * MVCCollection data model. This means that the range of legal values
     * for this model's value is [0..Collection.getCount()-1] plus
     * a "nothing selected" value.
     * @extends MVCScalarModel
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCSelectionModel()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCSelectionModel.kNothingSelected)){

    		// init static member variables
    		MVCSelectionModel.kNothingSelected  = -1;
    		MVCSelectionModel.kPropertyIdSelect = 'selected';
    	}

    	/** @param {Object} collModel Collection data model we select from
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( collModel, optName )
    	{
    		this.souper( MVCSelectionModel.kPropertyIdSelect, optName );

    		// init instance variables
    		this._selectNothing();
    		this.subscribe( collModel );
    	}

    	/** return our Collection model @type Collection */
    	this.getList = function( ){ return this.model; }

    	/** return the specified item in our Collection
    	 * @param {Object} i item key
    	 */
    	this.getItem = function(i){ return this.getList().getItem(i); }

    	/** return the size of our MVCCollection model */
    	this.getCount = function( ){ return this.getList().getCount(); }

    	/** return whether there is currently a selection @type boolean */
    	this.hasSelection = function( ){ return this.getValue()!=MVCSelectionModel.kNothingSelected; }

    	/** return the currently selected MVCCollection item or undefined if nothing selected */
    	this.getSelection = function( ){ return this.getItem( this.getValue() ); }

    	/** return the currently selected MVCCollection item formatted as string @type String */
    	this.getSelectionStr = function( ){ return this.getSelection().toString(); }

    	/**  return the specified MVCCollection item formatted as description @type String */
    	this.getDescription  = function(i){
    		var item = this.getItem(i);
    		return item ? item.getDescription() : 'Unknown Value "'+i+'"';
    	}

    	/** select "nothing" but dont publish */
    	this._selectNothing = function() { this._setValue( MVCSelectionModel.kNothingSelected ); }

    	/** select "nothing" and publish */
    	this.selectNothing = function(){
    		this._selectNothing();
    		this.publish();
    	}

    	/** select the specified Collection item but dont publish
    	 * @param {Object} k key of item to select
    	 * @param {boolean} optLenient optional flag to allow out of range to selectNothing
    	 */
    	this._select = function(k,optLenient)
    	{
    		if ( k!=0 && this.getIndex(k)<0 ) {
    			if (!optLenient) grvError("BAD SelectionModel KEY["+k+"]");
    			this._selectNothing();
    		}
    		else this._setValue(k);
    	}

    	/** select the specified MVCCollection item and publish
    	 * @param {Object} k key of item to select
    	 * @param {boolean} optLenient optional flag to allow out of range to selectNothing
    	 */
    	this.select	= function(k,optLenient){ this._select(k,optLenient); this.publish(); }

    	/** select the zero-th item (NOT "nothing selected") but dont publish */
    	this._unselect = function( ){ this._select(0); }

    	/** select the zero-th item (NOT "nothing selected") and publish */
    	this.unselect = function( ){ this._unselect(); this.publish(true);}

    	/** re-select the current value and publish */
    	this.reselect = function( ){ this.publish(); }

    	/** handle MVCCollection model update event by "unselect"ing */
    	this.update = function( ){
    		if ( this.getList().hasChanged ) this.unselect();
    //grvTraceEvt("UnSelectUpdate["+this+"]");
    	}

    	/** return the index into the MVCCollection model of the specified key
    	 * (or the current selection if no key specified)
    	 * @return -1 if not found
    	 * @type int
    	 */
    	this.getIndex = function( optKey )
    	{
    		var index = -1;
    		var goal  = optKey ? optKey : this.getValue();
    		this.model.iterate(
    		  function(key,o,i){ if (key==goal){ index = i; return true; } }
    		);
    		return index;
    	}

    	/** return a memento of the current state of "this" @type Object */
    	this.getMemento = function()
    	{
    		var  m = new Object();
    		m.list = this.getList().getMemento();
    		m.val  = this.getValue();
    		return m;
    	}

    	/** update "this" based on the given memento
    	 *  @param {Object} m the memento
    	 */
    	this.setMemento = function(m)
    	{
    		this.getList().setMemento( m.list );
    		this.select( m.val, true );
    	}
    }

    Class(MVCUndoRedoModel).Extends(MVCDequeModel);
    /**
     * @class This class encapsulates the data model for the
     * Command Dequeue which supports deep undo and redo.
     * NOTE: This logic invokes the "synchronized" version
     * of the MVCCommand API.
     * @extends MVCDequeModel
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCUndoRedoModel()
    {
    	this.konstructor = function(){
    		this.souper( "undo/redo commands" );
    	}

    	/** Add a new command object to the top of the "stack" and "do" it. */
    	this.newDo = function(c){ this._cutback();
    							{this.push(c).syncDoIt(); this.publish();} }

    	/** "pop the top" command object and "undo" it. */
    	this.unDo  = function( ){ if (this.downIndex()!=null)
    							{this._down().syncUnDo(); this.publish();} }

    	/** "unpop the top" command object and "redo" it. */
    	this.reDo  = function( ){ if (this.  upIndex()!=null)
    							{this.  _up().syncReDo(); this.publish();} }

    	/** iff command index is defined, return command description @type String */
    	this.cmdDesc = function(i){
    		return (i!=null) ? this.getItem(i).toString() : null;
    	}

    	/** iff there is another undo-able command return its description @type String */
    	this.hasUnDo = function(){ return this.cmdDesc( this.downIndex() ); }

    	/** iff there is another redo-able command return its description @type String */
    	this.hasReDo = function(){ return this.cmdDesc( this.  upIndex() ); }
    }

    /** Global Undo Command Queue */
    gMVCUndoCmds = new MVCUndoRedoModel();

    ////////////////////////////////////////////
    ///////////////// VIEWS  ///////////////////
    ////////////////////////////////////////////

    Class(MVCView);
    /**
     * @class This class acts as the base class for each (MVC) View.
     *<p>
     * Views are responsible for keeping up-to-date the HTML
     * associated with a particular portion of the web page
     * identified via a "hook" (i.e. an HTML element ID).
     *<p>
     * The view should display the current state of the data
     * in the model(s) that it "watches".  [NOTE: Views do not
     * "subscribe" to Models and react to their individual update
     * events because all views need to draw in a coordinated
     * top-down fashion.]
     *<p>
     * Each View is also a container of subviews (as needed)
     * and coordinates their layout by managing some skeleton
     * framework HTML (e.g. tables/divs/spans/etc) to which
     * the subviews hook and manage [I.E. the GoF  <a target="_blank"
     * href="http://www.javaworld.com/javaworld/jw-09-2002/jw-0913-designpatterns_p.html">
     * Composite design pattern</a>].
     *<p>
     * Once set up, views just react to "draw" events where
     * they draw "this" view and then recurse thru any subviews
     * invoking their draw method. [This is so that any elements
     * of this view that are to be "hooks" for any subviews can
     * be generated by this view first.]
     *<p>
     * Whenever a {@link MVCController} event causes some {@link MVCModel}
     * to change, (and after all observing data models have finished
     * their updates), the global "root container" view will initiate
     * a single draw event cascade to update all Views on the page.
     *<p>
     * "Drawing" entails first looking at the appropriate data
     * model(s) for this view and deciding whether they require
     * "rebuilding" the HTML of this view, and if so, replacing
     * the current HTML with newly generated HTML [via the innerHTML
     * of the HTML Element ID associated with this view].
     * Then the {@link #paint} method is invoked to "decorate" the view
     * with the model(s) current data (i.e. set any HTML attributes
     * that need updating e.g. background color). Normally, HTML
     * need not be constantly rebuilt, only decorated.
     * A special case is where a container needs to rebuild its
     * HTML, all subviews are forced to as well (even if they
     * wouldnt normally based on the views they are watching).
     * <p>
     * The HTML (in string form) is generated by the abstract method
     * "buildHTMLstr". The abstract method "mustRebuild" decides
     * whether the draw event requires the HTML to be rebuilt before
     * calling "paintHTML" (which is called if "mustRepaint").
     *<p><pre>
     * Subclasses of View should define/override:
     *   (1) {@link #buildHTML} constructs this view's HTML
     * OR, use the default buildHTML which builds, via innerHTML, the
     * HTML string returned from buildHTMLstr(), hence you would
     * override instead:
     *   (1) {@link #buildHTMLstr} generates this view's HTML (as string)
     *
     *   (2) {@link #paintHTML} modifies/decorates existing HTML structures
     *   (3) {@link #mustRepaint} decides if this view's HTML needs repainting
     *   (4) {@link #mustRebuild} decides if this view's HTML needs rebuilding
     * The default implementation of mustRebuild() requires that
     * instead of overriding mustRebuild() instead override:
     *   (4) {@link #currentContext} returns an MVCContext subclass object
     *       containing the driver information for building this view.
     *</pre>
     * @extends GrvObject
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCView()
    {
    	// init "static" Class methods/elements
    	if ( grvIsUndefined(MVCView.Depth) ) {//keep this line above jsdoc comments!

    		/** Static method to draw the root view (and hence all views) */
    		MVCView.DrawRoot = function() { gMVCRootView.draw(); }

    		//init static member variables
    		MVCView.Depth = 0;
    	}

    	/** @param {boolean} optDisable optional "disable view" flag (default false)
    	 *  @param {String} optName optional name of this instance
    	 *  @param {String} optViewID optional ViewID of this instance.
    	 *                  Note that while all views must have a view ID,
    	 *                  they are usually assigned an ID at the point where
    	 *                  a view is added as a subview to another view
    	 * via {@link #addSubView}, {@link #embedView}, etc.
    	 */
    	this.konstructor = function( optName, optViewID, optDisable )
    	{
    		// init instance variables
    	    if (optName) this.name = optName;

    		this.disabled         = optDisable || false;
    		this.visible          = true;
    		this.model            = null;	//default (but not necessarily only) model
    		this.subviews	      = new MVCMap( this.name+" subviews" );
    		this.parentView       = null;
    		this.embeddedSubViews = false;	//if true subviews are built via buildHTMLstr()
    		this.lastBuildContext = null;
    		this.setViewID( optViewID );
    	}

    	/** return an HTML string of the basic structure for this view.
    	 * As a side effect, build/update subview list for this view.
    	 * As a side effect, invoke this method on each nested subview.
    	 * @return HTML (suitable for assigning to innerHTML)
    	 * @type String
    	 */
    	this.buildHTMLstr = function(){ return ""; /*override me*/ }

    	/** update/modify attributes of basic existing HTML for THIS view */
    	this.paintHTML    = function(){ /*override me*/ }

    	/** return true IFF model(s) require the HTML rebuilt for THIS view
    	 * default implementation: If there is a difference in the current
    	 * "build context" and the current one then rebuild else not.
    	 * @type boolean
    	 */
    	this.mustRebuild  = function(){ return this.contextChanged(); }

    	/** return true IFF model(s) require HTML repaint.
    	 * NOTE: rebuilding view forces repaint of all subviews.
    	 * NOTE: repainting THIS view does NOT force repaint of subviews.
    	 * @type boolean
    	 */
    	this.mustRepaint  = function(){ return true; /*override me if needed*/ }

    	/** return the current "context" of this view.
    	 * Default is "no rebuild ever needed" (unless others force us to)
    	 * @type MVCContext
    	 */
    	this.currentContext = function(){ return new MVCContext( 0 ); /*override me*/ }

    	/** register the given model as our primary data model
    	 * @param {MVCModel} m the data model to "watch"
    	 */
    	this.watchModel = function(m ){ this.model  = m;    }

    	/** (re)define the view ID for this view */
    	this.setViewID  = function(ID){ this.viewID = ID;
    	                                 this.hook   = null;
    	                                 this.widget = null; }

    	/** return the "inner" view ID for this view @type String */
    	this.innerID    = function(){ return this.viewID + ".inner"; }

    	/** Return the effective ID of the widget for this view.
    	 * Finesse the fact that the viewID is sometimes
    	 * the ID of a wrapper tag (e.g. span) and other
    	 * times is the ID of the actual view's tag.
    	 * We assume that views that are "embedded" have
    	 * no wrapper tag, otherwise they do. In any event,
    	 * the MVCView subclass can say what the "inner" ID
    	 * is when there is a wrapper.
    	 * @type String
    	 */
    	this.getWidgetID = function( optAttributeID )
    	{
    		var noWrapper = this.parentView && this.parentView.embeddedSubViews;
    		var id        = noWrapper ? this.viewID : this.innerID();
    		return id + (optAttributeID ? "."+optAttributeID : "");
    	}

    	/** return the HTML element of this view's widget tag @type element */
    	this.getWidget = function(forceReload) // cached lazy load
    	{
    		if (this.widget==null || forceReload)
    			this.widget = grvGetHook( this.getWidgetID() );
    		return this.widget;
    	}

    	/** return the HTML element of this view's hook tag @type element */
    	this.getHook = function(forceReload) // cached lazy load
    	{
    		if (this.hook==null || forceReload)
    			this.hook = grvGetHook( this.viewID );
    		return this.hook;
    	}

    	/** Build the HTML for this view. This can be overrided to directly
    	 * build HTML via DOM operations or use this default implementation
    	 * that takes an HTML string from {@link #buildHTMLstr} and puts it
    	 * into the innerHTML of this view's hook HTML element. Note that
    	 * building the HTML for this view implies rebuilding the HTML for
    	 * all "embedded" subviews.
    	 */
    	this.buildHTML = function()
    	{
    		var hook = this.getHook(true);
    		if (hook!=null){
    			if (this.embeddedSubViews) this.clearSubViews();
    			hook.innerHTML = this.buildHTMLstr();
    		}
    	}

    	/** update and save the current {@link MVCContext} for this view */
    	this.updateContext = function(){
    		this.lastBuildContext = this.currentContext();
    	}

    	/** return true iff the view context has changed @type boolean */
    	this.contextChanged = function(){
    		var same = this.currentContext().sameAs( this.lastBuildContext );
    //grvTraceMrk(this.name+" same="+same+" old="+this.currentContext()+" new="+this.lastBuildContext);
    		return ! same;
    	}

    	/** disable this view (and hence all subviews) */
    	this.disable = function( ){ this.disabled = true;  }

    	/** enable this view (thereby enabling all enabled subviews) */
    	this.enable  = function( ){ this.disabled = false; }

    	/** set this view as visible or not and manifest it via the HTML.
    	 * If setting to invisible then we also set all subviews to invisible
    	 * BUT NOT THE OTHER WAY ROUND!
    	 */
    	this.setVisible = function(isVisible)
    	{	//fix problem where tooltips quit working switching to WinXP
    		grvSetElemDisplay( this.getWidget  (), this.visible=isVisible );//TRICKY
    //		grvSetVisibility ( this.getWidgetID(), this.visible=isVisible );//TRICKY
    		if (!isVisible) this.setSubViewsVisible(false);
    	}

    	/** set all subview visiblility */
    	this.setSubViewsVisible = function(v){
    		this.subviews.iterate( function(viewID,aView){aView.setVisible(v)} );
    	}

    	/** return whether this view is visible @type boolean */
    	this.isVisible = function(){ return this.visible; }

    	/** if enabled, force a "draw" of this view (and all enabled subviews) */
    	this.redraw	= function(){ this.draw(true); }

    	/** if enabled, cause entire subview tree to be built/painted as needed */
    	this.draw = function( optForceRebuild )
    	{
    		if (this.disabled) return;
    //grvBreak("draw ["+this.name+"] depth="+MVCView.Depth);
    	    //Recursively (re)build any view in the
    	    //entire subview tree that needs building.
    	    var rebuilt = this.build( optForceRebuild );

    	    //subview tree should be stable now, so,
    	    //recursively (re)paint entire subview tree
    	    this.paint( rebuilt );
    	}

    	/** does this view or any embedded subview need rebuilding? @type boolean */
    	this.rebuildAny = function()
    	{
    		var must = this.mustRebuild();
    		if (!must) //no need to check if we already know we need to build
    		 if (this.embeddedSubViews)
    		  this.subviews.iterate(
    		   function(vID,vw){ if (vw.rebuildAny()) return must = true;/*TRICKY!*/ }
    							 );
    //		if (must) grvTraceMVC(this.name+" says must rebuild");
    		return must;
    	}

    	/** Recursively build this view and entire subview tree.
    	 * @return whether rebuild was done.
    	 * @type boolean
    	 */
    	this.build = function( optForceRebuild )
    	{
    		if (this.disabled) return false;
    //grvBreak("build ["+this.name+"] depth="+MVCView.Depth);
    	    var rebuild = optForceRebuild || this.rebuildAny();
    //grvTraceEvt("BUILD: "+this.name+"[rebuild="+rebuild+"] ID="+this.viewID);
    		if (rebuild) { this.updateContext(); this.buildHTML(); }
    		if (!this.embeddedSubViews) this.buildsubviews( rebuild );
    		return rebuild;
    	}

    	/** Recursively invoke build on entire subview tree.
    	 * @return whether rebuild was done.
    	 * @type boolean
    	 */
    	this.buildsubviews = function( optForceRebuild )
    	{
    	  this.subviews.iterate(
    		function( viewID, aView ){

    		  	aView.setViewID( viewID );
    			aView.build( optForceRebuild );
    		}
    	  );
    	}

    	/** Recursively invoke paint on entire subview tree. */
    	this.paint = function( optForceRepaint )
    	{
    		if (this.disabled) return;
    //grvBreak("paint ["+this.name+"] depth="+MVCView.Depth);
    		if (optForceRepaint || this.mustRepaint()) this.paintHTML();
    		this.subviews.iterate( function( viewID, aView ){ aView.paint(); } );
    	}

    	/////////// CONTAINER INTERFACE //////////////

    	// NOTE: Changing the subviews list does not cause a redraw
    	// so changes will not appear until the next global draw.

    	/** set the parent view of this view */
    	this.setParentView  = function(v){ this.parentView = v;   }

    	/** clear the list of subviews of this view */
    	this.clearSubViews	= function( ){ this.subviews.reset(); }

    	/** add the specified view/ID to our subview list */
    	this.addSubView		= function( viewID, aView )
    	{
    		aView.setViewID( viewID );
    		aView.setParentView( this );
    		this.subviews.addItem( viewID, aView );
    	}

    	/** find the specified view in the tree of subviews @type MVCView*/
    	this.getSubView	= function( viewID )
    	{
    		var theView = this.subviews.getItem( viewID );
    		if (!theView)
    		 this.subviews.iterate(
    		  function( aViewID, aView ){
    		  	var v = aView.getSubView( viewID );
    		  	if (v) {theView = v; return true;}
    		  }
    		 );
    		return theView;
    	}

    	/** delete the specified view from our subview list */
    	this.delSubView = function( viewID ){ this.subviews.delItem( viewID ); }

    	/** add the specified view/ID as an "embedded" subview. Embedded subviews
    	 * are those whose HTML string is embedded in the HTML string of its
    	 * parent view. I.E. When {@link #buildHTMLstr} is called for a view,
    	 * it is expected to return a string containing its HTML and all the
    	 * HTML for its embedded subviews. NOTE: If one subview is embedded
    	 * then ALL subviews of this view must be embedded.
    	 * @return view
    	 * @type MVCView
    	 */
    	this.embedView = function( viewID, view )
    	{
    		this.embeddedSubViews = true;
    		this.addSubView( viewID, view );
    		       view.updateContext();
    		return view;
    	}

    	/** same as {@link #embedView} but return the HTML string of the
    	 * view rather than the View object.
    	 * @return HTML string
    	 * @type String
    	 */
    	this.embedHTML = function( viewID, view ){
    		return this.embedView( viewID, view ).buildHTMLstr();
    	}

    	/** same as {@link #embedHTML} but auto-creates viewID and saves reference
    	 * to view based on specified "attribute" ID.
    	 * @param {MVCView} view the attribute's subview to embed
    	 * @param {String} attrID the identifier of the attribute
    	 * @return HTML string
    	 * @type String
    	 */
    	this.embedAttr = function( attrID, view )
    	{
    		var viewID = this.getWidgetID(attrID);
     	    this[attrID] = view; //squirrel away reference to view
    		return this.embedView( viewID, view ).buildHTMLstr();
    	}

    	// NOTE: DONT! call grvBusy() in block because it causes the screen
    	// to flash on menu selections!?!  It took hours to track it down...
    	// You are warned!

    	/** API to block/unblock view updating (to stop redraw thrashing) */
    	this.block = function(){
    		++MVCView.Depth; //grvBreak("++depth="+MVCView.Depth);
    	}

    	/** API to block/unblock view updating (to stop redraw thrashing) */
    	this.unblock = function(){
    		if (--MVCView.Depth <= 0) { //grvBreak("--depth="+MVCView.Depth);
    			  MVCView.Depth = 0;
    			  MVCView.DrawRoot(); //grvBreak("finished rootdraw depth="+MVCView.Depth);
    		}
    	}
    }

    /** Global Root Container View (initialized to empty container) */
    gMVCRootView = new MVCView( "root view", "fauxRootHook", true );

    Class(MVCListView).Extends(MVCView);
    /**
     * @class This class is a MVCView that expects to subscribe to a
     * {@link MVCListModel} and will invoke {@link #itemHTMLstr} on
     * each member of the list when {@link #buildHTMLstr} is called
     * and {@link #itemPaint} on each member when {@link #paintHTML}
     * is called.
     *<p><pre>
     * Subclasses of MVCListView should define/override:
     * (A) {@link #itemHTMLstr} which creates HTML for specified item
     * (B) {@link #itemPaint} which decorates HTML for specified item
     * and optionally...
     * (C) {@link #headHTMLstr} which creates HTML for a header item
     * (D) {@link #headPaint} which decorates HTML for a header item
     *</pre>
     * @extends MVCView
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCListView()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		this.souper( optName );

    		//each list element view is effectively an implicit embedded view,
    		//hence dont try to build them again via buildsubviews()!
    	    this.embeddedSubViews = true;
    	}

    	/** method that should return HTML string for specified list item
    	 * @param {int} index index into our MVCListModel
    	 * @param {Object} item the actual item from our MVCListModel
    	 * @param {String} itemID the view ID of the corresponding item subview
    	 * @type String
    	 */
    	this.itemHTMLstr = function(index,item,itemID){ /* abstract */ }

    	/** method that should decorate HTML for specified item
    	 * @param {int} index index into our MVCListModel
    	 * @param {Object} item the actual item from our MVCListModel
    	 * @param {String} itemID the view ID of the corresponding item subview
    	 */
    	this.itemPaint   = function(index,item,itemID){ /* abstract */ }

    	/** method that should return HTML string for the header item
    	 * @type String
    	 */
    	this.headHTMLstr = function(){ /* abstract */ return ""; }

    	/** method that should decorate HTML for the header item
    	 */
    	this.headPaint   = function(){ /* abstract */ }

    	/** return the view ID for the item subview specified
    	 * @param {int} index index into our MVCListModel/MVCListView
    	 * @type String
    	 */
    	this.itemViewID	 = function(index){ return this.getWidgetID() + index; }

    	/** invoke {@link #itemPaint} for each item in our list */
    	this.paintHTML   = function()
    	{
    		this.headPaint();
    		var listView = this;
    		this.model.iterate(
    		  function( i, ithItem ){
    		  	listView.itemPaint( i, ithItem, listView.itemViewID(i) );
    		  }
    		);
    	}

    	/** return the combined HTML string built from each {@link #itemHTMLstr}
    	 * @type String
    	 */
    	this.listHTMLstr = function()
    	{
    		var HTML = new Array();
    		HTML.push( this.headHTMLstr() );
    		var listView = this;
    		this.model.iterate(
    		  function( i, ithItem ){
    		  	HTML.push( listView.itemHTMLstr( i, ithItem, listView.itemViewID(i) ) );
    		  }
    		);
    		return HTML.join('');
    	}

    	/** generate container/framework HTML @type String */
    	this.buildHTMLstr = function()
    	{
    		var HTML = new Array();
    		//HTML.push( '<table>' );
    		//HTML.push( '<tbody>' );
    		HTML.push( this.listHTMLstr() );
    		//HTML.push( '</tbody>' );
    		//HTML.push( '</table>' );
    		return HTML.join('');
    	}
    }

    Class(MVCScalarView,["Scalar Model"]).Extends(MVCView);
    /**
     * @class This class produces a view of the specified
     * {@link MVCScalarModel}. It accepts an optional formatter
     * function specification that will transform the raw value
     * of the model into a desired format. This formatter
     * function should accept one parameter (the value) and
     * return a formatted string version of that value.
     * @extends MVCView
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCScalarView()
    {
    	/** @param {MVCScalarModel} xModel the data model to view
    	 *  @param {Function} optFormatFunction optional formatter function
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, optFormatFunction, optName )
    	{
    		this.souper( optName );

    		// init instance variables
    		this.watchModel( xModel );
    		this.formatter = optFormatFunction;
    	}

    	/** return the formatted version of the given value using our formatter
    	 * function (where the raw value is returned if no formatter is registered)
    	 * @type String
    	 */
    	this.formatted    = function(x){ return this.formatter ? this.formatter(x) : x; }

    	/** return the current value of our model formatted with our formatter @type String */
    	this.getValueStr  = function( ){ return this.formatted( this.model.getValue() ); }

    	/** set the value of our display "widget" */
    	this.setDisplay   = function(x){ this.getWidget().innerHTML = x; }

    	/** update the display with the current model value */
    	this.updateView   = function( ){ this.setDisplay( this.getValueStr() ); }

    	this.buildHTMLstr = function( ){ return grvGenHook( this.getWidgetID() ); }
    	this.paintHTML    = function( )
    	{
    		var visible = this.visible;
    		grvSetElemVisibility( this.getWidget(), visible );
    		if (visible) this.updateView();
    	}
    }

    Class(MVCPadScalarView,["Scalar Model"]).Extends(MVCScalarView);
    /**
     * @class This class produces a blank-padded view of the specified
     * {@link MVCScalarModel}. It transforms the raw value
     * of the model into a right-blank-padded-to-min-len format.
     * @extends MVCScalarView
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCPadScalarView()
    {
    	/** @param {MVCScalarModel} xModel the data model to view
    	 *  @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, minLen, optName )
    	{
    		this.souper( xModel, null, optName );
    		this.minLen = minLen;
    	}

    	/** update the display with the current model value */
    	this.updateView = function( ){ this.setDisplay(
    			 grvFormatPaddedStr( this.getValueStr(),this.minLen ) ); }
    }

    ////////////////////////////////////////////
    ////////////// Controllers /////////////////
    ////////////////////////////////////////////

    Class(MVCController).Extends(MVCView);
    /**
     * @class This class acts as the base class for each controller
     * (that supports an underlying MVCView). Subclasses of MVCController
     * should define/override all overrides required by MVCView.
     * @extends MVCView
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCController()
    {
    	/** @param {String} optName optional name of this instance */
    	this.konstructor = function( optName )
    	{
    		this.souper( optName );
    	}
    }

    Class(MVCButtonController,["descriptive text", "event handler"])
    .Extends(MVCController);
    /**
     * @class This class is a Controller that<pre>
     *  (1) watches a {@link MVCBoolModel} from which the "enable" state can
     *      be deduced (actually only uses {@link MVCBoolModel#isTrue}),
     *  (2) manages a button view using the enable state,
     *  (3) defines a "pushed" event.
     *</pre>
     * @extends MVCController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCButtonController()
    {
    	/**
    	 * @param {MVCBoolModel} optEnableModel optional "is enabled" data model
    	 * @param {String} evtHandlerName name of event handler function
    	 * @param {String} optName optional name of this instance
    	 * @param {String} desc tooltip description for this button
    	 */
    	this.konstructor = function( desc, evtHandlerName, optEnableModel, optName )
    	{
    		this.souper( "BUTTON:"+optName );

    		// init instance variables
    		this.altText        = desc;
    		this.eventHandler   = eval( evtHandlerName );//lazy bind
    		if (optEnableModel) this.watchModel( optEnableModel );
    	}

    	/** Return whether this button should be enabled.
    	 * If no enable model was defined then we are always enabled.
    	 * @type boolean
    	 */
    	this.isEnabled = function(){
    		return this.model ? this.model.isTrue() : true;
    	}

    	/** return the alternate text given the current state of this view @type String */
    	this.getAltText = function( enabled ){
    		return (enabled?"":"disabled:")+ this.altText;
    	}

    	/** put the HTML in <a target="_blank"
    	 * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a>
    	 * given the specified enable flag and this controller's state
    	 * <p>ABSTRACT: sub classes of MVCButtonController should override this</p>
    	 */
    	this.canonical = function( enableFlag )
    	{ grvMustOverride("MVCButtonController.canonical"); }

    	this.paintHTML = function(){ this.canonical( this.isEnabled() ); }
    }

    Class(MVCFormButtonController,["button label", "descriptive text", "event handler"])
    .Extends(MVCButtonController);
    /**
     * @class This class is a Button Controller that manages a form button
     * @extends MVCButtonController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCFormButtonController()
    {
    	/**
    	 * @param {String} label label of the button
    	 * @param {MVCBoolModel} optEnableModel optional "is enabled" data model
    	 * @param {String} evtHandlerName name of event handler function
    	 * @param {String} optName optional name of this instance
    	 * @param {String} desc tooltip description for this button
    	 */
    	this.konstructor = function( label, desc, evtHandlerName, optEnableModel, optName )
    	{
    		this.souper( desc, evtHandlerName, optEnableModel, optName );

    		// init instance variables
    		this.label = label;
    	}

    	/** put the HTML in <a target="_blank"
    	 * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a>
    	 * given the specified enable flag and this controller's state
    	 */
    	this.canonical = function( enableFlag )
    	{
    	  var txtBtn = this.getWidget();
    	  if ( enableFlag )
    	  {
    		txtBtn.onclick      = this.eventHandler;
    	    txtBtn.title        = this.getAltText( enableFlag );
    	    txtBtn.style.cursor = "hand";
    	    txtBtn.disabled     = false;
    	  }
    	  else
    	  {
    	  	txtBtn.onclick      = null;
    	    txtBtn.title        = this.getAltText( enableFlag );
    	  	txtBtn.style.cursor = "not-allowed";
    	    txtBtn.disabled     = true;
    	  }
    	}

    	this.buildHTMLstr = function(){
    		return "<input type='button' class='mvcFormBtn' value='"+this.label+"' id='"+this.getWidgetID()+"'></input>";
    	}
    }

    Class(MVCImgButtonController,["button image filename", "descriptive text", "event handler"])
    .Extends(MVCButtonController);
    /**
     * @class This class is a ButtonController that manages an image button
     * @extends MVCButtonController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCImgButtonController()
    {
    	/**
    	 * @param {MVCBoolModel} optEnableModel optional "is enabled" data model
    	 * @param {String} evtHandlerName name of event handler function
    	 * @param {String} optName optional name of this instance
    	 * @param {String} imgFN filename of the (enabled) button image
    	 * @param {String} desc tooltip description for this button
    	 */
    	this.konstructor = function( imgFN, desc, evtHandlerName, optEnableModel, optName )
    	{
    		this.souper( desc, evtHandlerName, optEnableModel, optName );

    		// init instance variables
    		this.imgFilename = imgFN;
    	}

    	/** put the HTML in <a target="_blank"
    	 * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a>
    	 * given the specified enable flag and this controller's state
    	 */
    	this.canonical = function( enableFlag )
    	{
    	  var imgBtn = this.getWidget();
    	  if ( enableFlag )
    	  {
    	  	imgBtn.src          = kMVCImgPath + this.imgFilename;
    		imgBtn.onclick      = this.eventHandler;
    	    imgBtn.alt          = this.getAltText( enableFlag );
    	    imgBtn.style.cursor = "hand";
    	  }
    	  else
    	  {
    	  	imgBtn.src          = kMVCImgPath + kMVCDim_ + this.imgFilename;
    	  	imgBtn.onclick      = null;
    	    imgBtn.alt          = this.getAltText( enableFlag );
    	  	imgBtn.style.cursor = "not-allowed";
    	  }
    	}

    	this.buildHTMLstr = function(){
    		return "<img align='middle' src='"+kMVCImgPath+kMVCImgSpacer
    		        +"' name='"+this.getWidgetID()+"'>";
    	}
    }

    Class(MVCCmdButtonController,["undo/redo flag"])
    .Extends(MVCFormButtonController);
    /**
     * @class This class is a Button Controller for "undo/redo" buttons.
     * @extends MVCFormButtonController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCCmdButtonController()
    {
    	/**
    	 * @param {boolean} isUndo true if this is undo button else redo button
    	 */
    	this.konstructor = function( isUndo )
    	{
    		this.souper(
    			isUndo?"Undo":"Redo", "foo",
    			isUndo?"onMVCUndoBtnPressed":"onMVCRedoBtnPressed", null,
    			isUndo?"undo":"redo"
    		);
    		this.isUndo  = isUndo;
    	}

    	/** return the alternate text for this button
    	 * @param {String} enabled a string with the description of what
    	 * is about to be undone/redone or null if button should not be enabled
    	 * @type String
    	 */
    	this.getAltText = function( enabled ){
    		return (this.isUndo ? "[ctrl-z] UNDO: " : "[ctrl-y] REDO: ")
    		     + (enabled ? enabled : "No more to "+(this.isUndo?"undo":"redo"));
    	}

    	this.isEnabled = function(){
    		return this.isUndo ? gMVCUndoCmds.hasUnDo() : gMVCUndoCmds.hasReDo();
    	}
    }

    Class(MVCCmdImgButtonController,["button image filename", "undo/redo flag"])
    .Extends(MVCImgButtonController);
    /**
     * @class This class is an Image Button Controller for "undo/redo" buttons.
     * @extends MVCImgButtonController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCCmdImgButtonController()
    {
    	/**
    	 * @param {boolean} isUndo true if this is undo button else redo button
    	 * @param {String} imgFN filename of the (enabled) button image
    	 */
    	this.konstructor = function( imgFN, isUndo )
    	{
    		this.souper( imgFN, "foo",
    			 isUndo?"onMVCUndoBtnPressed":"onMVCRedoBtnPressed", null,
    			(isUndo?"undo":"redo")
    		);
    		this.isUndo  = isUndo;
    	}

    	/** return the alternate text for this button
    	 * @param {String} enabled a string with the description of what
    	 * is about to be undone/redone or null if button should not be enabled
    	 * @type String
    	 */
    	this.getAltText = function( enabled ){
    		return (this.isUndo ? "[ctrl-z] UNDO: " : "[ctrl-y] REDO: ")
    		     + (enabled ? enabled : "No more to "+(this.isUndo?"undo":"redo"));
    	}

    	this.isEnabled = function(){
    		return this.isUndo ? gMVCUndoCmds.hasUnDo() : gMVCUndoCmds.hasReDo();
    	}
    }

    Class(MVCTxtButtonController,["button text model", "descriptive text", "event handler"])
    .Extends(MVCButtonController);
    /**
     * @class This class is a Button Controller that manages a button that can
     * display text based on a data model's current value
     * @extends MVCButtonController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCTxtButtonController()
    {
    	/**
    	 * @param {MVCScalarModel} labelModel data model with button label text
    	 * @param {MVCBoolModel} optEnableModel optional "is enabled" data model
    	 * @param {String} evtHandlerName name of event handler function
    	 * @param {String} optName optional name of this instance
    	 * @param {String} desc tooltip description for this button
    	 */
    	this.konstructor = function( labelModel, desc, evtHandlerName, optEnableModel, optName )
    	{
    		this.souper( desc, evtHandlerName, optEnableModel, optName );

    		// init instance variables
    		this.labelModel = labelModel;
    	}

    	/** put the HTML in <a target="_blank"
    	 * href="http://en.wikipedia.org/wiki/Canonical">canonical form</a>
    	 * given the specified enable flag and this controller's state
    	 */
    	this.canonical = function( enableFlag )
    	{
    	  var txtBtn = this.getWidget();
    	  grvSetButtonElemText( txtBtn, this.labelModel.getValue() );
    	  if ( enableFlag )
    	  {
    		txtBtn.onclick      = this.eventHandler;
    	    txtBtn.title        = this.getAltText( enableFlag );
    	    txtBtn.style.cursor = "hand";
    	    txtBtn.disabled     = false;
    	  }
    	  else
    	  {
    	  	txtBtn.onclick      = null;
    	    txtBtn.title        = this.getAltText( enableFlag );
    	  	txtBtn.style.cursor = "not-allowed";
    	    txtBtn.disabled     = true;
    	  }
    	}

    	this.buildHTMLstr = function(){
    		return "<button class='mvcFormBtn' type='button' id='"+this.getWidgetID()+"'></button>";
    	}
    }

    Class(MVCScalarEditController,["Scalar Model"]).Extends(MVCController);
    /**
     * @class This abstract class manages an editor of the specified
     * {@link MVCScalarModel}. This controller edits a single scalar value.
     * @extends MVCController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCScalarEditController()
    {
    	/**
    	 * @param {MVCScalarModel} sModel scalar data model to edit
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {Function} optPostEdit optional post edit value cleanup function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( sModel, optName, optEvtHndlrName, optPostEdit )
    	{
    		this.souper( optName );

    		// init instance variables
    		this.watchModel( sModel );
    		this.evtHndlrName = optEvtHndlrName ? optEvtHndlrName : "onMVCScalarEditUpdate";
    		this.maxchars     = -1;//negative means no limit
    		this.allowCR      = false;//default is "CR means accept value"
    		this.postEditor   = optPostEdit;
    	}

    	/** return the current (post-edit) value of the controller widget */
    	this.postEdit = function()
    	{
    		var theVal = this.getCtrlValue();
    		if (this.postEditor) theVal = this.postEditor(theVal);
    		else if (theVal instanceof String)
    				theVal = grvTrimStr( theVal );//default policy
    		return theVal;
    	}

    	/** set our controller widget to the given value */
    	this.setCtrlValue  = function(x){        this.getWidget().value = x; }

    	/** return the current value of the controller widget */
    	this.getCtrlValue  = function( ){ return this.getWidget().value    ; }

    	/** return a string with full invocation of our event handler @type String */
    	this.getEvtHandlerStr = function(){ return this.evtHndlrName+"('"+this.viewID+"')"; }

    	/** set our display to the given value */
    	this.setDisplay    = function(x){ this.setCtrlValue(x); }

    	/** return the current value of our data model */
    	this.getModelValue = function( ){ return this.model.getValue( ); }

    	/** set our data model to the given value */
    	this.setModelValue = function(x){        this.model.setValue(x); }

    	/** update our display to the current value of our data model */
    	this.updateView    = function( ){ this.setDisplay( this.getModelValue() ); }

    	/** force this controller to the given value */
    	this.forceValue    = function(x){ this.setCtrlValue(x); this.setModels(x); }

    	/** set our (and our parents if we are embedded within a
    	 * {@link MVCDualController}) data model to the given value
    	 */
    	this.setModels     = function(x){
    		this.setModelValue( x );
    		if (this.parentView && this.parentView.setModels)
    			this.parentView.setModels( x );
    	}

    	this.paintHTML     = function( ){
    		grvSetElemVisibility( this.getWidget(), this.visible );
    		if (this.visible) this.updateView();
    	}
    }

    Class(MVCPopupMenuController,["selection model"]).Extends(MVCScalarEditController);
    /**
     * @class This class manages a popup menu which watches
     * a {@link MVCListModel} (specifying the menu items)
     * embedded within a {@link MVCSelectionModel} that reflects
     * which item is/should-be currently selected. [The scalar value
     * we "edit" is the "select index" of the MVCSelectionModel.]
     * Each item in the list should implement the {@link MVCMenuItem} interface.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCPopupMenuController()
    {
    	/**
    	 * @param {MVCSelectionModel} sModel selection data model to control
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {String} optName optional name of this instance
    	 * @param {String} optClassName optional CSS classname to use for formatting
    	 */
    	this.konstructor = function( sModel, optName, optEvtHndlrName, optClassName ){
    	  this.souper( sModel, optName, optEvtHndlrName );
    	  this.className = optClassName ? optClassName : "mvcEntryfield";
    	}

    	this.currentContext = function(){
    		return new MVCContext( this.model.model.timestamp );
    	}

    	this.buildHTMLstr = function()
    	{
    		var L    = this.model.getList();
    		var HTML = new Array( L.getCount()+2 );
    		HTML.push( '<select title="foo" onchange="'+this.getEvtHandlerStr()
    		             +'" class="'+this.className+'" name="'+this.getWidgetID()+'">' );

    	L.iterate( function(key,ithItem){
    		HTML.push( '<option title="bar" value="'+key+'">'
    		             + ithItem.getDescription() +'</option>' );
    	} );

    		HTML.push( '</select>' );
    		return HTML.join('');
    	}
    }

    Class(MVCFieldEditController,["Scalar Model","min chars","max chars","css classname"])
    .Extends(MVCScalarEditController);
    /**
     * @class This class manages a form field editor of the
     * specified {@link MVCScalarModel}.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCFieldEditController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to edit
    	 * @param {int} min minimum number of chars to support (i.e. display size)
    	 * @param {int} max maximum number of chars to support
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {Function} optKeyFilter optional keypress filter function
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {Function} optPostEdit optional post edit value cleanup function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, min, max, className,
    								optKeyFilter, optName, optEvtHndlrName, optPostEdit )
    	{
    		this.souper( xModel, optName, optEvtHndlrName, optPostEdit );

    		// init instance variables
    		this.minchars  = min;
    		this.maxchars  = max;
    		this.className = className;
    		this.keyFilter = optKeyFilter;
    	}

    	/** Produce HTML version of a scalar editor.<p>
    	 * ala {input class='className' name="paylg1" size="min"
    	 *             maxlength="max" value="123456789.01" onchange="dirty('1');"
    	 *             onblur="validateDollar('paylg1')"/}
    	 */
    	this.buildHTMLstr = function()
    	{
    		var keybrdEvent = "onMVCFieldEditKey  ('"+this.viewID+"')";
    		var  focusEvent = "onMVCFieldEditFocus(this)";
    		return '<input onfocus="'+focusEvent
    		+'" onblur="return '+this.getEvtHandlerStr()
    		+'" onkeypress="return '+keybrdEvent
    		+'" NAME="'+this.getWidgetID()
    		+'" class="'+this.className
    		+'" size="'+this.minchars
    		+'" maxlength="'+this.maxchars
    		+'"/>';
    	}
    }

    Class(MVCDateEditController,["Date String Scalar Model","css classname"])
    .Extends(MVCScalarEditController);
    /**
     * @class This class manages a Date form field editor of the
     * specified {@link MVCScalarModel}.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDateEditController()
    {
    	// init "static" Class methods/elements
    	if (grvIsUndefined(MVCDateEditController.LaunchCalendar)){//keep this line above jsdoc comments!

    		/** Static method to launch a calendar window with given ID
    		 * @param {String} HTML ID of target form element
    		 */
    		MVCDateEditController.LaunchCalendar = function( fieldName ) {
    			show_calendar( fieldName, "getElementById(self.opener.gVID)" );
    		}
    	}

    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {Function} optPostEdit optional post edit value cleanup function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, className,
    								optName, optEvtHndlrName, optPostEdit )
    	{
    		this.souper( xModel, optName, optEvtHndlrName, optPostEdit );

    		// init instance variables
    		this.className = className;
    		this.keyFilter = grvDateKeyFilter;
    	}

    	this.calendarID = function(){ return this.getWidgetID()+".cal"; }

    	/** TODO fix bug where being made invisible then visible again
    	 * leaves calendar icon still invisible...probably need to make
    	 * calendar its own Controller
    	 */
    	this.paintHTML = function( ){
    		grvSetElemVisibility( this.getWidget(), this.visible );
    		grvSetVisibility( this.calendarID(), this.visible);
    		if (this.visible) this.updateView();
    	}

    	/** Produce HTML version of a Data string scalar editor. */
    	this.buildHTMLstr = function()
    	{
    		var fieldName   = this.getWidgetID();
    		var keybrdEvent = "onMVCFieldEditKey  ('"+this.viewID+"')";
    		var  focusEvent = "onMVCFieldEditFocus(this)";
    		var calHTML     = '<img alt="Launch calendar window" id="'
    			+this.calendarID()+'" src="'
    			+kMVCImgPath+kMVCImgCalendar
    			+'" onclick="MVCDateEditController.LaunchCalendar(\''+fieldName+'\')" border="0"/>';

    		return calHTML+'<input onfocus="'+focusEvent
    		+'" onblur="return '+this.getEvtHandlerStr()
    		+'" onkeypress="return '+keybrdEvent
    		+'" NAME="'+fieldName
    		+'" class="'+this.className
    		+'" size="10" maxlength="10"/>';
    	}
    }

    Class(MVCTextEditController,["Scalar Model","rows","cols","max chars","css classname"])
    .Extends(MVCScalarEditController);
    /**
     * @class This class manages a form field "textarea" editor of the
     * specified {@link MVCScalarModel}.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCTextEditController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to edit
    	 * @param {int} rows number of rows to support
    	 * @param {int} cols number of cols to support
    	 * @param {int} max maximum number of chars to support
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {Function} optKeyFilter optional keypress filter function
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, rows, cols, max, className,
    								optKeyFilter, optName, optEvtHndlrName )
    	{
    		this.souper( xModel, optName, optEvtHndlrName );

    		// init instance variables
    		this.rows      = rows;
    		this.cols      = cols;
    		this.maxchars  = max;
    		this.allowCR   = true;
    		this.className = className;
    		this.keyFilter = optKeyFilter;
    	}

    	/** Produce HTML version of a textarea editor.<p>
    	 * ala {textarea NAME="comments" cols=40 rows=6}{/textarea}
    	 */
    	this.buildHTMLstr = function()
    	{
    		var keybrdEvent = "onMVCFieldEditKey  ('"+this.viewID+"')";
    		var  focusEvent = "onMVCFieldEditFocus(this)";
    		return '<textarea onfocus="'+focusEvent
    		+'" onblur="return '+this.getEvtHandlerStr()
    		+'" onkeypress="return '+keybrdEvent
    		+'" NAME="'+this.getWidgetID()
    		+'" class="'+this.className
    		+'" rows="'+this.rows
    		+'" cols="'+this.cols
    		+'"/></textarea>';
    	}
    }

    Class(MVCChkBoxEditController,["Scalar Model","css classname"])
    .Extends(MVCScalarEditController);
    /**
     * @class This class manages a form field "checkbox" editor of the
     * specified {@link MVCScalarModel}.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCChkBoxEditController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, className, optName, optEvtHndlrName )
    	{
    		this.souper( xModel, optName, optEvtHndlrName, null );

    		// init instance variables
    		this.className = className;
    	}

    	/** set our controller widget to the given value */
    	this.setCtrlValue  = function(x){        this.getWidget().checked = grvBoolValue(x); }

    	/** return the current value of the controller widget */
    	this.getCtrlValue  = function( ){ return this.getWidget().checked;                }

    	/** Produce HTML version of a checkbox editor.<p>
    	 * ala {INPUT type="checkbox" name="ACTIVE_ONLY" value="Y" readonly="readonly"/}
    	 */
    	this.buildHTMLstr = function()
    	{
    		return '<input type="checkbox"'
    		+'" onblur="return '+this.getEvtHandlerStr()
    		+'" NAME="'+this.getWidgetID()
    		+'" class="'+this.className
    		+'"/>';
    	}
    }

    Class(MVCBoolEditController,["Bool Model","css classname"])
    .Extends(MVCScalarEditController);
    /**
     * @class This class manages a scalar editor with a set of yes,no,unknown
     * radio buttons attached to the specified Boolean (i.e. can be null)
     * {@link MVCBoolModel}.
     * @extends MVCScalarEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCBoolEditController()
    {
    	/**
    	 * @param {MVCBoolModel} xModel scalar data model to edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optEvtHndlrName optional name of event handler function
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, className, optName, optEvtHndlrName )
    	{
    		this.souper( xModel, optName, optEvtHndlrName, null );

    		// init instance variables
    		this.className = className;
    		this.visible   = false;//HACK!!
    		this.lastviz   = true;//HACK!!!
    	}

    	//BUG IN Internet Exploder causes radio buttons to not display
    	//on being made visible after being invisible...
    	//SO, rebuild rather than repaint HACK!!
    	this.mustRebuild  = function()
    	{
    		var becameVisible = this.visible && !this.lastviz;
    //if (becameVisible) grvBreak("must? rebuild= "+becameVisible+" me="+itsMe+" v="+this.visible+" l="+this.lastviz);
    		this.lastviz = this.visible;
    		return becameVisible;
    	}

    	this.btnID        = function(){ return this.getWidgetID() + ".rb"; }
    	this.getButtons   = function(){ return document.getElementsByName( this.btnID() ); }

    	this.setCtrlValue = function(x)
    	{
    		var buttons = this.getButtons();
    		x = x ? x : ""; //null is represented by empty string
    		for (var i=0; i<buttons.length; ++i)
    		  buttons[i].checked = (x==buttons[i].value);
    	}

    	this.getCtrlValue = function()
    	{
    		var buttons = this.getButtons();
    		for (var i=0; i<buttons.length; ++i)
    		  if (buttons[i].checked) return buttons[i].value;
    		return null;
    	}

    	this.radBtnHTMLstr = function( name, evtHndlr, code, desc, optChecked )
    	{
    		return '<input type="radio" value="'+code+'"'
    		+'" onclick="return '+evtHndlr
    		+'" NAME="'+name
    		+'" class="'+this.className
    		+'" STYLE="background-color:transparent"'
    		+ (optChecked ? ' checked="checked"' : '')
    		+'/>'+desc+'&nbsp;&nbsp;<br/>&nbsp;';
    	}

    	this.buildHTMLstr = function()
    	{
    		var wid  = this.getWidgetID();
    		var name = this.btnID();
    		var eh   = this.getEvtHandlerStr();
    		return grvGenHook( wid,
    		       this.radBtnHTMLstr( name, eh, 'Y', 'Yes' )
    			 + this.radBtnHTMLstr( name, eh, 'N', 'No'  )
    			 + this.radBtnHTMLstr( name, eh,  '', 'Unknown' )
    			 			);
    	}

    	this.paintHTML = function()
    	{
    		var widget = this.getWidget();
    		grvSetElemVisibility( widget, this.visible );
    		//BUG IN Internet Exploder causes radio buttons to not display
    		//on being made visible after being invisible...BTW, putting
    		//an alert() here to debug it, makes the problem stop <sigh>
    		if (this.visible) this.updateView();
    		var buttons = this.getButtons();
    		for (var i=0; i<buttons.length; ++i)
    		  grvSetElemVisibility( buttons[i], this.visible );
    	}
    }

    Class(MVCDollarEditController,["Scalar Model"]).Extends(MVCFieldEditController);
    /**
     * @class This class manages an editor of the specified Dollar
     * {@link MVCScalarModel}.
     * @extends MVCFieldEditController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDollarEditController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to edit
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, optName ){
    		this.souper( xModel, 15, 20, "mvcEntryfield", grvDollarKeyFilter, optName );
    	}

    	this.setDisplay   = function(x){
    		this.getWidget().value = parseFloat(x).toFixed(2);
    	}

    	this.getCtrlValue = function(){
    		var s = grvDollarStrFilter( this.getWidget().value );
    		if (       grvIsEmpty(s)) return 0;
    //		if (!grvIsSignedFloat(s)) return 0;
    		return  parseFloat(s);
    	}
    }

    Class(MVCDualController,["scalar model","css classname"]).Extends(MVCController);
    /**
     * @class This abstract class manages dual views (viewer and editor)
     * of a {@link MVCScalarModel}.
     * @extends MVCController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualController()
    {
    	/**
    	 * @param {MVCScalarModel} sModel scalar data model to view/edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( sModel, className, optName )
    	{
    		this.souper( optName );

    		// init instance variables
    		this.watchModel( sModel );
    		this.editing   = false;
    		this.viewer    = null;
    		this.editor    = null;
    		this.className = className;
    		this.mode      = MVCEditRule.kEdit;
    	}

    	this.enableEdit = function( enable ){ this.editing = enable; }
    	this.formatted  = function( value  ){ return this.viewer.formatted(value); }
    	this.setModels  = function( value  ){
    		this       .model._setValue( value );
    		this.editor.model._setValue( value );
    	}

    	this.setEditMode = function( mode, inEdit )
    	{
    		this.mode = mode;
    		if (mode==MVCEditRule.kZero) this.setModels( 0 );
    		this.enableEdit( inEdit && !MVCEditRule.IsReadOnly(mode) );
    	}

    	/** validate data and return null if valid else error msg @type String */
    	this.validator = function() {
    		return MVCEditRule.Validate( this.model, this.mode );
    	}

    	/** update Validity attributes @return errmsg @type String */
    	this.updateValidity = function()
    	{
    		var errMsg = this.validator();
    		this.       model._setValidity( errMsg );
    		this.editor.model._setValidity( errMsg );
    		return errMsg;
    	}

    	this.paintHTML = function()
    	{
    		this.editor.setVisible( this.visible &&  this.editing );
    		this.viewer.setVisible( this.visible && !this.editing );

    		var errMsg = this.updateValidity();
    		var widget = this.editor.getHook();//we want the wrapper HTML element!
    		if (!widget || !widget.parentNode) return;
    		var parent = widget.parentNode;
    		    widget = this.editor.getWidget();
    		if (widget==null){ grvBreak("missing editor widget"); widget = parent; }//HACK!!
    		if (errMsg) {
    		  parent.title     = widget.title     = errMsg;
    //dont style popup menus after all...
    //		  parent.className = widget.className = this.className+'bad';
    		  parent.className                    = this.className+'bad';
    		  if (!(this.editor instanceof MVCPopupMenuController))
    		  	widget.className = parent.className;
    		}
    		else {
    		  parent.title     = widget.title     = parent.parentNode.title;
    //dont style popup menus after all...
    //		  parent.className = widget.className = this.className;
    		  parent.className                    = this.className;
    		  if (!(this.editor instanceof MVCPopupMenuController))
    		  	widget.className = parent.className;
    		}
    	}

    	this.buildHTMLstr = function()
    	{
    		var hookID = this.getWidgetID();
    		var HTML   = new Array();
    	 	HTML.push( this.embedHTML( hookID+".edit", this.editor ) );
    		HTML.push( this.embedHTML( hookID+".view", this.viewer ) );
    		return HTML.join('');
    	}
    }

    Class(MVCDualChkBoxController,["Scalar Model","min chars", "css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified (boolean) Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualChkBoxController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to view/edit
    	 * @param {int} min minimum number of chars to support (i.e. display size)
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, min, className, optName )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		this.viewer = new MVCPadScalarView( xModel, min );
    		this.editor = new MVCChkBoxEditController( xModel, className );
    	}
    }

    Class(MVCDualBoolController,["Bool Model","min chars", "css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified (Boolean i.e. can be null) Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualBoolController()
    {
    	/**
    	 * @param {MVCBoolModel} xModel scalar data model to view/edit
    	 * @param {int} min minimum number of chars to support (i.e. display size)
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, min, className, optName )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		this.viewer = new MVCPadScalarView( xModel, min );
    		this.editor = new MVCBoolEditController( xModel, className );
    	}
    }

    Class(MVCDualDateController,["Date String Model", "css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified Date Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualDateController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to view/edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, className, optName )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		this.viewer = new MVCScalarView( xModel );
    		this.editor = new MVCDateEditController( xModel, className );
    	}
    }

    Class(MVCDualStringController,["Scalar Model","min chars", "max chars", "css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified String Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualStringController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to view/edit
    	 * @param {int} min minimum number of chars to support (i.e. display size)
    	 * @param {int} max maximum number of chars to support
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 * @param {Function} optKeyFilter optional keypress filter function
    	 */
    	this.konstructor = function( xModel, min, max, className, optName, optKeyFilter )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		this.viewer = new MVCPadScalarView( xModel, min );
    		this.editor = new MVCFieldEditController( xModel,min,max,className,optKeyFilter );
    	}
    }

    Class(MVCDualTextController,["Scalar Model","rows","cols","max chars","css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified "Text" Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualTextController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to view/edit
    	 * @param {int} rows number of rows to support
    	 * @param {int} cols number of cols to support
    	 * @param {int} max maximum number of chars to support
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 */
    	this.konstructor = function( xModel, rows, cols, max, className, optName )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		this.viewer = new MVCScalarView( xModel, grvFormatMultilineStr );
    		this.editor = new MVCTextEditController( xModel,rows,cols,max,className );
    	}
    }

    Class(MVCDualDollarController,["Scalar Model","css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and editor) of the
     * specified Dollar Scalar Model.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualDollarController()
    {
    	/**
    	 * @param {MVCScalarModel} xModel scalar data model to view/edit
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 * @param {Function} optFormatFunction optional formatter function
    	 */
    	this.konstructor = function( xModel, className, optFormatFunction, optName )
    	{
    		this.souper( xModel, className, optName );

    		// init instance variables
    		var formatter = grvIsUndefined( optFormatFunction )
    		              ? grvFormatDollarNot
    		              : optFormatFunction;
    		this.viewer = new MVCScalarView( this.model, formatter );
    		this.editor = new MVCDollarEditController( this.model );
    	}

    	this.validator = function() {
    		if (this.mode==MVCEditRule.kZero) this.setModels( 0 );//@todo is this needed??
    		return MVCEditRule.Validate( this.model, this.mode );
    	}
    }

    Class(MVCDualMenuController,["Menu Selection Model","scalar model","css classname"])
    .Extends(MVCDualController);
    /**
     * @class This class manages dual views (viewer and popup menu) of the
     * specified Models.
     * @extends MVCDualController
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCDualMenuController()
    {
    	/**
    	 * @param {MVCSelectionModel} mModel selection data model for menu
    	 * @param {MVCScalarModel} sModel scalar data model to view
    	 * @param {String} className the CSS classname to use for formatting
    	 * @param {String} optName optional name of this instance
    	 * @param {String} optEvtHndlrName optional name of menu event handler function
    	 * @param {String} optInitDesc optional initial description string
    	 */
    	this.konstructor = function( mModel, sModel, className, optName, optEvtHndlrName, optInitDesc )
    	{
    		this.souper( sModel, className, null, optName );

    		// init instance variables
    		this.initialVal  = sModel.getValue();
    		this.initialDesc = mModel.getDescription( sModel.getValue() );
    		if (optInitDesc && !mModel.loaded) this.initialDesc = optInitDesc;

    		//logic changed to suit selection models that are not loaded yet
    		this.viewer = new MVCScalarView( sModel,
    			/*formatter method (of MVCScalarView)*/ function( value )
    			{
    				var s = (value==this.parentView.initialVal)
    				      ? this.parentView.initialDesc
    				      : this.parentView.editor.model.getDescription(value)

    				return (gGrvTraceTODO ? value+":" : "") + s;
    			}
    		);

    		this.editor = new MVCPopupMenuController( mModel, "menu for "+optName, optEvtHndlrName, className );

    		// default data validation rule
    		this.setEditMode( MVCEditRule.kNonZ, false );
    	}

    	this.setModels = function( value ){ this.model._setValue( value ); }
    }

    ////////////////////////////////////////////
    //////////////// Commands //////////////////
    ////////////////////////////////////////////

    // ----------------------------------------------------------------------------
    // Utility routines for making AJAX requests
    // ----------------------------------------------------------------------------

    Class(MVCAJAXReplyCmd,["xml request object","xsl DOM"]).Extends(MVCCommand);
    /**
     * @class This class implements the "process AJAX reply" command.
     * NOTE: It isnt undoable (because we lose all edits!).
     * @extends MVCCommand
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCAJAXReplyCmd()
    {
    	/** @param {XMLreq} xmlReq  the XML request being replied to
    	 *  @param {ActiveXObject} xslDOM  xsl DOM doc to transform reply XML
    	 *  @param {String} optName  optional name of this instance
    	 */
    	this.konstructor = function( xmlReq, xslDOM, optName )
    	{
    		this.souper( optName );
    		this.canUndo = false;

    		//translate received XML into javascript (via XSL) and save
    		try { this.script = xmlReq.xform2( xslDOM ); }
    		catch(e) { this.state = -1; }
    	}

    	this.doit = function() {
    		if (gGrvTraceEvt) grvDebugWindow( this.script );
    		eval( this.script );
    	}
    }

    Class(MVCScalarEditCmd,["view ID"]).Extends(MVCCommand);
    /**
     * @class This class implements a scalar edit command.
     *<p>
     * This code should work with either standalone
     * scalar edit controllers, or, with scalar edit
     * controllers that are embedded within "dual"
     * controllers that contain editor and viewer
     * child controllers and therefore may have data
     * model(s) at the "dual" level to update that are
     * separate from the edit controller data model.
     * @extends MVCCommand
     * @see #konstructor
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function MVCScalarEditCmd()
    {
    	/** @param {String} viewID ID of controller generating this command */
    	this.konstructor = function( viewID )
    	{
    		this.souper("edit");

    		this.viewID   = viewID;
    		this.editCtrl = gMVCRootView.getSubView( viewID );
    		if ( this.editCtrl )
    		{
    			this.dual     = (this.editCtrl.parentView
    				          && this.editCtrl.parentView instanceof MVCDualController)
    						  ?  this.editCtrl.parentView
    						  :  null;

    //			this.newValue = this.editCtrl.getCtrlValue();
    			this.oldValue = this.editCtrl.getModelValue();
    			this.newValue = this.editCtrl.postEdit();

    			if (this.oldValue != this.newValue)
    			{
    				this.oldDesc = this.getFormattedValue( this.oldValue );
    				this.newDesc = this.getFormattedValue( this.newValue );
    			}
    			else { this.state = -1; this.editCtrl.updateView(); }
    		}
    		else { this.state = -1; grvError("missing "+viewID); }
    	}

    	/** return the formatted version of the given value using controller's formatter */
    	this.getFormattedValue = function( value )
    	{
    		var x = this.dual ? this.dual.formatted(value) : value;
    		return x=="&nbsp;" ? 0 : x; //Extreme Hack!!
    	}

    	/** stuff the given value into our controller's view */
    	this.updateController = function(value){ this.editCtrl.forceValue( value ); }

    	/** set our controller to the new value */
    	this.doit     = function(){ this.updateController( this.newValue ); }

    	/** set our controller to the old value */
    	this.undo     = function(){ this.updateController( this.oldValue ); }

    	this.details  = function(){ return this.dual.model.name
    	                                 + " from '" + this.oldDesc
    								     +  "' to '" + this.newDesc
    								     +  "'";
    							  }
    }

    ////////////////////////////////////////////
    ////////// Static Event Handlers ///////////
    ////////////////////////////////////////////

    /** undo-button-pressed event handler
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCUndoBtnPressed(){ gMVCUndoCmds.unDo(); }

    /** redo-button-pressed event handler
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCRedoBtnPressed(){ gMVCUndoCmds.reDo(); }

    /** pre-screen all keypress events for entire page
     * This implementation handles ctrl-z and ctrl-y
     * to invoke Undo and Redo respectively.
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCGlobalKeyPress()
    {
        if ( event.keyCode==26 ) { onMVCUndoBtnPressed(); return true; }
        if ( event.keyCode==25 ) { onMVCRedoBtnPressed(); return true; }
    }

    /** ScalarEditController update-event handler
     * @param {String} viewID view ID of the {@link MVCScalarEditController}
     * generating this event.
     * @return event success?? flag
     * @type boolean
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCScalarEditUpdate( viewID )
    {
    	mvcDoCmd( new MVCScalarEditCmd( viewID ) );
    	return true;
    }

    /** handle "key pressed" events in text fields
     * @param {String} viewID view ID of the {@link MVCFieldEditController}
     * generating this event.
     * @return false if this key should be suppressed
     * @type boolean
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCFieldEditKey( viewID )
    {
    	// browser handles ctrl-z ["undo"] in a text field.

    	var SEC = gMVCRootView.getSubView( viewID );

    	// If "enter" key pressed, cause update event [indirectly via blur]
    	var char    = event.keyCode;
    	var isEnter = (char==13 || char==3);
    	if (isEnter && !SEC.allowCR) {
    		event.srcElement.blur();
    		event.srcElement.select();
    		return false;
    	}

    	// otherwise validate value length
    	// (and key if user specified a keyFilter).
    	var currentValue = SEC.getWidget().value;
    	if (SEC.maxchars>=0 && currentValue.length>SEC.maxchars)
    		return false;

    	// keyFilters should return the KEYCODE of the canonical new char
    	// or null if the current char was not legal
    	if (SEC.keyFilter)
    	{
    		char = SEC.keyFilter( char, currentValue );
    		if (char!=null)
    		{
    			 event.keyCode = char;
    			 return true;
    		}
    		else return false;
    	}

    	return true;
    }

    /**
     * handle "entering a text field" event per <a target="_blank"
     * href="http://www.webreference.com/js/tips/000805.html">webreference tip</a>)
     * @param {Element} field HTML element of text field generating this event
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function onMVCFieldEditFocus( field )
    {
    //	field.focus();
    //	field.blur();
    	field.select();
    }

    ////////////////////////////////////////////
    //////////// Utility Functions /////////////
    ////////////////////////////////////////////

    /**
     * Create and embed, as a subview, a {@link MVCScalarView} of the specified
     * data model attribute.
     * @param {MVCView} parentView the view to embed the new view into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view
     * @param {Function} optFormatter optional formatter function
     * @param {Object} optParam optional parameter to pass to model method
     * if the attribute actually refers to a method rather than a data element
     * @return the HTML string of the parentView (that includes the new embedded subview)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedAttributeViewer( parentView, attribute, optFormatter, optParam )
    {
    	var model = new MVCROAttributeModel( parentView.model, attribute, optParam );
     	var view  = new MVCScalarView( model, optFormatter );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualDollarController} of
     * the specified data model attribute which is expected to be a dollar
     * amount data element.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {String} className the CSS classname to use for formatting
     * @param {Function} optFormatFunction optional formatter function
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedDollarDualEditor( parentView, attribute, className, optFormatFunction )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualDollarController( model, className, optFormatFunction );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualChkBoxController} of
     * the specified data model attribute which is expected to be a (boolean)
     * data element.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {int} min minimum number of chars to support (i.e. display size)
     * @param {String} className the CSS classname to use for formatting
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedChkBoxDualEditor( parentView, attribute, min, className )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualChkBoxController( model, min, className );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualBoolController} of
     * the specified data model attribute which is expected to be a (boolean)
     * data element.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {int} min minimum number of chars to support (i.e. display size)
     * @param {String} className the CSS classname to use for formatting
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedBoolDualEditor( parentView, attribute, min, className )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualBoolController( model, min, className );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualStringController} of
     * the specified data model attribute which is expected to be a String
     * data element.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {int} min minimum number of chars to support (i.e. display size)
     * @param {int} max maximum number of chars to support
     * @param {String} className the CSS classname to use for formatting
     * @param {Function} optKeyFilter optional keypress filter function
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedStringDualEditor( parentView, attribute, min, max, className, optKeyFilter )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualStringController( model, min, max, className, "string dual", optKeyFilter );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualTextController} of
     * the specified data model attribute which is expected to be a String
     * data element.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {int} rows number of rows to support
     * @param {int} cols number of cols to support
     * @param {int} max maximum number of chars to support
     * @param {String} className the CSS classname to use for formatting
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedTextDualEditor( parentView, attribute, rows, cols, max, className )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualTextController( model, rows, cols, max, className );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualDateController} of
     * the specified data model attribute which is expected to be a String
     * data element containing a Date.
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     * parent view's primary data model to view/edit
     * @param {String} className the CSS classname to use for formatting
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedDateDualEditor( parentView, attribute, className )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualDateController( model, className, "date dual" );
     	return parentView.embedAttr( attribute, view );
    }

    /**
     * Create and embed, as a subview, a {@link MVCDualMenuController} of
     * the specified data model attribute
     * @param {MVCView} parentView the view to embed the new controller into
     * @param {String} attribute the identifier of the attribute of the
     *                 parent view's primary data model to view/edit
     * @param {MVCSelectionModel} menuModel data model for the popup menu
     * @param {String} className the CSS classname to use for formatting
     * @param {String} optEvtHndlrName optional edit event handler (default
     *                   is to launch a {@link MVCScalarEditCmd} command).
     * @param {String} optInitDesc optional initial description string
     * @return the HTML string of the parentView (that includes the new embedded subviews)
     * @type String
     * @author Bruce Wallace  (PolyGlotInc.com)
     * @version 2.0
     */
    function mvcEmbedDualMenu( parentView, attribute, menuModel, className, optEvtHndlrName, optInitDesc )
    {
    	var model = new MVCAttributeModel( parentView.model, attribute );
     	var view  = new MVCDualMenuController(menuModel,model,className,"menu dual",optEvtHndlrName,optInitDesc);
     	return parentView.embedAttr( attribute, view );
    }

Overview | File | Class | Tree | Index | Help
| The Gravey Framework and RATS RIA
PREV NEXT | FRAMES NO FRAMES

Documentation generated by JSDoc on Sun Feb 11 13:31:25 2007