// This file uses JSDoc-friendly comments [ http://jsdoc.sourceforge.net/ ]

/**
 * @file         grvEDO.js
 * @fileoverview Collection of Editable Domain Object framework classes.
 * This library builds on the MVC framework and adds support for EDOs
 * (Editable Domain Objects) including undo-able select/edit/delete/insert
 * operations.
 *
 * @todo EDO and EDOHolderModel and EDOListModel should be combined
 * into an EDO that supports the Composite Design Pattern. Of course, to
 * do this, all of the MVC "Model" family should be reworked to do the same.
 * The MVC "View" family DOES support Composites but for some reason the
 * Models didnt start out that way. [...sigh...]
 *
 * @author       Bruce Wallace  (PolyGlotInc.com)
 * @requires     grvMVC.js
 * @version      2.0
 */

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


////////////////////////////////////////////
/////////// Global Variables ///////////////
////////////////////////////////////////////

/** Global Server Error Messages Model */
var gEDOServerError = null;

/** Global EDO Selection Model */
var gEDOSelected = null; //new EDOSelectedModel(...);

/** Global EDO Holder Model */
var gEDOHolder = null; //new EDOHolderModel(...);

/** Global EDO Holder Selector Model */
var gEDOHolderSelected = null; //new EDOHolderSelectionModel(...);

/** Initialize EDO Framework global variables.
 * @param {EDOHolderModel} edoHolder wrapper model for set of EDOs
 * @param {Function} edoIndexClass class of index used with EDOHolder 
 * @param {Function} edoHolderViewClass "class" of EDOHolderView to use
 * @param {Function} counterViewClass "class" of EDOCountersView to use
 * @param {PopupMenuController} optMenuCtrlr optional menu override
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOInitialize
( edoHolder, edoIndexClass, edoHolderViewClass, optCounterViewClass, optMenuCtrlr  )
{
	gEDOHolder         = edoHolder;
	gEDOHolderSelected = edoHolder.model;

	// DATA MODEL: which "Editable Domain Object" is Selected
	gEDOSelected = new EDOSelectedModel( gEDOHolder, edoIndexClass );

	// DATA MODEL: Server Error Messages
	gEDOServerError = new MVCScalarModel();  gEDOServerError.setValue("");

	if (gGrvTraceMVC) gEDOHolderSelected.model.dumpSubscribers();
	if (gGrvTraceMVC) gEDOHolderSelected      .dumpSubscribers();
	if (gGrvTraceMVC) gEDOHolder              .dumpSubscribers();
	
	// DATA VIEW: EDO Framework Application Panel View.
	gMVCRootView.addSubView( "EDOAppPanel",
		new EDOAppPanelView( gEDOHolder,
			edoHolderViewClass, optCounterViewClass, optMenuCtrlr ) );
}


////////////////////////////////////////////
//////////// Domain Objects ////////////////
////////////////////////////////////////////

Class(EDOIndex,["index into EDO list"]);
/**
 * @class This simple class encapsulates an EDO index to allow more
 * complex composite indexes to be defined as subclasses.
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOIndex()
{
	/** @param {int} optXI optional "index" of which EDO is selected.
	 * If not specified or if null then this represents "no selection".
	*/
	this.konstructor = function( optXI ){
		this.xi = optXI===undefined ? null : optXI;
	}

	/** return "this" as event handler arg list string @type String */	
	this.asEventArgs = function(){ return this.xi; }

	/** return "this" as a description string @type String */	
	this.asString = function(){ return "EDO["+this.xi+"]"; }

	/** return whether "this" contains a selection @type boolean */
	this.hasSelection = function(){ return this.xi!=null; }

	/** return whether "this" equals given EDOIndex @type boolean */
	this.equals = function(edoIndex){ return this.xi==edoIndex.xi; }
}


Class(EDO, [ "edo Class","parent holder model", "property values array",
			 "edit Date","edit User","unique Key" ]);
/**
 * @class This class defines a framework for Editable Domain Objects.<p>
 * Note that subclasses MUST override the {@link #clone} method.
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDO()
{
	// init "static" Class methods/elements
	if (!EDO.kMsgSep){//keep this line above jsdoc comments!
		 EDO.kMsgSep = "~"; /** message separator character */
	}

	/** 
	 * @param {Function} CLAZZ this EDO's "class"
	 * @param {EDOHolderModel} parentHolder holder model containing this
	 * @param {array}  propValues array of property initial values
	 * @param {String} editDate date of last edit
	 * @param {String} editUser ID of last user to edit
	 * @param {int} theKey Oracle primary key of this record
	 * @param {int} optEditStatus optional edit status where the
	 *  status codes are: 0=no-change; 1=delete; 2=create; 3=hidden; 4=update
	 * @param {String} optName optional name of this instance
	 */
	this.konstructor = function
	(
		CLAZZ,
		parentHolder,
		propValues,
		editDate,
		editUser,
		theKey,
		optEditStatus,
		optName
	){
		// immutable instance variables
		this.clazz = CLAZZ;
		this.rulez = CLAZZ.EditRules ? CLAZZ.EditRules.join('') : null;//backwards compatibility w/RATS

		// init fixed instance variables
		this.holder= parentHolder;
		this.date  = editDate;
		this.whom  = editUser;
		this.key   = theKey;
		if (optName) this.name = optName;

		//0=no-change; 1=delete; 2=create; 3=hidden; 4=update
		this.editStatus = optEditStatus?optEditStatus:0;

		//display expanded/collapsed
		this.expanded = true;

		// define variable instance variables...
		// each of these has a "validity" attribute associated with it
		var propNames = CLAZZ.Properties;
		for (var i=0; i<propNames.length; ++i)
		  this[ propNames[i] ] = propValues ? propValues[i] : "";
	}

	/** return a clone of "this" object. 	 
	 * <p>ABSTRACT: sub classes of EDO should override this.</p>
	 *  @type EDO
	 */
	this.clone = function()
	{
		grvMustOverride("EDO.clone");
/*		return new <<your subclass name goes here>>(
		 this.propValues(), this.date, this.whom, this.key, this.editStatus );
*/	}

	/** push into given array the editable property values URI encoded
	 * @return resultant array
	 * @type Array
	 */
	this.pushEncodedValues = function( pArray )
	{
		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i)
		  pArray.push( encodeURIComponent(this[ propz[i] ]) );
		return pArray;
	}
	/** push into given array the editable property values
	 * @return resultant array
	 * @type Array
	 */
	this.pushValues = function( pArray )
	{
		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i)
		  pArray.push( this[ propz[i] ] );
		return pArray;
	}

	/** convert all property edit rules into "read-only" */
	this.makeReadOnly = function() {
		var N = this.rulez.length;
		this.rulez = "";
		for (var i=0; i<N; ++i)
		  this.rulez += EditRule.kRO;
	}
	
	/** generate array of editable property values @return value array @type Array */
	this.propValues = function() {
		return this.pushValues( new Array() );
	}

	/** return "this" as human-readable string @type String */
	this.toString = function() {
		return this.propValues().join("-");
	}

	/** return the DEFAULT tooltip for "this" @type String
	 * @param {EDOIndex} xi edo index
	 */
	this.edoToolTip = function( xi ){
		return xi.asString()+" \nState:"+this.state()+" Key="+this.key;
	}

	/** return the tooltip for "this" @type String
	 * @param {EDOIndex} xi edo index
	 */
	this.toolTip = function( xi ){
		return this.edoToolTip( xi );
	}

	/** return whether this EDO has validated data @type boolean */
	this.isValid = function()
	{
		//if I am deleted then I am not invalid.
		if ( !this.isActive() ) return true;

		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i)
		  if ( this.getValidity( propz[i] ) )
		    return false;

		return true;
	}

	/** return the debug details of "this" @type String */
	this.dump = function()
	{
		var		dStr = new Array();
				dStr.push( this.clazz.UpdateID+">>>[" );
				dStr.push( "date="+this.date );
				dStr.push( "whom="+this.whom );
				dStr.push( "key ="+this.key  );
				dStr.push( "edit="+this.editStatus );

		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i){
				var prop = propz[i];
				dStr.push( prop +"="+ this[prop] );
		}
				dStr.push( "]<<<"+this.name );
		return	dStr.join("\n");
	}

	/** return an UpdateItem Message (as a POST argument) 
	 *  for (just) this particular EDO formatted as:<pre>
	 * "UPDATES={class}~{key}~{action}~{content}~END_UPDATE"
	 * where the fields are defined as follows...
	 *     {class}  = the entity's type
	 *     {key}    = the entity's unique key number
	 *     {action} = the entity's action code
	 * (encode as strings 'C','D', and 'U' for values create, delete, and update.)
	 *     {content}= the EDO's specific content in EDO-specific-format
	 *</pre>
	 * @type String
	 */
	this.buildUpdateItemPost = function()
	{
		var uFields = new Array();
			uFields.push( "UPDATES=" + this.clazz.UpdateID   );
			uFields.push( this.key ? this.key : "0" );
			uFields.push( this.action()             );
			   this.pushUpdateFields( uFields );
			uFields.push( "END_UPDATE"              );
	 return uFields.join( EDO.kMsgSep );
	}

	/** push into given array all EDO-specific fields of the UpdateItem message.
	 * This default implementation generates content formatted as<pre>
	 * "{property1}~{property2}~...~{propertyN}~"
	 * where the fields are defined as follows...
	 *     {propertyI} = the i-th editable property of this EDO
	 *</pre>
	 * @param {Array} uFields  string array containing Update Msg fields
	 */
	this.pushUpdateFields = function( uFields ) {
		this.pushEncodedValues( uFields );
	}

	/** push into given array all AJAX SAVE Post params for this model
	 * @param {Array} posts  string array containing POST params
	 */
	this.pushPosts = function( posts ) {
		if (this.inEdit()) posts.push( this.buildUpdateItemPost() );
	}

	/** return the edit/validation rule for the specified property.
	 * If no property is specified then return entire set of rules
	 * for this EDO. Each Rule is one character long.
	 * @see MVCDualStringController
	 * @type String
	 * @param {String} optPropID optional property ID
	 */
	this.getEditRule = function( optPropID )
	{
		//if no rules specified then anything goes
		if (!this.rulez) return MVCEditRule.kEdit;

		//if optional property ID not specified then return all rules
		if (grvIsEmpty(optPropID)) return this.rulez;

		//optional property ID was specified so return specific rule
		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i)
		  if (optPropID==propz[i]) return this.rulez.charAt(i);

		throw "bad Property ID["+optPropID+"] in getEditRule()";
	}

	/** set the validity code of the specified attribute
	 * @param {String} ID repayment attribute name
	 * @param {String} v validity "code" (actually an error message or null)
	 */
	this.setValidity = function(ID,v){ this[ID+MVCScalarModel.kValiditySuffix] = v; }
	
	/** return the validity "code" of the specified attribute
	 * @param {String} ID repayment attribute name
	 * @type String
	 */
	this.getValidity = function(ID){ return this[ID+MVCScalarModel.kValiditySuffix]; }

	/** Is it legal to delete "this"?
	 * @return errmsg or null
	 * @type String
	 */
	this.canDelete = function(){ 
		if ( this.isActive() ) return null;
		return "This is already deleted.";
	}

	/** mark this EDO as deleted */
	this.deleteMe = function(){
		this.preDeleteExpand = this.expanded;
		this.preDeleteStatus = this.editStatus;
		this.editStatus      = this.editStatus==2 ? 3 : 1;
		this.collapse();
	}
	
	/** restore this EDO to its pre-deleted state */
	this.undeleteMe = function(){
		this.editStatus = this.preDeleteStatus;
		if (this.preDeleteExpand) this.expand();
		delete this.preDeleteStatus;
		delete this.preDeleteExpand;
	}

	/** Is it legal to create a new one of "this/these"?
	 * @param {boolean} appendFlag if true then append else insert
	 * @return errmsg or null
	 * @type String
	 */
	this.canCreate = function(appendFlag){ return null; }//override me

	/** If created, can we undo creation? @type boolean
	 * @param {boolean} appendFlag if true then append else insert
	 */
	this.canUncreate = function(appendFlag){ return true; }//override me

	/** mark this entity as being newly added */
	this.addMe       = function(){ this.editStatus = 2; }
	
	/** is this entity newly added? @type boolean */
	this.isNew       = function(){ return this.editStatus==2; }
	/** is this entity newly added? @type boolean */
	this.isDeleted   = function(){ return this.editStatus==1; }


	/** return whether this entity has been changed since last save @type boolean */
	this.inEdit = function(){ return this.editStatus > 0 && this.editStatus!=3; }

	/** Is it legal to edit "this"?
	 * @return errmsg or null
	 * @type String
	 */
	this.canEdit = function(){
		if ( kReadOnly ) return "Not allowed in read-only mode.";
		if ( this.isActive() ) return null;
		return "This is deleted.";
	}

	/** mark this entity as having been edited  */
	this.editMe = function()
	{
		this.preExpand  = this.expanded;
		this.preEdit    = this.clone();//save previous state
		this.editStatus = this.editStatus==2 ? 2 : 4;
		this.expanded   = true;//alway expand on edit
		this.whom       = kUserID;
		this.date       = grvGetTodayAsMMDDYYYY();
	}

	/** restore this EDO to its pre-edited state */
	this.uneditMe = function()
	{
		var propz = this.clazz.Properties;
		for (var i=0; i<propz.length; ++i){
		  var prop = propz[i];
		  this[ prop ] = this.preEdit[ prop ];
		}

		this.date       = this.preEdit.date;
		this.whom       = this.preEdit.whom;
		this.key        = this.preEdit.key;
		this.editStatus = this.preEdit.editStatus;
		this.expanded   = this.preExpand;
		delete this.preEdit;
	}

	/** force this EDO to be collapsed */
	this.collapse = function(){ this.expanded=false; }

	/** force this EDO to be expanded */
	this.expand = function(){ this.expanded=true; }

	/** return whether this EDO is collapsed or open
	 * @return true if open
	 * @type boolean
	 */
	this.isExpanded = function(){
		return this.expanded;
	}
	/** can this EDO toggle the "expanded" flag? @type boolean
	 * @param {boolean} doAllFlag if true, all EDOs are to be set
	 */
	this.canToggleExpand = function( doAllFlag ) {
		return this.isActive(); /* override as needed */
	}
	/** toggle (or set) the "expanded" flag of this EDO.
	 * @param {boolean} optValue if specified, set flag to it, else toggle
	 */
	this.toggleExpand = function( optValue )
	{
		if ( this.canToggleExpand() )
		  this.expanded = (optValue===undefined||optValue==null)
					    ? (!this.expanded)
					    : optValue;
	}

	/** Is this EDO active and editable? (i.e not deleted or in limbo)
	 * @return true if active
	 * @type boolean
	 */
	this.isActive = function(){ return this.editStatus!=1 && this.editStatus!=3; }
	
	/** Is this EDO undead? (i.e in limbo)
	 * @return true if in Limbo
	 * @type boolean
	 */
	this.inLimbo = function(){ return this.editStatus==3; }
	
	/** return whether this EDO can be viewed if desired @type boolean */
