This post is quite long, so I thought "answering" my question with the solution rather than "editing" the question to show the solution was most appropriate. I will be editing the question to reflect the parameters of the problem more accurately (i.e. the requirement that the solution not include any additional Flex framework such as Cairngorm or PureMVC, so as to remain simple).
I also admit that this solution is a bit weak. At the moment, I have one extra event firing which I need to figure out and remove - but it does work and fit the business/technical requirements. This also felt as though I was "re-inventing the wheel" and I'd rather not. So if anyone has an example (some design pattern?) that includes "client and server validation", as well as using the Flex validation framework per some change to an itemEditor (my need is a DataGrid cell edit), I would really appreciate it if you would list it here as an answer and maybe I can offer you some points!
I'm also not entirely sure about the way that I'm closing / committing the editor. I did attempt to use destroyItemEditor(), but it didn't seem to work for me.
Here is my source code:
MyPage.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:validators="validators.*"
preinitialize="myPage_preinitializeHandler(event)"
initialize="myPage_initializeHandler(event)"
creationComplete="myPage_creationCompleteHandler(event)">
<mx:Script>
<![CDATA[
import entities.MyModel;
import entities.MyUser;
import events.MyValidatorEvent;
import mx.collections.ArrayCollection;
import mx.controls.TextInput;
import mx.events.DataGridEvent;
import mx.events.DataGridEventReason;
import mx.events.FlexEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.mxml.RemoteObject;
import services.UserRemoteObjectService;
import validators.UserValidator;
private var _userValidator:UserValidator;
private var _securedPageService:RemoteObject;
private var _securedUsersService:RemoteObject;
private var _userRemoteObjectService:UserRemoteObjectService;
[Bindable]
private var _myModelList:ArrayCollection;
protected function myPage_preinitializeHandler(event:FlexEvent):void
{
_userValidator = new UserValidator();
_myModelList = new ArrayCollection();
}
protected function myPage_initializeHandler(event:FlexEvent):void
{
_securedPageService = new RemoteObject();
_securedPageService.destination = "securedPageService";
_securedPageService.getAllData.addEventListener("result",getAllData_resultHandler);
_securedUsersService = new RemoteObject();
_securedUsersService.destination = "securedUsersService";
// For client-side and server-side validation using a RemoteObject service
_userRemoteObjectService = new UserRemoteObjectService(_securedUsersService);
_userValidator.userService = _userRemoteObjectService;
}
protected function myPage_creationCompleteHandler(event:FlexEvent):void
{
initializeModelList();
}
private function initializeModelList():void
{
_securedPageService.getAllData();
}
private function getAllData_resultHandler(event:ResultEvent):void
{
var untypedList:ArrayCollection = (event.result as ArrayCollection);
var myModel:MyModel;
for each(var m:Object in untypedList)
{
myModel = new MyModel(m.auditModelId, m.groupName,
m.reviewRequired, m.fieldNeedingValidation, m.lastReview)
_myModelList.addItem(myModel);
}
}
private function verifyInputIsValid(dgEvent:DataGridEvent):void
{
if (dgEvent.reason == DataGridEventReason.CANCELLED)
{
return; // Edit is "cancelled", do not update
}
// For the fieldNeedingValidation column only
if(dgEvent.dataField == "fieldNeedingValidation") {
// Get the new data value from the editor.
var userID:String = TextInput(dgEvent.currentTarget.itemEditorInstance).text;
_userValidator.addEventListener("totallyComplete",userValidator_completeHandler);
_userValidator.addEventListener("error",userValidator_errorHandler);
_userValidator.validateSystemUser(userID, myPageGrid.itemEditorInstance, dgEvent);
}
}
private function userValidator_completeHandler(event:MyValidatorEvent):void
{
TextInput(event.target.itemEditorInstance).errorString = "";
event.target.dataGridEvent.itemRenderer.data.fieldNeedingValidation = (event.myUser as MyUser).fullName;
myPageGrid.editedItemPosition = null;
myPageGrid.selectedIndex = -1;
}
private function userValidator_errorHandler(event:MyValidatorEvent):void
{
// Prevent the user from removing focus, and leave the cell editor open.
// The edit will not continue and store the blank value
(event.target.dataGridEvent as DataGridEvent).preventDefault();
// Write a message to the errorString property.
// This message appears when the user mouses over the editor.
TextInput(event.target.itemEditorInstance).errorString = event.errorMessage;
return;
}
]]>
</mx:Script>
<mx:Panel title="My Page">
<mx:DataGrid id="myPageGrid" dataProvider="{_myModelList}"
itemEditEnd="verifyInputIsValid(event)" editable="true">
<mx:columns>
<mx:DataGridColumn dataField="someField" headerText="Something" editable="false" />
<mx:DataGridColumn dataField="fieldNeedingValidation" editable="true" headerText="Input User ID"/>
</mx:columns>
</mx:DataGrid>
</mx:Panel>
</mx:Panel>
UserValidator.as
package validators {
import entities.IMyUser;
import entities.MyUser;
import events.MyValidatorEvent;
import flash.events.Event;
import mx.controls.TextInput;
import mx.controls.listClasses.IListItemRenderer;
import mx.events.DataGridEvent;
import mx.events.ValidationResultEvent;
import mx.rpc.AsyncToken;
import mx.rpc.Responder;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.validators.ValidationResult;
import mx.validators.Validator;
import services.IUserService;
public class UserValidator extends Validator
{
public var userService:IUserService; //Service delegate to process the remote validation
private var _itemEditor:IListItemRenderer;
private var _dataGridEvent:DataGridEvent;
private var _inputValue:String = null;
public function UserValidator()
{
super();
}
/**
* The "Core Method" of this class. Invokes validation of a userIDToValidate
* and later takes the appropriate action on UI components as need be.
*/
public function validateSystemUser(userIDToValidate:String, itemEditor:IListItemRenderer ,dgEvent:DataGridEvent):void
{
this._dataGridEvent = dgEvent;
this._itemEditor = itemEditor;
var validatorResult:ValidationResultEvent = this.validate(userIDToValidate);
if(validatorResult.type==ValidationResultEvent.INVALID){
if(validatorResult.results[0].errorCode == "validating"){
// Prevent the user from removing focus, and leave the cell editor open.
// Also, the edit will not continue and store the blank value
dgEvent.preventDefault();
// Write a message to the errorString property.
// This message appears when the user mouses over the editor.
TextInput(itemEditor).errorString = validatorResult.message;
trace("Please wait, server is validating...");
return;
}
else{
// A client-side "invalid", handled the same. This time the message
// does not include "Please wait" text
dgEvent.preventDefault();
TextInput(itemEditor).errorString = validatorResult.message;
return;
}
}
else if(validatorResult.type==ValidationResultEvent.VALID){
// Everything was successful, update the UI
TextInput(itemEditor).errorString = "";
TextInput(itemEditor).text = userIDToValidate;
return;
}
}
// Overide this method to start the validation process
override protected function doValidation(value:Object):Array
{
if (_inputValue != String(value)){
_inputValue = String(value);
}
var results:Array = super.doValidation(value); // Call base class doValidation().
if(results.length > 0){
return results; // Return if there are errors.
}
//Business rules for client side validation will determine this
var someErrorCondition:Boolean = false;
if (someErrorCondition == true)
{
results.push(new ValidationResult(true, null, "errorCode", "Error description"));
return results;
}
else{
trace("All client-side validation has passed");
/**
* Call the remote service, return an 'error' indicating server validation
* is pending. The String identifier is meaningless, except to indicate
* that it should be handled differencly by the consumer of the validation.
*/
results.push(new ValidationResult(true, null, "validating",
"Please wait: \nThe server is validating this corpID."));
var token:AsyncToken = this.userService.service_findByID(_inputValue);
token.addResponder(new Responder(userValidator_resultHandler,
userValidator_faultHandler));
return results;
}
}
private function userValidator_resultHandler(re:ResultEvent):void
{
if(re.result.errorMessage == null)
{
var myUser:IMyUser = new MyUser(re.result.corpID,re.result.fullName,re.result.managerFullName);
var validatorCompleteEvent:Event = new MyValidatorEvent("totallyComplete", "", myUser);
this.dispatchEvent(validatorCompleteEvent);
}
else
{
trace("ERROR: Something went wrong in the userValidator_resultHandler");
}
}
/**
* This fault handler is invoked because my Server (via BlazeDS) actually
* returns/throws a custom Exception. This will dispatch an error to it's consumer
* (MyPage.mxml) using the details of that Exception/FaultEvent, used later to populate
* the same UI component as Flex's standard "Validator" (client-side) would.
* @see: http://livedocs.adobe.com/flex/3/html/help.html?content=validators_2.html
*/
private function userValidator_faultHandler(fe:FaultEvent):void
{
var myUser:IMyUser = new MyUser(this._inputValue,null,null);
var errorEvent:Event = new MyValidatorEvent("error", fe.fault.rootCause.message, myUser);
dispatchEvent(errorEvent);
}
public function get itemEditorInstance():IListItemRenderer
{
return _itemEditor;
}
public function get dataGridEvent():DataGridEvent
{
return _dataGridEvent;
}
}
}
UserRemoteObjectService.as
package services
{
import mx.rpc.AsyncResponder;
import mx.rpc.AsyncToken;
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.mxml.RemoteObject;
public class UserRemoteObjectService implements IUserService
{
private var _userService:RemoteObject;
public function UserRemoteObjectService(userService:RemoteObject)
{
this._userService = userService;
}
public function service_findByID(userID:String):AsyncToken
{
var token:AsyncToken = _userService.findById(userID);
token.addResponder(
new AsyncResponder(findByID_resultHandler,
findByID_faultHandler)
);
return token;
}
private function findByID_resultHandler(event:ResultEvent, token:AsyncToken=null):void
{
event.token.dispatchEvent(event);
}
private function findByID_faultHandler(event:FaultEvent, token:AsyncToken=null):void
{
event.token.dispatchEvent(event);
}
}
}
So that's the current code, @drkstr and @Kyle I'm interested to see what you think.
Thanks StackOverflow, @drkstr you're getting the "Accepted" check mark today, you inspired my solution.