///////////////////////////////////////////////////////////////////////////
// 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.
 *
 * @author       Bruce Wallace  (PolyGlotInc.com)
 * @requires     grvUtils.js
 * @requires     grvValidate.js
 * @requires     grvClass.js
 * @version      2.5
 */

//////////////////////////////////////////////////////////////////
// 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"
//////////////////////////////////////////////////////////////////

grvTraceCmp("grvMVC.js: Begin");

// 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.1
 */
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.subscribers = new Array();
		this.inXaction   = false;
		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" flag to true */
	this.dirty = function(){
		this.hasChanged = true;
		this.updateStamp();
	}

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

	/** tell observers that we have changed 
	 * @param {Object} optAdhocObj optional adhoc object to pass to observers
	 */
	this.publish = function( optAdhocObj )
	{
    	this.dirty();
    	if (!this.inXaction) 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(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()
	{
		grvASSERT(!this.inXaction,"already started transaction");
//		gMVCRootView.block();
		this.inXaction = true;
	}

	/** publish a "batch" of updates */
	this.EndTransaction = function()
	{
		grvASSERT(this.inXaction,"Not in transaction!");
		this.inXaction = false;
		this.publish();
//		gMVCRootView.unblock();
	}

	/** {@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; }

	/** clear list and publish */
	this.reset      = function( ){ this._reset(); this.publish(); 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; }

	/** 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(); }
	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();}
	
	/** 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(){
		return ! this.currentContext().sameAs( this.lastBuildContext );
	}

	/** 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.5
 */
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.readonly     = false;
	    txtBtn.className    = 'mvcFormBtn';
	  }
	  else
	  {
	  	txtBtn.onclick      = null;
	    txtBtn.title        = this.getAltText( enableFlag );
	    txtBtn.readonly     = true;
	    txtBtn.className    = 'mvcFormBtn mvcFormBtnRO';
	  }
	}

	this.buildHTMLstr = function(){
		return "<input type='button' 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.5
 */
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.title        = this.getAltText( enableFlag );
	    imgBtn.style.cursor = "pointer";
	  }
	  else
	  {
	  	imgBtn.src          = kMVCImgPath + kMVCDim_ + this.imgFilename;
	  	imgBtn.onclick      = null;
	    imgBtn.title        = this.getAltText( enableFlag );
	  	imgBtn.style.cursor = "not-allowed";
	  }
	}

	this.buildHTMLstr = function(){
		return "<img align='middle' src='"+kMVCImgPath+this.imgFilename//kMVCImgSpacer
		        +"' id='"+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.5
 */
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 = "pointer";
	    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.5
 */
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()
	{//alert("in "+this.name+".postEdit()");
		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+"(event,'"+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.5
 */
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 : "mvcPopupMenu";
	}

	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 onchange="'+this.getEvtHandlerStr()
		             +'" class="'+this.className
		             +'" name="' +this.getWidgetID()
		             +'" id="'   +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"])
.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.5
 */
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} optClassName optional 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, optClassName, 
								optKeyFilter, optName, optEvtHndlrName, optPostEdit )
	{
		this.souper( xModel, optName, optEvtHndlrName, optPostEdit );

		// init instance variables
		this.minchars  = min;
		this.maxchars  = max;
		this.className = optClassName ? optClassName : "mvcEntryField";
		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 keybrdEvtHandlerStr = "onMVCFieldEditKey  (event,'"+this.viewID+"')";
		var  focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)";
		return '<input onfocus="'+focusEvtHandlerStr
		+'" onblur="return ' + this.getEvtHandlerStr()
		+'" onkeypress="return '+keybrdEvtHandlerStr
		+'" name="'     +this.getWidgetID()
		+'" id="'       +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.5
 */
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 keybrdEvtHandlerStr = "onMVCFieldEditKey  (event,'"+this.viewID+"')";
		var  focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)";
		var calHTML             = '<img alt="Launch calendar window" id="'
			+this.calendarID()+'" src="'
			+kMVCImgPath+kMVCImgCalendar
			+'" onclick="MVCDateEditController.LaunchCalendar(\''+fieldName+'\')" border="0"/>';

		return calHTML
		+'<input onfocus="'     +   focusEvtHandlerStr
		+'" onblur="return '    +this.getEvtHandlerStr()
		+'" onkeypress="return '+  keybrdEvtHandlerStr
		+'" name="' +fieldName
		+'" id="'   +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.5
 */
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 keybrdEvtHandlerStr = "onMVCFieldEditKey  (event,'"+this.viewID+"')";
		var  focusEvtHandlerStr = "onMVCFieldEditFocus(event,this)";
		return '<textarea onfocus="'+focusEvtHandlerStr
		+'" onblur="return '     +this.getEvtHandlerStr()
		+'" onkeypress="return ' +  keybrdEvtHandlerStr
		+'" name="' +this.getWidgetID()
		+'" id="'   +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()
	{
		//hack to fix safari/chrome onblur bug: send onchange events to the same handler
		return '<input type="checkbox"'
		+'" onchange="return '+this.getEvtHandlerStr()
		+'" onblur="return '  +this.getEvtHandlerStr()
		+'" name="' +this.getWidgetID()
		+'" id="'   +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, evtHndlrStr, code, desc, optChecked )
	{
		return '<input type="radio" value="'+code+'"'
		+'" onclick="return '+evtHndlrStr
		+'" 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 ehs  = this.getEvtHandlerStr();
		return grvGenHook( wid,
		       this.radBtnHTMLstr( name, ehs, 'Y', 'Yes' )
			 + this.radBtnHTMLstr( name, ehs, 'N', 'No'  )
			 + this.radBtnHTMLstr( name, ehs,  '', '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 {grvXMLreq} 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.
 * @param {event} e browser event
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function onMVCGlobalKeyPress(e)
{
	e = e || window.event;
	var code = e.keyCode || e.which;
    if ( code==26 || code==122 && e.ctrlKey) { onMVCUndoBtnPressed(); return true; }
    if ( code==25 || code==121 && e.ctrlKey) { onMVCRedoBtnPressed(); return true; }
}

/** ScalarEditController update-event handler
 * @param {event} e browser event
 * @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.5
 */
function onMVCScalarEditUpdate( e, viewID )
{
	mvcDoCmd( new MVCScalarEditCmd( viewID ) );
	return true;
}

/** handle "key pressed" events in text fields
 * @param {event} e browser event
 * @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.5
 */
function onMVCFieldEditKey( e, viewID )
{
	e = e || window.event;
	var code = e.keyCode || e.which;
	var targ = e.target  || e.srcElement;

	if (targ.nodeType == 3) // defeat Safari bug
		targ = targ.parentNode;
	// 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 isEnter = (code==13 || code==3);
	if (isEnter && !SEC.allowCR) {
		targ.blur();
		targ.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)
	{
		code = SEC.keyFilter( code, currentValue );
		if (code!=null)
		{
			 if (e.keyCode) e.keyCode = code; else e.which = code;
			 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 {event} e browser event
 * @param {Element} field HTML element of text field generating this event
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.5
 */
function onMVCFieldEditFocus( e, 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 );
}

grvTraceCmp("grvMVC.js: End");