//	this.isViewable = function(){ return this.isActive(); }
	
	/** return whether this EDO can be viewed if desired @type boolean */
	this.isViewable = function(){ return this.isActive() || this.inEdit(); }

	/** return the formatted version of our edit state @type String */
	this.state = function()
	{
		switch( this.editStatus )
		{
			case 0: return "Unchanged";
			case 1: return "Deleted";
			case 2: return "Added";
			case 3: return "Limbo";
			case 4: return "Updated";
		}
		return "unknown state:["+this.editStatus+"]";
	}

	/** return our edit state translated to a broadcast message code @type String */
	this.action = function()
	{
		switch( this.editStatus )
		{
			case 1: return "D";
			case 2: return "C";
			case 4: return "U";
		}
		return "";
	}
}

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

Class(EDOListModel,["EDO Class"]).Extends(MVCListModel);
/**
 * @class This class encapsulates a list model for EDOs.
 * @extends MVCListModel
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOListModel()
{
	/** @param {Function} edoClass the class of the EDO
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( edoClass, optName )
	{
		this.souper( optName );

		// init instance variables
		this.itemType = edoClass;
	}

	/** return the count of viewable EDOs associated with "this" @type int */
	this.getVisibleCount = function()
	{
		var count = 0;
		this.iterate( function(i,edo){ if (edo.isViewable()) ++count; } );
		return count;
	}

	/** return the count of active EDOs associated with "this" @type int */
	this.getActiveCount = function()
	{
		var count = 0;
		this.iterate( function(i,edo){ if (edo.isActive()) ++count; } );
		return count;
	}

	/** return whether any EDOs are changed for this list @type boolean */
	this.isDirty = function(){
		var changed = false;
		this.iterate( function(i,edo){ if (edo.inEdit()) return changed = true; } );
		return changed;
	}

	/** return whether all EDOs are valid for this model @type boolean */
	this.isValid = function(){
		var valid = true;
		this.iterate( function(i,edo){ if (!edo.isValid()){valid = false; return true;} } );
		return valid;
	}

	/** push into given array all AJAX SAVE Post params for this model
	 * @param {Array} posts  string array containing POST params
	 */
	this.pushPosts = function( posts ) {
		this.iterate( function(i,edo){ edo.pushPosts(posts); } );
	}

	/** toggle the "expanded" flag of the EDO.
	 * @param {boolean} newVal value to set all of the EDO flags to
	 */
	this.toggleExpandAll = function( newVal ) {
		this.iterate( function(i,edo){ edo.toggleExpand(newVal); } );
	}

}


Class(EDOHolderModel,["Holder Selection Model","Load XSL DOM","Entity Name"])
.Extends(MVCModel);
/**
 * @class This abstract class encapsulates the functionality of
 * a data model managing a set of editable domain objects (EDOs).
 * <ul>The following methods MUST be overridden
 * <li>{@link #isDirty}</li>
 * <li>{@link #isValid}</li>
 * <li>{@link #_resetEDOs}</li>
 * <li>{@link #keysMatch}</li>
 * <li>{@link #propMatch}</li>
 * <li>{@link #hasEDO}</li>
 * <li>{@link #_newEDO}</li>
 * <li>{@link #getEDO}</li>
 * <li>{@link #createEDO}</li>
 * <li>{@link #appendEDO}</li>
 * <li>{@link #pushPosts}</li>
 * <li>{@link #getUpdatedProperty}</li>
 * <li>{@link #createSelectionItem}</li>
 *</ul><p>
 * Second, this model implements the {@link MVCBoolModel#isTrue}
 * interface where the value reflects whether this model is "dirty".
 * @extends MVCModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 * @see EDOIndex
 * @see EDOSelectedModel
 */
