blong
blong

Reputation: 2713

Handle client and server validation with Flex 3?

I'm a new grad, so please be kind. I'm working on validating input in a Flex DataGrid cell that the user can edit. The rows in the DataGrid are backed by an mx.collections.ArrayCollection that includes a [Bindable]Model that I wrote. I want to validate against a custom client-side Validator, if-and-only-if that passes I want to validate the input on the server. If client-side validation fails, I want to display the normal validation error (see image below). If server-side validation fails, I want to use the same sort of UI components to notify the user. The solution should not include any external framework (Cairngorm or PureMVC).

Standard Validator usage

My DataGrid implementation is:

<mx:DataGrid id="myPageGrid" dataProvider="{myModelList}" editable="true"
             itemEditEnd="verifyInputIsValid(event)">
    <mx:columns>
        <mx:DataGridColumn dataField="name" headerText="Name"
                           editable="false" />

        <mx:DataGridColumn dataField="fieldNeedingValidation" editable="true" 
                           id="fnv" headerText="Field Needing Validation" />

    </mx:columns>
</mx:DataGrid>

When a user edit's a cell, this function is called:

private function verifyInputIsValid(event:DataGridEvent):void
{
    // Check the reason for the event.
    if (event.reason == DataGridEventReason.CANCELLED)
    {
        return; // Do not update cell.
    }            

    // For the fieldNeedingValidation only
    if(event.dataField == "fieldNeedingValidation") {
        // Get the new data value from the editor.
        var newValue:String = TextInput(event.currentTarget.itemEditorInstance).text;
        var validatorResult:ValidationResultEvent = myValidator.validate(newValue);

        if(validatorResult.type==ValidationResultEvent.INVALID){
            // Prevent the user from removing focus,  and leave the cell editor open.  
            // Also, the edit will not continue and store the blank value
            event.preventDefault();
            // Write a message to the errorString property. 
            // This message appears when the user mouses over the editor.
            TextInput(myPageGrid.itemEditorInstance).errorString = validatorResult.message;
            return;                     
        }
        else if(validatorResult.type==ValidationResultEvent.VALID){
            // Assuming the data is valid on the Server, this is fine
            TextInput(myPageGrid.itemEditorInstance).errorString = "";
            TextInput(myPageGrid.itemEditorInstance).text = newValue;
            return;


            // I'd rather do this
            remoteObjectValidationService.validate(newValue);
            // Get a String result back from the call to the RemoteObject
            // Mark this "edit" (of the cell) as invalid, just as the client-side validator would

        }
    }
}

Of course, for this to work, the resultHandler of the remoteObjectValidationService would need to be invoked (and run) before we exit the verifyInputIsValid function. In a "synchronous" fashion. I know “All IO in Flex is asynchronous” , but there must be a standard way to do something like this right? I've already implemented my custom Validator and that works just fine.

How do Flex programmers validate on the server, immediately after the valid client-side validation passes?

I realize it seems silly to search out this "synchronous" design and I hope someone has an answer to solve my problem with best practices. In my defense, the reason I want to validate on the server immediately following client-side validation is so that I'm using Flex's validation framework. If I get an invalid response from the server, I want to leverage the built-in UI components that Flex has to tell the user something is incorrect about his/her input.

Any ideas?

Upvotes: 1

Views: 1622

Answers (3)

blong
blong

Reputation: 2713

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.

Upvotes: 1

drkstr
drkstr

Reputation: 609

You can create a custom validator that calls a remote service. Just have the validator run any internal checks before making the call.

I wrote something like this a loooong time ago. It could certainly have been done better, but you may be able to pull a few ideas from it.

http://www.actionscript.org/forums/showthread.php3?t=173275

UPDATE

Here is a cleaner example of what something like this might look like.

