text: define([
  	"dojo/_base/array", // array.filter array.forEach array.indexOf array.some
  	"dojo/aspect", // aspect.after
  	"dojo/_base/declare", // declare
  	"dojo/_base/lang" // lang.hitch
  ], function(array, aspect, declare, lang){
  
  	// module:
  	//		dijit/tree/TreeStoreModel
  
  	return declare("dijit.tree.TreeStoreModel", null, {
  		// summary:
  		//		Implements dijit/Tree/model connecting to a dojo.data store with a single
  		//		root item.  Any methods passed into the constructor will override
  		//		the ones defined here.
  
  		// store: dojo/data/api/Read
  		//		Underlying store
  		store: null,
  
  		// childrenAttrs: String[]
  		//		One or more attribute names (attributes in the dojo.data item) that specify that item's children
  		childrenAttrs: ["children"],
  
  		// newItemIdAttr: String
  		//		Name of attribute in the Object passed to newItem() that specifies the id.
  		//
  		//		If newItemIdAttr is set then it's used when newItem() is called to see if an
  		//		item with the same id already exists, and if so just links to the old item
  		//		(so that the old item ends up with two parents).
  		//
  		//		Setting this to null or "" will make every drop create a new item.
  		newItemIdAttr: "id",
  
  		// labelAttr: String
  		//		If specified, get label for tree node from this attribute, rather
  		//		than by calling store.getLabel()
  		labelAttr: "",
  
  		// root: [readonly] dojo/data/Item
  		//		Pointer to the root item (read only, not a parameter)
  		root: null,
  
  		// query: anything
  		//		Specifies datastore query to return the root item for the tree.
  		//		Must only return a single item.   Alternately can just pass in pointer
  		//		to root item.
  		// example:
  		//	|	{id:'ROOT'}
  		query: null,
  
  		// deferItemLoadingUntilExpand: Boolean
  		//		Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes
  		//		until they are expanded. This allows for lazying loading where only one
  		//		loadItem (and generally one network call, consequently) per expansion
  		//		(rather than one for each child).
  		//		This relies on partial loading of the children items; each children item of a
  		//		fully loaded item should contain the label and info about having children.
  		deferItemLoadingUntilExpand: false,
  
  		constructor: function(/* Object */ args){
  			// summary:
  			//		Passed the arguments listed above (store, etc)
  			// tags:
  			//		private
  
  			lang.mixin(this, args);
  
  			this.connects = [];
  
  			var store = this.store;
  			if(!store.getFeatures()['dojo.data.api.Identity']){
  				throw new Error("dijit.tree.TreeStoreModel: store must support dojo.data.Identity");
  			}
  
  			// if the store supports Notification, subscribe to the notification events
  			if(store.getFeatures()['dojo.data.api.Notification']){
  				this.connects = this.connects.concat([
  					aspect.after(store, "onNew", lang.hitch(this, "onNewItem"), true),
  					aspect.after(store, "onDelete", lang.hitch(this, "onDeleteItem"), true),
  					aspect.after(store, "onSet", lang.hitch(this, "onSetItem"), true)
  				]);
  			}
  		},
  
  		destroy: function(){
  			var h;
  			while(h = this.connects.pop()){ h.remove(); }
  			// TODO: should cancel any in-progress processing of getRoot(), getChildren()
  		},
  
  		// =======================================================================
  		// Methods for traversing hierarchy
  
  		getRoot: function(onItem, onError){
  			// summary:
  			//		Calls onItem with the root item for the tree, possibly a fabricated item.
  			//		Calls onError on error.
  			if(this.root){
  				onItem(this.root);
  			}else{
  				this.store.fetch({
  					query: this.query,
  					onComplete: lang.hitch(this, function(items){
  						if(items.length != 1){
  							throw new Error("dijit.tree.TreeStoreModel: root query returned " + items.length +
  								" items, but must return exactly one");
  						}
  						this.root = items[0];
  						onItem(this.root);
  					}),
  					onError: onError
  				});
  			}
  		},
  
  		mayHaveChildren: function(/*dojo/data/Item*/ item){
  			// summary:
  			//		Tells if an item has or may have children.  Implementing logic here
  			//		avoids showing +/- expando icon for nodes that we know don't have children.
  			//		(For efficiency reasons we may not want to check if an element actually
  			//		has children until user clicks the expando node)
  			return array.some(this.childrenAttrs, function(attr){
  				return this.store.hasAttribute(item, attr);
  			}, this);
  		},
  
  		getChildren: function(/*dojo/data/Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
  			// summary:
  			//		Calls onComplete() with array of child items of given parent item, all loaded.
  
  			var store = this.store;
  			if(!store.isItemLoaded(parentItem)){
  				// The parent is not loaded yet, we must be in deferItemLoadingUntilExpand
  				// mode, so we will load it and just return the children (without loading each
  				// child item)
  				var getChildren = lang.hitch(this, arguments.callee);
  				store.loadItem({
  					item: parentItem,
  					onItem: function(parentItem){
  						getChildren(parentItem, onComplete, onError);
  					},
  					onError: onError
  				});
  				return;
  			}
  			// get children of specified item
  			var childItems = [];
  			for(var i=0; i<this.childrenAttrs.length; i++){
  				var vals = store.getValues(parentItem, this.childrenAttrs[i]);
  				childItems = childItems.concat(vals);
  			}
  
  			// count how many items need to be loaded
  			var _waitCount = 0;
  			if(!this.deferItemLoadingUntilExpand){
  				array.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } });
  			}
  
  			if(_waitCount == 0){
  				// all items are already loaded (or we aren't loading them).  proceed...
  				onComplete(childItems);
  			}else{
  				// still waiting for some or all of the items to load
  				array.forEach(childItems, function(item, idx){
  					if(!store.isItemLoaded(item)){
  						store.loadItem({
  							item: item,
  							onItem: function(item){
  								childItems[idx] = item;
  								if(--_waitCount == 0){
  									// all nodes have been loaded, send them to the tree
  									onComplete(childItems);
  								}
  							},
  							onError: onError
  						});
  					}
  				});
  			}
  		},
  
  		// =======================================================================
  		// Inspecting items
  
  		isItem: function(/* anything */ something){
  			return this.store.isItem(something);	// Boolean
  		},
  
  		fetchItemByIdentity: function(/* object */ keywordArgs){
  			// summary:
  			//		Given the identity of an item, this method returns the item that has
  			//		that identity through the onItem callback.  Conforming implementations
  			//		should return null if there is no item with the given identity.
  			//		Implementations of fetchItemByIdentity() may sometimes return an item
  			//		from a local cache and may sometimes fetch an item from a remote server.
  			// tags:
  			//		extension
  			this.store.fetchItemByIdentity(keywordArgs);
  		},
  
  		getIdentity: function(/* item */ item){
  			return this.store.getIdentity(item);	// Object
  		},
  
  		getLabel: function(/*dojo/data/Item*/ item){
  			// summary:
  			//		Get the label for an item
  			if(this.labelAttr){
  				return this.store.getValue(item,this.labelAttr);	// String
  			}else{
  				return this.store.getLabel(item);	// String
  			}
  		},
  
  		// =======================================================================
  		// Write interface
  
  		newItem: function(/* dijit/tree/dndSource.__Item */ args, /*dojo/data/api/Item*/ parent, /*int?*/ insertIndex){
  			// summary:
  			//		Creates a new item.   See `dojo/data/api/Write` for details on args.
  			//		Used in drag & drop when item from external source dropped onto tree.
  			// description:
  			//		Developers will need to override this method if new items get added
  			//		to parents with multiple children attributes, in order to define which
  			//		children attribute points to the new item.
  
  			var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem;
  
  			if(this.newItemIdAttr && args[this.newItemIdAttr]){
  				// Maybe there's already a corresponding item in the store; if so, reuse it.
  				this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){
  					if(item){
  						// There's already a matching item in store, use it
  						this.pasteItem(item, null, parent, true, insertIndex);
  					}else{
  						// Create new item in the tree, based on the drag source.
  						LnewItem=this.store.newItem(args, pInfo);
  						if(LnewItem && (insertIndex!=undefined)){
  							// Move new item to desired position
  							this.pasteItem(LnewItem, parent, parent, false, insertIndex);
  						}
  					}
  				}});
  			}else{
  				// [as far as we know] there is no id so we must assume this is a new item
  				LnewItem=this.store.newItem(args, pInfo);
  				if(LnewItem && (insertIndex!=undefined)){
  					// Move new item to desired position
  					this.pasteItem(LnewItem, parent, parent, false, insertIndex);
  				}
  			}
  		},
  
  		pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){
  			// summary:
  			//		Move or copy an item from one parent item to another.
  			//		Used in drag & drop
  			var store = this.store,
  				parentAttr = this.childrenAttrs[0];	// name of "children" attr in parent item
  
  			// remove child from source item, and record the attribute that child occurred in
  			if(oldParentItem){
  				array.forEach(this.childrenAttrs, function(attr){
  					if(store.containsValue(oldParentItem, attr, childItem)){
  						if(!bCopy){
  							var values = array.filter(store.getValues(oldParentItem, attr), function(x){
  								return x != childItem;
  							});
  							store.setValues(oldParentItem, attr, values);
  						}
  						parentAttr = attr;
  					}
  				});
  			}
  
  			// modify target item's children attribute to include this item
  			if(newParentItem){
  				if(typeof insertIndex == "number"){
  					// call slice() to avoid modifying the original array, confusing the data store
  					var childItems = store.getValues(newParentItem, parentAttr).slice();
  					childItems.splice(insertIndex, 0, childItem);
  					store.setValues(newParentItem, parentAttr, childItems);
  				}else{
  					store.setValues(newParentItem, parentAttr,
  						store.getValues(newParentItem, parentAttr).concat(childItem));
  				}
  			}
  		},
  
  		// =======================================================================
  		// Callbacks
  
  		onChange: function(/*dojo/data/Item*/ /*===== item =====*/){
  			// summary:
  			//		Callback whenever an item has changed, so that Tree
  			//		can update the label, icon, etc.   Note that changes
  			//		to an item's children or parent(s) will trigger an
  			//		onChildrenChange() so you can ignore those changes here.
  			// tags:
  			//		callback
  		},
  
  		onChildrenChange: function(/*===== parent, newChildrenList =====*/){
  			// summary:
  			//		Callback to do notifications about new, updated, or deleted items.
  			// parent: dojo/data/Item
  			// newChildrenList: dojo/data/Item[]
  			// tags:
  			//		callback
  		},
  
  		onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){
  			// summary:
  			//		Callback when an item has been deleted.
  			// description:
  			//		Note that there will also be an onChildrenChange() callback for the parent
  			//		of this item.
  			// tags:
  			//		callback
  		},
  
  		// =======================================================================
  		// Events from data store
  
  		onNewItem: function(/* dojo/data/Item */ item, /* Object */ parentInfo){
  			// summary:
  			//		Handler for when new items appear in the store, either from a drop operation
  			//		or some other way.   Updates the tree view (if necessary).
  			// description:
  			//		If the new item is a child of an existing item,
  			//		calls onChildrenChange() with the new list of children
  			//		for that existing item.
  			//
  			// tags:
  			//		extension
  
  			// We only care about the new item if it has a parent that corresponds to a TreeNode
  			// we are currently displaying
  			if(!parentInfo){
  				return;
  			}
  
  			// Call onChildrenChange() on parent (ie, existing) item with new list of children
  			// In the common case, the new list of children is simply parentInfo.newValue or
  			// [ parentInfo.newValue ], although if items in the store has multiple
  			// child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue,
  			// so call getChildren() to be sure to get right answer.
  			this.getChildren(parentInfo.item, lang.hitch(this, function(children){
  				this.onChildrenChange(parentInfo.item, children);
  			}));
  		},
  
  		onDeleteItem: function(/*Object*/ item){
  			// summary:
  			//		Handler for delete notifications from underlying store
  			this.onDelete(item);
  		},
  
  		onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){
  			// summary:
  			//		Updates the tree view according to changes in the data store.
  			// description:
  			//		Handles updates to an item's children by calling onChildrenChange(), and
  			//		other updates to an item by calling onChange().
  			//
  			//		See `onNewItem` for more details on handling updates to an item's children.
  			// item: Item
  			// attribute: attribute-name-string
  			// oldValue: Object|Array
  			// newValue: Object|Array
  			// tags:
  			//		extension
  
  			if(array.indexOf(this.childrenAttrs, attribute) != -1){
  				// item's children list changed
  				this.getChildren(item, lang.hitch(this, function(children){
  					// See comments in onNewItem() about calling getChildren()
  					this.onChildrenChange(item, children);
  				}));
  			}else{
  				// item's label/icon/etc. changed.
  				this.onChange(item);
  			}
  		}
  	});
  });
  