function EDOHolderModel()
{
	// init "static" Class methods/elements
	if (!EDOHolderModel.AjaxRequest){//keep this line above jsdoc comments!

		/** Make a REST-style AJAX request to the server.</br>
		 * @param {String}  theType     requested entity name
		 * @param {String}  theKey      unique key of requested entity
		 * @param {Function} replyHandler reply event handler static function
		 * @param {String}  optPostData optional string to pass as POST data
		 * @param {boolean} optWaitFlag optional flag that if true says
		 *                  call synchronously (i.e. wait for reply)
		 */
		EDOHolderModel.AjaxRequest = function( theType, theKey, replyHandler, optPostData, optWaitFlag )
		{
			grvWAIT();
			var url = kXMLPath + theType + theKey + ".xml";
			if (!_kGrvAjaxTestData)
			{
				var requestName = (optPostData?"Update":"GetXML") + theType + "DATA";
				var parms = new Array();
					parms.push( requestName );
					parms.push(    'user=' + grvGetArg("user")    );
					parms.push( 'testing=' + grvGetArg("testing") );
					parms.push( theType.toUpperCase() + '_KEY=' + theKey  );
		// var requestPrefix = "?CONTROLLER=funb.cat.reconcile.controller.";
		// var requestPrefix = "?CONTROLLER=funb.cat.bass.controller.";
		   var requestPrefix = "?action=";
				url = location.pathname + requestPrefix + parms.join("&");
			}

			return new XMLreq( url, replyHandler, optPostData, optWaitFlag );
		}
	}

	/** @param {MVCSelectionModel} holderSelectionModel model of which edoHolderModel is selected
	 *  @param {Object} loadXslDOM XSL DOM that transforms newly loaded XML data into "this"
	 *  @param {String} entityName entity name of this instance
	 */
	this.konstructor = function( holderSelectionModel, loadXslDOM, entityName )
	{
		this.souper( entityName );

		// init instance variables
		this.subscribe( holderSelectionModel );
		this.loadXslDOM = loadXslDOM;
	}

	/** {@link MVCBoolModel} API: return "I am modified" flag @type boolean */
	this.isTrue = function(){ return this.isDirty(); }

	/** return whether any EDOs are changed for this model @type boolean
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.isDirty = function()
	{ grvMustOverride("EDOHolderModel.isDirty"); }

	/** return whether all EDOs are valid for this model @type boolean
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.isValid = function()
	{ grvMustOverride("EDOHolderModel.isValid"); }

	/** clear EDOs
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this._resetEDOs = function()
	{ grvMustOverride("EDOHolderModel._resetEDOs"); }

	/** clear EDOs and update timestamp but dont publish */
	this._reset = function(){
		this._resetEDOs();
		this.updateStamp();
	}

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

	/** return whether there is currently an EDO @type boolean
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.hasEDO = function()
	{ grvMustOverride("EDOHolderModel.hasEDO"); }

	/** return a new blank EDO of the type implied by EDOIndex @type EDO
	 * @param {EDOIndex} xi edo index
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this._newEDO = function(xi)
	{ grvMustOverride("EDOHolderModel._newEDO"); }

	/** return a new blank EDO of the type implied by EDOIndex @type EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.newEDO = function(xi)
	{
	    var edo = this._newEDO(xi);
	    edo.addMe();
	    return edo;
	}

	/** return reference to the specified editable domain object
	 * @param {EDOIndex} xi edo index
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.getEDO = function(xi)
	{ grvMustOverride("EDOHolderModel.getEDO"); }

	/** return the specified EDO with the intent to change its attributes
	 * @type EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.openEDO = function(xi){
	    this.publish();
	    return this.getEDO(xi);
	}

	/** return whether the specified EDO is collapsed or open
	 * @param {EDOIndex} xi edo index
	 * @return true if open
	 * @type boolean
	 */
	this.isExpandedEDO = function(xi){
		return this.getEDO(xi).isExpanded();
	}
	/** can we toggle the "expanded" flag of the specified EDO? @type boolean
	 * @param {boolean} doAllFlag if true, set all of the EDOs to new value
	 * @param {EDOIndex} xi edo index
	 */
	this.canToggleExpand = function( doAllFlag, xi ) {
		// OVERRIDE TO implement doAllFlag
		return this.getEDO(xi).canToggleExpand();
	}
	/** toggle the "expanded" flag of the specified EDO.
	 * @param {boolean} doAllFlag if true, set all of the EDOs to new value
	 * @param {EDOIndex} xi edo index
	 */
	this.toggleExpand = function( doAllFlag, xi )
	{
		// OVERRIDE TO implement doAllFlag
		this.getEDO(xi).toggleExpand();
		this.updateStamp();
	}

	/** Is it legal to delete selected EDO?
	 * @return errmsg or null
	 * @type String
	 * @param {EDOIndex} xi edo index
	 */
	this.canDeleteEDO = function(xi){
		return this.getEDO(xi).canDelete();
	}
	/** Is it legal to edit selected EDO?
	 * @return errmsg or null
	 * @type String
	 * @param {EDOIndex} xi edo index
	 */
	this.canEditEDO = function(xi){
		return this.getEDO(xi).canEdit();
	}
	/** Is it legal to create a new EDO?
	 * @return errmsg or null
	 * @type String
	 * @param {EDOIndex} xi edo index
	 * @param {boolean} appendFlag if true then append else insert
	 */
	this.canCreateEDO = function(xi,appendFlag){
		var edo = this.getEDO(xi);
		return edo ? edo.canCreate(appendFlag) : null;
	}
	/** If created, can we undo creation? @type boolean
	 * @param {EDOIndex} xi edo index
	 * @param {boolean} appendFlag if true then append else insert
	 */
	this.canUncreateEDO = function(xi,appendFlag){
		var edo = this.getEDO(xi);
		return edo ? edo.canUncreate(appendFlag) : true;
	}

	/** delete the specified EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.deleteEDO = function(xi){
		this.openEDO(xi).deleteMe();
	}

	/** undelete the specified EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.undeleteEDO = function(xi){
		this.openEDO(xi).undeleteMe();
	}

	/** enable editing on the specified EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.editEDO = function(xi){
		this.openEDO(xi).editMe();
	}

	/** rollback edit on the specified EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.uneditEDO = function(xi){
		this.openEDO(xi).uneditMe();
	}

	/** create new EDO which is inserted after selected EDO
	 * @return EDOIndex of new EDO
	 * @type EDOIndex
	 * @param {EDOIndex} xi edo index
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.createEDO = function(xi)
	{ grvMustOverride("EDOHolderModel.createEDO"); }

	/** create new EDO which is appended at end of EDO list
	 * @return EDOIndex of new EDO
	 * @type EDOIndex
	 * @param {EDOIndex} xi edo index
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.appendEDO = function(xi)
	{ grvMustOverride("EDOHolderModel.appendEDO"); }


	/** create a new item for the selection list from current EDO
	 * @type EDOHolderSelectionItem
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.createSelectionItem = function()
	{ grvMustOverride("EDOHolderModel.createSelectionItem"); }

	/** return the updated properties from the current EDO needed for select list update
	 * @return arbitrary value as needed @type Object
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.getUpdatedProperty = function()
	{ grvMustOverride("EDOHolderModel.getUpdatedProperty"); }


	/** handle CRUD event: "just created EDOHolder" */
	this.justCreated = function(){
		this.model.createSelection( this.createSelectionItem() );
	}
	/** handle CRUD event: "just updated EDOHolder" */
	this.justUpdated = function() {
		this.model.updateSelection( this.getUpdatedProperty() );
	}
	/** handle CRUD event: "just deleted EDOHolder" */
	this.justDeleted = function() {
		this.model.deleteSelection( true );
	}
	/** handle CRUD event: "just loaded EDOHolder" */
	this.justRead = function(){ /* do nothing special */ }

	/** return whether current EDO matches selection item "key"
	 * @type boolean
	 * @param {EDOHolderSelectionItem} item selection menu item
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.keysMatch = function(item)
	{ grvMustOverride("EDOHolderModel.keysMatch"); }

	/** return whether current EDO matches selection item "properties"
	 * @type boolean
	 * @param {EDOHolderSelectionItem} item selection menu item
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.propMatch = function(item)
	{ grvMustOverride("EDOHolderModel.propMatch"); }

	/** return the type of CRUD event that caused EDOHolder reload.
	 * @type String
	 * @return type of CRUD event encoded as char 'C' or 'R' or 'U' or 'D'
	 */
	this.getCRUDtype = function()
	{
		var listEmpty   = (this.model.getCount()<1);
		var hasSelection= listEmpty ? false : this.model.hasSelection();
		
		if (this.hasEDO())
		{
			if (listEmpty) return 'C';
			grvASSERT(hasSelection,"menu list exists with no selection");
			var selection = this.model.getSelection();
			var keysMatch = this.keysMatch(selection);
			var propMatch = this.propMatch(selection);
			if (keysMatch) return propMatch ? 'R' : 'U';
			grvASSERT(!propMatch,"EDO was saved with duplicate Properties");
			return 'C';
		}
		else
		{
			if (listEmpty) return 'R';
			grvASSERT(hasSelection,"menu list exists with no selection");
			return 'D';
		}
	}

	/** handle "EDOHolder reloaded" event. */
	this.reloaded = function()
	{
		if (gGrvTraceEvt) grvDebugWindow( this.dump() );//look at the updated data

		// HACK: this normally should have been accomplished via
		// subscriptions, but it would have set up a circular dependency.
		switch( this.getCRUDtype() )
		{
			case 'C': this.justCreated(); break;
			case 'R': this.justRead();    break;
			case 'U': this.justUpdated(); break;
			case 'D': this.justDeleted(); break;
			default: grvError("bad CRUD code");
		}

//		grvUNWAIT();
	}

	/** Make a Load-Data request to the AJAX server.
	 * @param {Object} edoHolderSelected selected item in observed model
	 */
	this.requestLoad = function( edoHolderSelected ){
		//Note: nothing selected when a new EDO is needed
		if (edoHolderSelected)
		  EDOHolderModel.AjaxRequest( this.name, edoHolderSelected.key, onEDOHolderLoadReply );
	}

	/** Make a Save-Data request to the AJAX server.
	 * @param {Object} edoHolderSelected selected item in observed model
	 * @param {String} updates string of EDO update POSTs
	 */
	this.requestSave = function( edoHolderSelected, updates ){
		//Note: nothing selected when saving new EDO
		var theKey = edoHolderSelected ? edoHolderSelected.key : null;
		EDOHolderModel.AjaxRequest( this.name, theKey, onEDOHolderSaveReply, updates );
	}

	/** push into given array all AJAX SAVE Post params for this model
	 * @param {Array} posts  string array containing POST params
	 * <p>ABSTRACT: sub classes of EDOHolderModel should override this</p>
	 */
	this.pushPosts = function( posts )
	{ grvMustOverride("EDOHolderModel.pushPosts"); }

	/** handle update events from the EDOHolder Selection Model */
	this.update = function()
	{
		// clear out old data now so that user doesnt keep seeing
		// old data while new data is being requested and loaded
		this.reset();

		// TODO make gMVCUndoCmds watch gEDOHolderSelected
		gMVCUndoCmds.reset();//must reset here because cmds in queue
						  //can refer to EDOs that dont exist
						  //at this point because of this.reset()

		// request EDOHolder data from server (after small delay)
		this.requestLoad( this.model.getSelection() );
	}

	/** save all of the edits into the database via service layer */
	this.save = function()
	{
		var             postArgs = new Array();
		                postArgs.push( "USER_ID=" + kUserID );
		this.pushPosts( postArgs );

		// immediate invocation of AJAX request with asynch reply handling
		this.requestSave( this.model.getSelection(), postArgs.join('&') );
	}
}


