【问题标题】:Handle client and server validation with Flex 3?使用 Flex 3 处理客户端和服务器验证?
【发布时间】:2011-05-05 12:48:22
【问题描述】:

我是一名新毕业生,所以请善待。我正在验证用户可以编辑的 Flex DataGrid 单元格中的输入。 DataGrid 中的行由mx.collections.ArrayCollection 支持,其中包括我编写的[Bindable]Model。我想验证 custom client-side Validator,如果通过,我想验证服务器上的输入。如果客户端验证失败,我想显示正常的验证错误(见下图)。如果服务器端验证失败,我想使用同一种 UI 组件来通知用户。解决方案不应包含任何外部框架(CairngormPureMVC)。

我的 DataGrid 实现是:

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

当用户编辑单元格时,调用此函数:

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

        }
    }
}

当然,要使其工作,需要在退出 verifyInputIsValid 函数之前调用(并运行)remoteObjectValidationServiceresultHandler。以“同步”方式。我知道“All IO in Flex is asynchronous”,但是必须有一种标准的方法来做这样的事情,对吧?我已经实现了我的自定义验证器,并且效果很好。

Flex 程序员如何在有效的客户端验证通过后立即在服务器上进行验证?

我意识到搜索这种“同步”设计似乎很愚蠢,我希望有人能用最佳实践解决我的问题。在我的辩护中,我想在客户端验证之后立即在服务器上进行验证的原因是我使用了 Flex 的验证框架。如果我收到来自服务器的无效响应,我想利用 Flex 必须告诉用户他/她的输入不正确的内置 UI 组件。

有什么想法吗?

【问题讨论】:

    标签: flash apache-flex actionscript-3 design-patterns flex3


    【解决方案1】:

    您可以创建一个调用远程服务的自定义验证器。只需在调用之前让验证器运行任何内部检查即可。

    很久以前我写过这样的东西。它当然可以做得更好,但你也许可以从中汲取一些想法。

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

    更新

    这是一个更简洁的示例,说明类似这样的情况。

    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
        }
    }
    

    这个想法本质上是运行验证器两次。一次是在初始检查完成时,然后在验证器收到异步操作的适当返回码时再次。如何执行此操作取决于您在应用中处理验证事件的方式。

    【讨论】:

    • @drkstr ,我正在尝试将我的代码重构到您的设计中。我可以快速问一下,您必须在 UserNameValidator 和使用它的任何组件/类中都有 handleNameCheckError 方法吗?方法名一样,对吗?
    • @drkstr 另外,第三个代码块中 myStatusText 变量的意义何在?
    • 这实际上是一个相当糟糕的例子,因为它是从我的应用程序中特定用例的上下文中取出的。我写它已经有几年了,但让我快速润色一下,我会回帖。
    • @drkstr 非常感谢您对此感兴趣 - 因为我尝试了多种解决方案,所以我耽搁了 2 天;包括thisthis。我现在正在查看Cairngorm 以尝试解决问题。 Cairngorm 包含一个看起来很有希望的Validation Library。我会及时通知你!
    • 现已更新。那里有一些逻辑空白,但是如果您从那里遇到困难,请告诉我。我可能还应该添加一个免责声明,有些人会说这不是最好的方法。我个人可能会从验证器(即某种服务控制器)外部监听结果事件,然后根据结果重新运行检查。验证器仍然可以发起调用并保留它自己的返回结果。
    【解决方案2】:

    执行此操作的“同步”方法是首先进行服务器端验证。创建远程对象并执行服务器端验证:

    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);
    }
    

    服务器验证完成后,执行客户端验证:

    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;
                        }
                    }
                }
            }
    

    【讨论】:

    • Kyle,抱歉,这不符合我的需要。我确信“最佳实践”是当且仅当客户端验证成功时才在服务器上进行验证。知道如何强制吗?
    • Brian,你可以重写我上面所做的,先进行客户端验证,然后再启动服务器验证。您只需要确保创建一个事件侦听器,该侦听器将数据网格事件与服务器验证的 resultEvent 一起传递。
    • Kyle,对我的更新有什么想法吗?我想我已经明白了,但如果能得到一些反馈,那就太好了。
    【解决方案3】:

    这篇文章很长,所以我想用解决方案“回答”我的问题,而不是“编辑” 显示解决方案的问题是最合适的。我将编辑问题以更准确地反映问题的参数(即要求解决方案不包含任何额外的 Flex 框架,例如 CairngormPureMVC,以保持简单)。

    我也承认这个解决方案有点弱。目前,我有一个额外的事件触发,我需要弄清楚并删除它 - 它确实有效并且符合业务/技术要求。这也感觉好像我在“重新发明轮子”,我宁愿不这样做。因此,如果有人有一个示例(一些设计模式?),其中包括“客户端和服务器验证”,以及对 itemEditor 的某些更改使用 Flex 验证框架(我需要的是 DataGrid cell edit),我真的会如果您将其列为答案,将不胜感激,也许我可以为您提供一些积分!

    我也不完全确定我关闭/提交编辑器的方式。我确实尝试过使用destroyItemEditor(),但它似乎对我不起作用。

    这是我的源代码:

    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);
            }
        }
    }
    

    这就是当前的代码,@drkstr 和@Kyle 我很想看看你的想法。

    感谢 StackOverflow,@drkstr 您今天获得了“已接受”复选标记,您启发了我的解决方案。

    【讨论】:

    • +1 用于发布完整解决方案! :) 我唯一的批评是服务代表。您是否有特殊原因从令牌中重新发送结果/故障事件?通常(至少根据我的经验)服务委托不会分派任何事件。我更喜欢使用某种控制器(在您的情况下为 UserValidator)封装对我的服务委托的任何调用,它应该是调用的唯一响应者。然后控制器可以调度某种有意义的自定义事件,其中包含从调用中接收到的信息,并且可以被任意数量的侦听器捕获。
    • @drkstr 嗯...很好。看来我的解决方案确实不对,我很高兴你看了一下。我正在尝试重构,我注意到在我的UserRemoteObjectService 类中取消结果和故障处理程序几乎没有任何效果。看来这个类只需要调用_userService 并返回token。我也 still 似乎有 1 个额外的事件触发。我描述的错误与您所指的错误相同吗? UserRemoteObjectService 应该返回一个令牌,对吧?
    猜你喜欢
    • 1970-01-01
    • 2021-08-01
    • 1970-01-01
    • 2016-10-16
    • 1970-01-01
    • 1970-01-01
    • 2010-11-20
    • 2011-10-17
    • 2023-04-08
    相关资源
    最近更新 更多