package
{
    import flash.events.Event;
    import mx.rpc.AsyncToken;
    import mx.rpc.Responder;
    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;
    import mx.validators.Validator;

    public class UsernameValidator extends Validator
    {
        /**
         *
         */
        public function UsernameValidator()
        {
            super();
        }

        /**
         * Inject or create some kind of service delegate to process the remote check.
         */
        public var userNameService:IUserNameService;

        /**
         * Store the result of the remote check for the second pass through. 
         */     
        private var _nameCheckResult:int = 0;


        /**
         * Overide this method to start the validation process
         */
        override protected function doValidation(value:Object):Array
        {
            var userName:String = String(value);
            var invalidChars:RegExp = /\W+/;

            // Call base class doValidation().
            var results:Array = super.doValidation(value);

            // Return if there are errors.
            if(results.length > 0)
                return results;

            // If input value is 0, or contains no value, 
            // issue a validation error.
            if(!userName)
            {
                results.push(new ValidationResult(true, null, "required", "No user name was entered. Please select a user name"));
            }
            else if(userName.match(invalidChars))
            {
                results.push(new ValidationResult(true, null, "invalidChars", "This user name contains non alphanumeric characters [a-zA-Z0-9_]. Please select another and try again."));
            }
            else if(_nameCheckResult == 1)
            {
                //well assume that 1 means it's bad
                results.push(new ValidationResult(true, null, "taken", "This user name has already been taken."));
            }
            else
            {
                //all checks have passed so return a special error type indicating a pending operation
                //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", "Checking username availability."));

                //call some kind of remote service

                var token:AsyncToken = this.userNameService.checkAvailability(userName);
                token.addResponder(new Responder(userNameService_resultHandler, userNameService_faultHandler));

                    //...you should also add some logic to handle a change to the input if you want to use "live validation"
            }

            return results;
        }

        /**
         * Dispatch some kind of event indicating an error (preferably with some return codes)
         */
        private function userNameService_faultHandler(event:FaultEvent):void
        {
            trace("UserNameValidator.handleNameCheckError");

            dispatchEvent(new Event("error"));
        }

        /**
         * Check the result and dispatch an event indicating the validation needs to be run again.
         */
        private function userNameService_resultHandler(event:ResultEvent):void
        {
            trace("userNameService_resultHandler(event)");

            _nameCheckResult = event.result as int;

            this.dispatchEvent(new Event("complete"));
        }
    }
}

package
{
    import mx.rpc.AsyncToken;

    /**
     * The interface to a service delegate that checks
     * the username and returns an AsyncToken.
     */
    public interface IUserNameService
    {
        function checkAvailability(userName:String):AsyncToken
    }
}

The idea is to essentially run the validater twice. Once when the initial check is done, then again when the validator as received an appropriate return code for the asynchronous operation. How you do this depends on how you are handling validation events in your app.

Upvotes: 1

Kyle
Kyle

Reputation: 4238

A "synchronous" way to do this would be to do server-side validation first. Create a remote object and perform server-side validation:

private function verifyInputIsValid(event:DataGridEvent):void
{
    var newValue:String = TextInput(evt.currentTarget.itemEditorInstance).text;
    remoteObjectValidationService.addEventListener("result", function(event:ResultEvent):void{
        resultHandler(event, evt); 
    });
    remoteObjectValidationService.validate(newValue);
}

Once server validation is complete, perform the client-side validation:

private function resultHandler(event:ResultEvent, evt:DataGridEvent):void{
    //Check that the server-side validation is successful
    if((event.result as String).toUpperCase() == "VALID"){

        // Check the reason for the event.
        if (event.reason == DataGridEventReason.CANCELLED)
        {
            return; // Do not update cell.
        }            

        // For the fieldNeedingValidation only
        if(event.dataField == "fieldNeedingValidation") {
            // Get the new data value from the editor.
        var newValue:String = TextInput(event.currentTarget.itemEditorInstance).text;
        var validatorResult:ValidationResultEvent = myValidator.validate(newValue);

        if(validatorResult.type==ValidationResultEvent.INVALID){
        // Prevent the user from removing focus,  and leave the cell editor open.  
        // Also, the edit will not continue and store the blank value
        event.preventDefault();
         // Write a message to the errorString property. 
        // This message appears when the user mouses over the editor.
                TextInput(myPageGrid.itemEditorInstance).errorString = validatorResult.message;
                return;                     
            }
            else if(validatorResult.type==ValidationResultEvent.VALID){
                // Assuming the data is valid on the Server, this is fine
                        TextInput(myPageGrid.itemEditorInstance).errorString = "";
                        TextInput(myPageGrid.itemEditorInstance).text = newValue;
                return;
                    }
                }
            }
        }

Upvotes: 1

Related Questions