Class(EDOSelectedModel,["EDO Holder model","EDO Index Class"])
.Extends(MVCBoolModel);
/**
 * @class This class encapsulates the data model for the flag
 * indicating whether an editable domain object (EDO) is selected
 * from a set of EDOs in an EDOHolderModel. One and only one
 * EDO can be selected at a time.  This model also keeps track
 * of which EDO is selected.
 * If a new EDOHolder is selected (thus loading new data)
 * "this" gets reset to false (i.e nothing selected).
 * @extends MVCBoolModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOSelectedModel()
{
	/** @param {EDOHolderModel} edoHolder model containing EDO set
	 *  @param {Function} edoIndexClass the class/function of the EDOIndex to use
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( edoHolderModel, edoIndexClass, optName )
	{
		this.souper( optName ? optName : "EDOSelected Flag" );

		// init instance variables
		this.subscribe( edoHolderModel.model );
		this.edoHolder = edoHolderModel;
		this.xiClass   = edoIndexClass;
		this._unselect();
	}

	/** return whether an EDO is selected @type boolean */
	this.hasSelection = function(){ return this.xi.hasSelection(); }

	/** return the selected EDO or null if none selected @type EDO */
	this.getSelected = function(){
		if ( !this.hasSelection() ) return null;
		return this.edoHolder.getEDO( this.xi  );
	}
	/** return the specified EDO (or current selection if not specified) @type EDO
	 * @param {EDOIndex} optXI optional edo index
	 */
	this.getEDO = function(optXI) {
		return optXI ? this.edoHolder.getEDO(optXI) : this.getSelected();
	}

	/** return the selected EDOIndex @type EDOIndex */
	this.getIndex = function(){ return this.xi; }

	/** force the "something is selected" flag to specified value and publish */
	this.forceFlag = function(f){ this.setFlag(f); }

	/** change to "nothing selected" */
	this._unselect = function(){ this._select( new this.xiClass() ); }

	/** {@link #_unselect} and publish */
	this.unselect = function(){ this._unselect(); this.publish(); }

	/** handle update events from the EDOHolder Selection Model */
	this.update = function(){ /*if (this.model.hasChanged)*/ this.unselect(); }

	/** select the specified EDO
	 * @param {EDOIndex} edoIndex edo index
	 */
	this._select = function( edoIndex ) {
		this.xi = edoIndex;
		this._setValue( this.hasSelection() );
	}

	/** select (and publish) the specified EDO
	 * @param {EDOIndex} xi edo index
	 */
	this.select = function(xi){ this._select(xi); this.publish(); }

	/** return whether specified EDO is selected @type boolean
	 * @param {EDOIndex} edoIndex edo index
	 */
	this.isSelected = function( edoIndex ) {
		return this.xi.equals(edoIndex);
	}

	/** Make the specified EDO selected, unless it is
	 * currently selected then merely deselect it.
	 * @param {EDOIndex} xi edo index
	 */
	this.toggleSelect = function(xi)
	{
		if ( this.isSelected(xi) )
			 this.unselect();
		else this.select(xi);
	}

	/** Is it legal to delete selected EDO? @return errmsg or null @type String */
	this.canDeleteSelected = function() {
		return this.edoHolder.canDeleteEDO( this.xi );
	}
	/** Is it legal to edit selected EDO? @return errmsg or null @type String */
	this.canEditSelected = function() {
		return this.canEdit( this.xi );
	}
	/** Is it legal to edit specified EDO? @return errmsg or null @type String
	 * @param {EDOIndex} xi edo index
	 */
	this.canEdit = function(xi) {
		return this.edoHolder.canEditEDO( xi );
	}

	/** Is it legal to create selected EDO? @return errmsg or null @type String
	 *  @param {boolean} appendFlag iff true this is an append else insert
	 *  @param {EDOIndex} optIndex optional index to use
	 */
	this.canCreateSelected = function(appendFlag,optIndex) {
		return this.edoHolder.canCreateEDO( optIndex?optIndex:this.xi, appendFlag );
	}
	/** If created, can we undo creation? @type boolean
	 *  @param {boolean} appendFlag iff true this is an append else insert
	 *  @param {EDOIndex} optIndex optional index to use
	 */
	this.canUncreateSelected = function(appendFlag,optIndex) {
		return this.edoHolder.canUncreateEDO( optIndex?optIndex:this.xi, appendFlag );
	}

	/** delete selected editable domain object */
	this.deleteSelected = function() {
		this.edoHolder.deleteEDO( this.xi );
		this.unselect();
	}

	/** undelete specified editable domain object
	 * @param {EDOIndex} xi edo index
	 */
	this.undelete = function(xi) {
		 this.edoHolder.undeleteEDO(xi);
	}

	/** edit selected editable domain object */
	this.editSelected = function() {
		this.edoHolder.editEDO( this.xi );
	}

	/** return whether specified editable is in edit mode @type boolean
	 * @param {EDOIndex} optXI optional edo index
	 */
	this.inEdit = function(optXI) {
		var selected = this.getEDO(optXI);
		return selected==null ? false : selected.inEdit();
	}

	/** unedit selected editable */
	this.uneditSelected = function() {
		this.edoHolder.uneditEDO( this.xi );
	}

	/** insert new EDO after selected EDO and select it */
	this.newAfterSelected = function() {
		this.select( this.edoHolder.createEDO( this.xi ) );
	}

	/** insert new EDO at end of EDO list and select it
	 * @param {EDOIndex} xi edo index
	 */
	this.appendSelected = function( xi ) {
		this.select( this.edoHolder.appendEDO( xi ) );
	}
}


Class(EDOHolderSelectionItem);
/**
 * @class This class defines the interface expected for items appearing in
 * the list model watched by {@link EDOHolderSelectionModel}.
 * This class implements the {@link MVCMenuItem} interface.
 * @extends GrvObject
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSelectionItem()
{
	/** quietly update the specified attributes
	 * @param {Object} args arbitrary list of args as needed
	 * <p>ABSTRACT: sub classes of EDOHolderSelectionItem should override this</p>
	 */
	this.updateProperties = function( args )
	{ grvMustOverride("EDOHolderSelectionItem.updateProperties"); }

	/** return "this" formatted for menu item @type String
	 * <p>ABSTRACT: sub classes of EDOHolderSelectionItem should override this</p>
	 */
	this.getDescription = function()
	{ grvMustOverride("MenuItem.getDescription"); }
}


Class(EDOHolderSelectionModel,["EDOHolderSelectionItem List Model"])
.Extends(MVCSelectionModel);
/**
 * @class This class encapsulates the data model for
 * "which EDOHolder is currently selected" from list of {@link EDOHolderSelectionItem}.
 * @extends MVCSelectionModel
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSelectionModel()
{
	/** @param {MVCListModel} listModel list of main menu items
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( listModel, optName ){
		 this.souper( listModel, optName );
	}

	/** Select the "initially selected" EDOHolder.
	 *  This is intended to be overridden.
	 * @param {boolean} selectFirst if selection critera has no match then
	 * if selectFirst==true then select the first in list
	 *                      else leave selection alone 
	 */
 	this.selectInitial = function( selectFirst )
 	{
		if (this.model && this.model.getCount()>0)
		{
			if ( selectFirst ) this.select( 0 );
		}
		else this.selectNothing();	//NO DATA IN MENU LIST!!
	}

	/** quietly update the specified attributes of the selected item
	 * @param {Object} args arbitrary list of args as needed
	 */
	this.updateSelection = function( args )
	{
		// we jump thru hoops here to get observers/watchers to notice
		// that the listModel item has changed but not think that
		// "which item is selected" has changed (which would trigger
		// reloading already-loaded EDOHolder data).
		this.getSelection().updateProperties( args );
		this.updateStamp();
//		this.publish(true);
	}

	/** quietly delete the selected item from the parent list
	 * @param {boolean} selectFirst if true select first item in list
	 */
	this.deleteSelection = function( selectFirst ) {
		this.model._del( this.getIndex() );
		if (selectFirst) this.select(0,true);
	}

	/** quietly append the given item into the parent list and select it 
	 * @param {EDOHolderSelectionItem} item list item to append/select
	 */
	this.createSelection = function( item ) {
		// we jump thru hoops here to get observers/watchers to notice
		// that the listModel has changed but not think that
		// "which item is selected" has changed (which would trigger
		// reloading already-loaded EDOHolder data).
		this._select( this.model._push( item ) );
		this.updateStamp();
	}
}


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

Class(EDOListView,["EDO ListModel","EDO View Class"])
.Extends(MVCListView);
/**
 * @class This class produces a view of a List of EDOs.
 * @extends MVCListView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOListView()
{
	/** @param {EDOListModel} edoListModel EDO list we are to observe
	 *  @param {Function} edoViewClass View Class for individual EDOs
	 */
	this.konstructor = function( edoListModel, edoViewClass  )
	{
		this.souper();

		// init instance variables
		this.itemView  = edoViewClass;
		this.watchModel( edoListModel );
	}

	this.currentContext = function() {
		return new MVCContext( this.model ? this.model.getActiveCount() : 0 );
	}

	/** return the EDOIndex appropriate for the given list index and EDO type. 	 
	 * <p>ABSTRACT: sub classes of EDOListView should override this.</p>
	 * @param {int} index index into ListModel
	 * @type EDOIndex
	 */
	this.itemEDOIndex = function( index ) {
		return new EDOIndex(index);
	}

	this.itemHTMLstr = function( index, item, itemID )
	{
		var HTML = new Array();
		var xi   = this.itemEDOIndex( index );
		HTML.push( this.embedHTML( itemID, new this.itemView(item,xi) ) );
		return HTML.join('');
	}

	this.headHTMLstr = function()
	{   if (this.model && this.model.getVisibleCount()>0) return "";
		return EDOView.ShellHTML( this.itemEDOIndex(0), this.model.itemType );
	}
}


Class(EDOView,["EDO object","EDO index object","inner view"])
.Extends(MVCView);
/**
 * @class This abstract class produces a view of an {@link EDO}.
 * Subclasses should override {@link #buildFields}.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOView()
{
	// init "static" Class methods/elements
	if (!EDOView.ShellHTML) {//keep this line above jsdoc comments!

		EDOView.kImgExpanded	= 'expanded.gif';
		EDOView.kImgCollapsed	= 'collapsed.gif';

		/** Static method to generate the shell HTML for given future EDO.
		 * @param {EDOIndex} xi index of EDO
		 * @param {Function} itemClazz "class" of EDO
		 */
		EDOView.ShellHTML = function( xi, EDOclazz )
		{
			if (kReadOnly) return "";
			var title = "Create New " + EDOView.Title( EDOclazz );
			var event = "onEDOAddClick("+ xi.asEventArgs() +")";
			var HTML  = new Array();
			HTML.push( '<tr><td><fieldset class="edoFieldset">' );
			HTML.push( '<span onclick="'+event+'">' );
			HTML.push( '<legend class="edoFieldsetLegend">'+title+'</legend></span>' );
			HTML.push( '&nbsp;' );
			HTML.push( "</fieldset></td></tr>" );
			return HTML.join('');
		}

		EDOView.Title = function( EDOclazz ){
			return EDOclazz.Title ? EDOclazz.Title : EDOclazz.UpdateID;
		}
	}

	/** @param {EDO} edo EDO we are to observe
	 *  @param {EDOIndex} edoIndex composite EDO index
	 */
	this.konstructor = function( edo, edoIndex, innerView )
	{
		this.souper();

		// init instance variables
		this.xi = edoIndex;
		this.watchModel( edo );
		this.inner = innerView;
	}

	/** return the view ID of our clickable panel @type String */
	this.clickID = function(){ return this.getWidgetID() + ".clik"; }
	/** return the view ID of our set of attribute fields @type String */
	this.innerID = function(){ return this.getWidgetID() + ".attr"; }

	/** set the "can edit" status of the specified attribute
	 * @param {boolean} enableEdit if true enable editor else viewer
	 * @param {String} attrID object element name specifying desired attribute
	 */
	this.updateEditMode = function( enableEdit, attrID )
	{
		var dual = this[ attrID ];//originally set in embedAttr()
		if (dual)
		dual.setEditMode( this.model.getEditRule(attrID), enableEdit );
	}

	/**
	 * Select the value of the given attribute into the given select model.
	 * Since global shared menu data models are just temp utility models,
	 * shared by all menu instances, we must manually set each to the
	 * current value of the REAL data model (iff this is the ONE
	 * AND ONLY currently-being-edited EDO). In other words, this
	 * trick of sharing a single data model is possible because we
	 * only allow one menu of this type to be editable at a time.
	 * @param {MVCSelectionModel} selectModel the shared menu model
	 * @param {String} attrID object element name specifying desired attribute
	 */
	this.cascadedMenuSelect = function(selectModel,attrID)
	{
		this[attrID].editor.watchModel( selectModel );
		selectModel._select( this.model[ attrID ], true );

//		selectModel._select( this.model[ attrID ] );
//		selectModel._setValue( this.model[ attrID ] );
//		this[attrID].setModels( this.model[ attrID ] );
	}

	/**
	 * Select the value of the given attribute into the given select model.
	 * Since global shared menu data models are just temp utility models,
	 * shared by all menu instances, we must manually set each to the
	 * current value of the REAL data model (iff this is the ONE
	 * AND ONLY currently-being-edited EDO). In other words, this
	 * trick of sharing a single data model is possible because we
	 * only allow one menu of this type to be editable at a time. 
	 * @param {MVCSelectionModel} selectModel the shared menu model
	 * @param {String} attrID object element name specifying desired attribute
	 */
	this.sharedMenuSelect = function(selectModel,attrID) {
		selectModel._select( this.model[ attrID ], true );
	}

	this.preSelect = function(edo){ return; /*override as needed*/ }

	this.paintHTML = function()
	{
		var widget        = this.getWidget(); if (!widget) {grvError("no["+this.getWidgetID()+"]");return;}
		var edo           = this.model;
		var isSelected    = gEDOSelected.isSelected( this.xi );
		var isDirty       = edo && edo.inEdit();
		var isBeingEdited = isSelected && isDirty;
		var isVisible     = edo && edo.isViewable();
		this.setVisible( isVisible );

		if ( isVisible )
		{
			widget.title = edo.toolTip( this.xi );
			grvSelectElem( widget, isSelected, kReadOnly ? kROUnselectedColor : kUnSelectedColor );

			if (edo.expanded)
			{
				this.setSubViewsVisible(true);
				if (isBeingEdited) this.preSelect();
			}
			else
			{
				this.setSubViewsVisible(false);
				grvSetVisibility( this.innerID(), false );
			}

			//set the edit rule for each attribute
			var propz = edo.clazz.Properties;
			for (var i=0; i<propz.length; ++i)
			  this.updateEditMode( isBeingEdited, propz[i] );

			grvEditElem( grvGetHook(this.clickID()), isSelected, isDirty, !edo.isValid(), kColorDaffodil );
		}
	}

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

	this.fieldWrapper = function( dual,f,newrow,optSpan ){
		var colSpan = optSpan ? (' colspan="'+optSpan+'"') : '';
		var s = '<td'+colSpan+' class="edoInnerTD">'
			  +'<label class="edoFieldlabel">'+f+'</label><br/>'+dual+'&nbsp;</td>';
		return s + (newrow?'</tr><tr class="edoInnerTR">':'');
	}

	this.buildNulField = function( newrow, optSpan ){
		var colSpan = optSpan ? (' colspan="'+optSpan+'"') : '';
		var s = '<td'+colSpan+' class="edoBlankInnerTD"><br/>&nbsp;</td>';
		return s + (newrow?'</tr><tr class="edoBlankInnerTR">':'');
	}

	this.buildTxtField = function( f,rows,cols,max,newrow,optSpan ){
		var dual = mvcEmbedTextDualEditor( this, f, rows, cols, max, 'edoFieldStrVal' );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildYN_Field = function( f,newrow,optSpan ){
		var dual = mvcEmbedBoolDualEditor( this, f, 5, 'edoFieldStrVal' );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildBoxField = function( f,newrow,optSpan ){
		var dual = mvcEmbedChkBoxDualEditor( this, f, 5, 'edoFieldStrVal' );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildStrField = function( f,min,max,newrow,optSpan ){
		var dual = mvcEmbedStringDualEditor( this, f, min, max, 'edoFieldStrVal' );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildPopField = function( f,newrow,listModel,optSpan,optEvtHandlerName,optInitDesc ){
		var dual = mvcEmbedDualMenu( this, f, listModel, 'edoFieldPopVal',optEvtHandlerName,optInitDesc );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildUprField = function( f,min,max,newrow,optSpan ){
		var dual = mvcEmbedStringDualEditor( this, f, min, max, 'edoFieldStrVal', grvUpperKeyFilter );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildDayField = function( f,newrow,optSpan ){
		var dual = mvcEmbedDateDualEditor( this, f, 'edoFieldStrVal' );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildAmtField = function( f,newrow,optSpan ){
		var dual = mvcEmbedDollarDualEditor( this, f, 'edoFieldAmtVal',grvFormatDollar );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	this.buildPctField = function( f,newrow,optSpan ){
		var dual = mvcEmbedDollarDualEditor( this, f, 'edoFieldPctVal',grvFormatPercent );
		return this.fieldWrapper(dual,f,newrow,optSpan);
	}

	/** push into given array the HTML needed for all EDO fields
	 * <p>ABSTRACT: sub classes of EDOView should override this</p>
	 */
	this.buildFields  = function( HTML )
	{ grvMustOverride("EDOView.buildFields"); }


	this.buildToggle = function()
	{
		var img = this.model.isExpanded()
				? "<img align='middle' src='"+kMVCImgPath+EDOView.kImgExpanded+"'>"
				: "<img align='middle' src='"+kMVCImgPath+EDOView.kImgCollapsed+"'>";

		return '<span title="Click to Toggle; Ctrl-Click to toggle all." onclick="'
		+this.buildToggleEvent()+'">'+img+'&nbsp;</span>';
	}

	this.buildTitle = function(){
		var edo   = this.model;
		var title = EDOView.Title( edo.clazz );

		if (!edo.isExpanded()){
			var suffix = edo.toString();
			if ( grvIsEmpty(suffix) ) suffix  = edo.toolTip( this.xi );
			else if (edo.inEdit()) suffix += (" (Status:"+edo.state()+")");
			title += (": "+suffix);
		}

		return '<fieldset class="edoFieldset" id="'+this.getWidgetID()+'">'
			  +'<legend class="edoFieldsetLegend" id="'+this.clickID()+'">'
			  + this.buildToggle()
			  +'<span onclick="'+this.buildClickEvent()+'">'+title+'</span></legend>';
	}

	this.buildClickEvent = function(){
		return "onEDOClick("+ this.xi.asEventArgs() +")";
	}
	this.buildToggleEvent = function(){
		return "onEDOToggle("+ this.xi.asEventArgs() +")";
	}

	this.buildHTMLstr = function()
	{
		var vwID = this.getWidgetID();
		var HTML  = new Array();
		HTML.push( '<tr><td>' );
		HTML.push( this.buildTitle() );
		HTML.push( '<table id="' + this.innerID()+ '" cellspacing=5 class="edoInnerTABLE"><tr class="edoInnerTR">' );
  		if (this.model.isExpanded()) this.buildFields( HTML );
		HTML.push( "</tr></table></fieldset></td></tr>" );
		return HTML.join('');
	}
}


Class(EDOHolderView,["EDOHolderModel"])
.Extends(MVCView);
/**
 * @class This class produces the view of an EDOHolderModel.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderView()
{
	/** @param {EDOHolderModel} edoHolderModel EDOHolderModel we are to observe */
	this.konstructor = function( edoHolderModel )
	{
		this.souper("Main Data Panel View");

		// init instance variables
		this.watchModel( edoHolderModel );
	}

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

	/** push into given array the HTML needed for all EDOs in holder
	 * <p>ABSTRACT: sub classes of EDOHolderView should override this</p>
	 */
	this.buildFields  = function( HTML )
	{ grvMustOverride("EDOHolderView.buildFields"); }

	this.buildHTMLstr = function()
	{
		var vwID = this.getWidgetID();
		var HTML = new Array(30);
		var clas = "edoDatapanel" + (kReadOnly ? "RO" : "");
		HTML.push( '<table class="'+clas+'" cellpadding="5" cellspacing="0" width="100%">' );
  		this.buildFields( HTML );
		HTML.push( '</table>' );
		return HTML.join('');
	}
}

Class(EDOCountersView,["EDOHolder Selection Model"])
.Extends(MVCView);
/**
 * @class This class produces a view of the current selection
 * and total size of a selection model (ie "I of N").
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOCountersView()
{
	/** @param {EDOHolderSelectionModel} sModel selection model */
	this.konstructor = function( sModel )
	{
		this.souper();

		// init instance variables
		this.watchModel( sModel );
	}

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

	this.buildHTMLstr = function(){
		var sModel = this.model;
		var x = sModel.hasSelection() ? (sModel.getIndex()+1) : 0;
		return sModel.name+x+"&nbsp;of&nbsp;"+sModel.getCount();
	}
}


Class(EDOControlPanelView,["EDOHolder data model"])
.Extends(MVCView);
/**
 * @class This class produces the view of the top control panel.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOControlPanelView()
{

	// init "static" Class methods/elements
	if (!EDOControlPanelView.kImgBtnSave){//keep this line above jsdoc comments!

		EDOControlPanelView.kImgBtnSave		= 'save.gif';
		EDOControlPanelView.kImgBtnCancel	= 'cancel.gif';
		EDOControlPanelView.kImgBtnDelete	= 'delete.gif';
		EDOControlPanelView.kImgBtnEdit		= 'edit.gif';
		EDOControlPanelView.kImgBtnExit		= 'exit.gif';
		EDOControlPanelView.kImgBtnInsert	= 'insert.gif';
		EDOControlPanelView.kImgBtnUndo		= 'undo.gif';
		EDOControlPanelView.kImgBtnRedo		= 'redo.gif';
	}

	/** @param {EDOHolderModel} edoHolderModel EDOHolder we are to observe
	 *  @param {Function} optCounterViewClass optional "class" of CountersView to use
	 *  @param {MVCPopupMenuController} optMenuCtrlr optional menu override
	 */
	this.konstructor = function( edoHolderModel, optCounterViewClass, optMenuCtrlr )
	{
		this.souper("Main Control Panel View");

		// init instance variables
		this.watchModel( edoHolderModel );
		this.cntrPanelClass = optCounterViewClass;
		this.optMenuOveride = optMenuCtrlr;
	}

	this.buildHTMLstr = function()
	{
		var vwID = this.getWidgetID();
		var HTML = new Array(30);

		HTML.push(
//		'<table bordercolordark="#eeeeee" bordercolorlight="#000000" bgcolor="#cccccc" valign="MIDDLE" align="CENTER" cellpadding="0" cellspacing="0" border="3" width="100%" class="edoControlPanel"><tr>'
		'<table bordercolordark="#eeeeee" bordercolorlight="#000000" cellpadding="0" cellspacing="0" border="3" class="edoControlPanel"><tr>'
//		'<table border="3" cellpadding="0" cellspacing="0" class="edoControlPanel"><tr>'
		 );

		//SAVE BTNS PANEL
		HTML.push( '<td align="center" nowrap="nowrap">' );

//		HTML.push( this.embedHTML( vwID+".saveBtns",
//						new EDOSaveBtnsView( this.model ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".save",
//				new MVCImgButtonController( EDOControlPanelView.kImgBtnSave,
				new MVCFormButtonController( "Save",
					"Save changes", "onEDOHolderSaveBtnPressed",
					this.model, "Save" ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".cancel",
//				new MVCImgButtonController( EDOControlPanelView.kImgBtnCancel,
				new MVCFormButtonController( "Cancel",
					"Cancel changes", "onEDOHolderCancelBtnPressed",
					this.model, "Cancel" ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".exit",
//				new MVCImgButtonController( EDOControlPanelView.kImgBtnExit,
				new MVCFormButtonController( "Exit",
					"Exit "+kAppName, "onEDOHolderExitBtnPressed",
					null, "Exit" ) ) );
			HTML.push( mvcSPACER() );

		HTML.push( '</td>' );

		//UNDO BTNS PANEL
		HTML.push( '<td align="center" nowrap="nowrap">' );
//		HTML.push( this.embedHTML( vwID+".undoBtns",
//						new EDOUndoBtnsView() ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".undo",
//				new MVCCmdImgButtonController( EDOControlPanelView.kImgBtnUndo, true ) ) );
				new MVCCmdButtonController( true ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".redo",
//				new MVCCmdImgButtonController( EDOControlPanelView.kImgBtnRedo, false ) ) );
				new MVCCmdButtonController( false ) ) );
			HTML.push( mvcSPACER() );

		HTML.push( '</td>' );

		//EDIT BTNS PANEL
		HTML.push( '<td align="center" nowrap="nowrap">' );
//		HTML.push( this.embedHTML( vwID+".editBtns",
//						new EDOEditBtnsView( this.model ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".delete",
//				new EDOImgButtonController( EDOControlPanelView.kImgBtnDelete,
				new EDOButtonController( "Delete",
					"Delete Selected Item", "onEDODeleteBtnPressed",
					gEDOSelected, kReadOnly, "Delete" ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".edit",
//				new EDOImgButtonController( EDOControlPanelView.kImgBtnEdit,
				new EDOButtonController( "Edit",
					"Edit Selected Item", "onEDOEditBtnPressed",
					gEDOSelected, kReadOnly, "Edit", true ) ) );
			HTML.push( mvcSPACER() );
			HTML.push( this.embedHTML( vwID+".insert",
//				new EDOImgButtonController( EDOControlPanelView.kImgBtnInsert,
				new EDOButtonController( "Insert",
					"Insert Item", "onEDOInsertBtnPressed",
					gEDOSelected, kReadOnly, "Insert" ) ) );
			HTML.push( mvcSPACER() );

		HTML.push( '</td>' );

		//TOP MENU PANEL
		HTML.push( '<td width="0px"></td>' );
		HTML.push( '<td align="center" nowrap="nowrap">' );
		HTML.push( this.embedHTML( vwID+".topMenu",
			( this.optMenuOveride ? this.optMenuOveride :
			 new EDOHolderSelectMenuController("top menu") )
		) );
		HTML.push( '</td>' );
		HTML.push( '<td width="0px"></td>' );

		//COUNTERS PANEL
		//sneaky...add a debug controller wrapper
		HTML.push( '<td align="center">' );
		HTML.push( '<span onclick="if (event.ctrlKey) grvBreakpoint()">' );
		HTML.push( mvcSPACER() );
		HTML.push(
		 this.embedHTML( vwID+".counter",
		 	( this.cntrPanelClass ? this.cntrPanelClass :
			new EDOCountersView( this.model.model ) )
		) );
		HTML.push( mvcSPACER() );
		HTML.push( '</span></td>' );

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


Class(EDOAppPanelView,["EDOHolderModel","EDOHolderView Class"])
.Extends(MVCView);
/**
 * @class This class produces the view of the entire application page.
 * @extends MVCView
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOAppPanelView()
{
	/** @param {EDOHolderModel} edoHolderModel EDOHolder we are to observe
	 *  @param {Function} edoHolderViewClass "class" of EDOHolderView to use
	 *  @param {Function} optCounterViewClass optional "class" of EDOCountersView to use
	 *  @param {MVCPopupMenuController} optMenuCtrlr optional menu override
	 */
	this.konstructor = function( edoHolderModel, edoHolderViewClass, optCounterViewClass, optMenuCtrlr )
	{
		this.souper("EDOAppPageView");

		// init instance variables
		this.watchModel( edoHolderModel );
		this.cntrPanelClass = optCounterViewClass;
		this.optMenuOveride = optMenuCtrlr;
		this.dataPanelClass = edoHolderViewClass;
	}

	this.buildHTMLstr = function()
	{
		var vwID = this.getWidgetID();
		var HTML = new Array(100);

//		HTML.push( '<body bgcolor="red" onLoad="onEDOHolderPageLoad()" onbeforeUnload="onEDOHolderPageBeforeUnLoad()" onKeyPress="onMVCGlobalKeyPress()">' );
		HTML.push( '<form method="POST" onsubmit="return false" action="foo">' );//WHY IS THIS NEEDED?

		HTML.push( this.embedHTML( vwID+".ctrl",
			new EDOControlPanelView( this.model, this.cntrPanelClass, this.optMenuOveride ) ) );

/* UNCOMMENT THIS TO SEE HOW UI COMPARES TO STD WINDOW SIZES */
//		HTML.push( mvcREDSPACER600() + "<br/>" + mvcREDSPACER768() );

		HTML.push( '<div style="width:100%; ">' );
		HTML.push( this.embedHTML( vwID+".err",
			new MVCScalarView( gEDOServerError, grvFormatErrorStr, "Server Error View" ) ) );
		HTML.push( '</div>' );

		<!-- DATA PANEL -->
		HTML.push( '<div style="width:100%; "'
		  + 'style="overflow-x:auto; overflow-y:auto; width:100%; height:expression(document.body.clientHeight-80);">' );
		HTML.push( this.embedHTML( vwID+".data",
			new this.dataPanelClass( this.model ) ) );
		HTML.push( '</div>' );
		HTML.push( '</form>' );
//		HTML.push( '</body>' );
		return HTML.join('');
	}
}

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

Class(EDOButtonController,["button image filename", "descriptive text",
						 "event handler","enable model","readonly flag"])
.Extends(MVCFormButtonController);
/**
 * @class This class is a button controller for EDO "edit" buttons
 * which shouldn't be enabled while app is in "read only" mode.
 * @extends MVCFormButtonController
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOButtonController()
{
	/**
	 * @param {String} label label of the button
	 * @param {EDOSelectedModel} enableModel "is enabled" data model
	 * @param {String} evtHandlerName name of event handler function
	 * @param {String} desc tooltip description for this button
	 * @param {boolean} roFlag read-only mode flag
	 * @param {String} optName optional name of this instance
	 * @param {boolean} optIsEdit editor flag says dont enable if already editing
	 */
	this.konstructor = function ( label, desc, evtHandler, enableModel, roFlag, optName, optIsEdit ) {
		this.souper( label, desc, evtHandler, enableModel, optName );
		this.isEdit = optIsEdit;
		this.readOnly = roFlag;
	}

	/** Return whether this button should be enabled.
	 * If in readonly mode then never enable.
	 * If an entity is selected then enable
	 * UNLESS this is the edit button and we are already editing.
	 * @type boolean
	 */
	this.isEnabled = function()
	{
		var enabled = this.readOnly ? false : this.model.isTrue(); //anything selected?
		if (enabled && this.isEdit && this.model.inEdit()) enabled = false;
		return enabled;
	}
}


Class(EDOImgButtonController,["button image filename", "descriptive text",
						 "event handler","enable model","readonly flag"])
.Extends(MVCImgButtonController);
/**
 * @class This class is an MVCImgButtonController for EDO "edit" buttons
 * which shouldnt be enabled while app is in "read only" mode.
 * @extends MVCImgButtonController
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOImgButtonController()
{
	/**
	 * @param {EDOSelectedModel} enableModel "is enabled" data model
	 * @param {String} evtHandlerName name of event handler function
	 * @param {String} imgFN filename of the (enabled) button image
	 * @param {String} desc tooltip description for this button
	 * @param {boolean} roFlag read-only mode flag
	 * @param {String} optName optional name of this instance
	 * @param {boolean} optIsEdit editor flag says dont enable if already editing
	 */
	this.konstructor = function ( imgFN, desc, evtHandler, enableModel, roFlag, optName, optIsEdit ) {
		this.souper( imgFN, desc, evtHandler, enableModel, optName );
		this.isEdit = optIsEdit;
		this.readOnly = roFlag;
	}

	/** Return whether this button should be enabled.
	 * If in readonly mode then never enable.
	 * If an entity is selected then enable
	 * UNLESS this is the edit button and we are already editing.
	 * @type boolean
	 */
	this.isEnabled = function()
	{
		var enabled = this.readOnly ? false : this.model.isTrue(); //anything selected?
		if (enabled && this.isEdit && this.model.inEdit()) enabled = false;
//grvTraceMVC("Btn["+this.altText+"].enabled="+enabled);
		return enabled;
	}
}


Class(EDOHolderSelectMenuController)
.Extends(MVCPopupMenuController);
/**
 * @class This class manages a popup menu controller for
 * the Selection List that chooses the current EDOHolder.
 * @extends MVCPopupMenuController
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSelectMenuController()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ) {
		 this.souper( gEDOHolderSelected, optName );
	}

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

	/** return event handler invocation string specific to this controller @type String */
	this.getEvtHandlerStr = function(){ return "onEDOHolderSelect(this.value)"; }
}

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

Class(EDOSuperMenuCmd,["view ID"]).Extends(MVCScalarEditCmd);
/**
 * @class This abstract class is a base for menu commands that affect other EDO attributes.
 * <ul>The following methods MUST be overridden
 * <li>{@link #updateEDO}</li>
 *</ul><p>
 * @extends MVCScalarEditCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 1.0
 */
function EDOSuperMenuCmd()
{
	/** @param {String} viewID viewID of menu generating this event */
	this.konstructor = function( viewID ) {
		this.souper( viewID );
		this.edo    = this.dual.model.base;
		this.oldEDO = this.edo.clone();
	}

	/** copy values that can change when menu selection is changed
	 * @param {EDO} exemplar where to copy attributes from
	 * @param {boolean} isRedo redo if true else undo
	 */
	this.updateEDO = function( exemplar, isRedo )
	{ grvMustOverride("EDOSuperMenuCmd.updateEDO"); }

	/** initialize EDO attributes if needed. */
	this.prefillEDO = function(){}

	this.updateOtherController = function( attrID, value ) {
		this.dual.parentView[attrID].setModels( value );//HACK!!
	}

	this.doit = function(){
		this.updateController( this.newValue );
		this.prefillEDO();
	}
	this.undo = function(){
		this.newEDO = this.edo.clone();
		this.updateEDO( this.oldEDO, false );
		this.updateController( this.oldValue );
	}
	this.redo = function(){
		this.updateEDO( this.newEDO, true );
		this.updateController( this.newValue );
	}
}


Class(EDOHolderCloseWindowIfSavedCmd).Extends(MVCCommand);
/**
 * @class This class implements the "close this window
 * if all EDOs in the global EDOHolder are saved" command.
 * NOTE: It isnt undoable (because we may close this window!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderCloseWindowIfSavedCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ) {
		this.souper( optName, (gEDOHolder.isDirty()
				   ? "You must Save or Cancel data changes before leaving."
				   : null ) );
		this.canUndo = false;
	}

	this.doit = function() {
		if (history.length) history.go(-1); else window.close();
	}
}


Class(EDOHolderCancelAllChangesCmd).Extends(MVCCommand);
/**
 * @class This class implements the "revert" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderCancelAllChangesCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ) {
		this.souper( optName );
		this.canUndo = false;
		if (!confirm("Are you sure you want to cancel all changes since last Save?"))
		  this.state = -1;
	}
							//causes reload of original data
	this.doit = function(){	gEDOHolderSelected.reselect(); }
}


Class(EDOHolderSaveAllChangesCmd).Extends(MVCCommand);
/**
 * @class This class implements the "save all changes
 * made to EDOs in the global EDOHolder" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSaveAllChangesCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ) {
		this.souper( optName, (gEDOHolder.isValid() ? null
			: "Can't save while any data is invalid." ) );
		this.canUndo = false;
	}

	this.doit = function() {
		gEDOSelected.unselect();
		gEDOHolder.save();//which implies a reload of data
	}
}


Class(EDOHolderSelectCmd,["selected holder Index"]).Extends(MVCCommand);
/**
 * @class This class implements the "select new EDO Holder" command.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSelectCmd()
{
	/** @param {Object} selectedHolderIndex index of EDOHolder
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( selectedHolderIndex, optName )
	{
		this.souper( optName, (gEDOHolder.isTrue()
				   ? "You must Save or Cancel data changes first."
				   : null )  );
		this.theIndex = selectedHolderIndex;
		this.canUndo  = false;
	}

	this.doit = function(){ gEDOHolderSelected.select( this.theIndex ); }
}


Class(EDOHolderSelectInitialCmd).Extends(MVCCommand);
/**
 * @class This class implements the "select initially specified EDOHolder" command.
 * NOTE: It isnt undoable (because we dont need it to be).
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderSelectInitialCmd()
{
	/** @param {String} optName optional name of this instance */
	this.konstructor = function( optName ){
		this.souper( optName );
		this.canUndo = false;
	}

	this.doit = function(){ gEDOHolderSelected.selectInitial(true); }
}


Class(EDOHolderLoadReplyCmd,["xml request object"]).Extends(MVCAJAXReplyCmd);
/**
 * @class This Command processes the reply of the "load EDOHolder data" request.
 * NOTE: It isnt undoable (because we lose all edits!).
 * @extends MVCAJAXReplyCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOHolderLoadReplyCmd()
{
	/** @param {XMLreq} xmlReq the XML request being replied to
	 *  @param {String} optName optional name of this instance
	 */
	this.konstructor = function( xmlReq, optName ) {
		this.souper( xmlReq, gEDOHolder.loadXslDOM, optName );
	}
}

///////////////////////////////////////////////////////////////////////

Class(EDOCmd).Extends(MVCCommand);
/**
 * @class This is the abstract base class for EDO commands.
 * @extends MVCCommand
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOCmd()
{
	/** @param {String} cmdDesc general description of this command
	 *  @param {String} optErrMsg if not null then cancel this cmd
	 */
	this.konstructor = function( cmdDesc, optErrMsg ){
		this.souper( cmdDesc, optErrMsg );
		if (optErrMsg) return;

		//save current EDO selection
		this.cmdX = this.oldX = gEDOSelected.getIndex();
	}

	/** return details portion of command description @type String */
	this.details = function(){ return this.cmdX.asString(); }
}


Class(EDOToggleSelectCmd,["edo index"]).Extends(EDOCmd);
/**
 * @class This class implements the toggle select EDO cmd.
 * @extends EDOCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOToggleSelectCmd()
{
	/** @param {boolean} autoEdit if true then automatically put EDO in edit mode
	 *  @param {EDOIndex} xi index of EDO to toggle selection
	 */
	this.konstructor = function( autoEdit, xi )
	{
		this.souper( "toggle selection"+(autoEdit?" and edit":"") );
		this.cmdX   = xi;
		this.doEdit = autoEdit ? this.canEdit() : false;
	}

	/** @return true iff we can put selection into edit mode @type boolean */
	this.canEdit = function()
	{
		//if we are about to "deselect" EDO then we cant edit
		if ( gEDOSelected.isSelected(this.cmdX) ) return false;

		//if we are not allowed to edit EDO then we cant edit
		var err = gEDOSelected.canEdit(this.cmdX);
		if (err) { alert(err); return false; }

		//if EDO is already in edit mode then we cant (re)edit
		if (gEDOSelected.inEdit(this.cmdX)) return false;

		return true;
	}

	this.doit = function() {
		gEDOSelected.toggleSelect( this.cmdX );
		if (this.doEdit) gEDOSelected.  editSelected();
	}
	this.undo = function() {
		if (this.doEdit) gEDOSelected.uneditSelected();
		gEDOSelected.toggleSelect( this.oldX );
	}
}

Class(EDOToggleExpandCmd,["toggle all flag","edo index"]).Extends(EDOCmd);
/**
 * @class This class implements the toggle EDO display cmd.
 * @extends EDOCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOToggleExpandCmd()
{
	/** @param {boolean} toggleAll if true then toggle all EDOs in EDOHolder
	 *  @param {EDOIndex} xi index of EDO to toggle collapse
	 */
	this.konstructor = function( toggleAll, xi )
	{
	  this.souper("toggle display");
	  if ( gEDOHolder.canToggleExpand( toggleAll, xi ) )
	  {
		   this.all  = toggleAll;
		   this.cmdX = xi;
	  }
	  else this.state = -1;
	}

	this.doit    = function(){ gEDOHolder.toggleExpand( this.all, this.cmdX ); }
	this.undo    = function(){ this.doit(); }
	this.details = function(){ return this.all ? "ALL" : this.cmdX.asString(); }
}


Class(EDOEditCmd).Extends(EDOCmd);
/**
 * @class This class implements the edit EDO cmd.
 * @extends EDOCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOEditCmd()
{
	this.konstructor = function(){
		this.souper("edit selection", gEDOSelected.canEditSelected());
	}
	this.doit = function(){ gEDOSelected.  editSelected(); }
	this.undo = function(){ gEDOSelected.uneditSelected(); }
}


Class(EDODeleteCmd).Extends(EDOCmd);
/**
 * @class This class implements the delete EDO cmd.
 * @extends EDOCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDODeleteCmd()
{
	this.konstructor = function(){
		this.souper("delete selection", gEDOSelected.canDeleteSelected());
	}
	this.doit = function(){
		gEDOSelected.deleteSelected();//leaves nothing selected
	}
	this.undo = function(){
		gEDOSelected.undelete( this.oldX );
		gEDOSelected.select  ( this.oldX );
	}
}


Class(EDOCreateCmd,["append flag"]).Extends(EDOCmd);
/**
 * @class This class implements the append and insert EDO cmds.
 * NOTE: If this is creating a new EDOholder, this command won't
 * be undoable (because we havent made EDO/EDOholder the same
 * via the Composite design pattern!).
 * @extends EDOCmd
 * @see #konstructor
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function EDOCreateCmd()
{
	//Static "Class" constants
	EDOCreateCmd.kAppend = true;
	EDOCreateCmd.kInsert = false;

	/** @param {boolean} appendFlag if true then append else insert EDO
	 *  @param {EDOIndex} optIndex optional index of EDO to insert after
	 */
	this.konstructor = function( appendFlag, optIndex )
	{
		var edoSelected = gEDOSelected;//for future un-globaling
		this.souper((appendFlag?"append":"insert")+" new ",
		  edoSelected.canCreateSelected(appendFlag,optIndex));

		if (kReadOnly) grvError("CreateCmd in read-only mode");
		this.append  = appendFlag;
		this.cmdX    = optIndex ? optIndex : edoSelected.getIndex();
		this.canUndo = edoSelected.canUncreateSelected(appendFlag,this.cmdX);
	}

	this.doit = function()
	{
		//create new EDO
		if (this.append)
			 gEDOSelected.appendSelected( this.cmdX );
		else gEDOSelected.newAfterSelected();
		//save new state
		this.newX = gEDOSelected.getIndex();
	}

	this.undo = function(){
		gEDOSelected.deleteSelected();
		gEDOSelected.select( this.oldX );
	}

	this.redo = function(){
		gEDOSelected.undelete( this.newX );
		gEDOSelected.select  ( this.newX );
	}
}

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


/** handle event for 'click on an EDO Add Button'
 * @param {EDOIndex} xi index of EDO to append after
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOAddClick( xi ){
	mvcDoCmd( new EDOCreateCmd( EDOCreateCmd.kAppend, xi ) );
}

/** handle event for 'click on an EDO'
 * @param {EDOIndex} xi index of EDO clicked
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOClick( xi ){
	mvcDoCmd( new EDOToggleSelectCmd( event.ctrlKey, xi ) );
}

/** handle event for 'toggle an EDO'
 * @param {EDOIndex} xi index of EDO to collapse/expand
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOToggle( xi ){
	mvcDoCmd( new EDOToggleExpandCmd( event.ctrlKey, xi ) );
}

/** handle event for 'delete EDO button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDODeleteBtnPressed(){
	mvcDoCmd( new EDODeleteCmd() );
}

/** handle event for 'edit EDO button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOEditBtnPressed(){
	mvcDoCmd( new EDOEditCmd() );
}

/** handle event for 'insert EDO button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOInsertBtnPressed()
{
// TODO dont insert until fix is found for insert goofing up the
// EDO indexes lodged in all those UNDO/REDO command objects!!!
//	mvcDoCmd( new EDOCreateCmd( EDOCreateCmd.kInsert ) );
	mvcDoCmd( new EDOCreateCmd( EDOCreateCmd.kAppend ) );
}


/** handle event for 'save button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderSaveBtnPressed(){
	mvcDoCmd( new EDOHolderSaveAllChangesCmd() );
}

/** handle event for 'cancel button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderCancelBtnPressed(){
	mvcDoCmd( new EDOHolderCancelAllChangesCmd() );
}

/** handle event for 'exit button pressed'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderExitBtnPressed(){
	mvcDoCmd( new EDOHolderCloseWindowIfSavedCmd() );
}

/** handle event for 'user selects from the select menu
 * which loads a new EDOHolder'
 * @param {int} selectedHolderIndex the new menu-selection
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderSelect( selectedHolderIndex ){
	mvcDoCmd( new EDOHolderSelectCmd( selectedHolderIndex ) );
}

/** handle event for 'reply received from "load EDOHolder data" server-request'
 * @param {XMLreq} xmlReq the XML request being replied to
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderLoadReply( xmlReq ){
	mvcDoCmd( new EDOHolderLoadReplyCmd( xmlReq ) );
}

/** handle event for 'reply received from "save EDOHolder data" server-request'
 * @param {XMLreq} xmlReq the XML request being replied to
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderSaveReply( xmlReq )
{	//since we get back official new version of EDOHolder Data, just
	//do the same thing as asking for data load in the first place!
	mvcDoCmd( new EDOHolderLoadReplyCmd( xmlReq ) );
}

/** handle event for 'entering EDOHolder web page'
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
function onEDOHolderPageLoad()
{
	gMVCRootView.enable();

// HACK! windows get goofed up if we dont suppress
// the "please wait" window on launch...
_gGrvHackStartupInhibitWait = true;
	mvcDoCmd( new EDOHolderSelectInitialCmd() );
_gGrvHackStartupInhibitWait = false;
}

/** handle event for 'about to leave EDOHolder web page';
 * warn if attempting to leave with unsaved changes in the EDOHolder
 * @author Bruce Wallace  (PolyGlotInc.com)
 * @version 2.0
 */
var _gEDOClosingWindow = true;
function onEDOHolderPageBeforeUnLoad()
{	//This cant be done in a Command since this logic must be completed
	//by the time that we return. [A Command may be queued and finished
	//later, and that is out of the callers control.]
	if (_gEDOClosingWindow) //e.g. calendar window launch
	{
	 if ( gEDOHolder.isTrue() )  // i.e. unsaved data
	  event.returnValue = "Edits are NOT saved. Are you sure you want to abandon your changes?";
	}
	else _gEDOClosingWindow = true;
}


