1. #1
    Sencha User maiconschelter's Avatar
    Join Date
    Nov 2008
    Location
    Brazil
    Posts
    15
    Vote Rating
    6
    maiconschelter is on a distinguished road

      2  

    Default OData proxy for ExtJS 4.1

    OData proxy for ExtJS 4.1


    This is my extension for using OData protocol and REST services in ExtJS 4.1.
    Please, report bugs if exists.

    Code:
    /**
    
     * Class for construction the OData proxy for ExtJS 4.1.
     * @extends Ext.data.proxy.Ajax
     * @author Maicon Schelter
     * @example
     * Ext.create('Ext.data.Store',{
     *          autoLoad    : true
     *         ,autoSync    : true
     *         ,proxy : {
     *              type    : 'odata'
     *             ,url    : 'DTE/services/ListCars'
     *             ,params : {
     *                 type : 'DTE.Model.Cars'
     *             }
     *         }
     *     });
     */
    Ext.define('Ext.data.proxy.OData',{
    
         extend                : 'Ext.data.proxy.Ajax'
        ,alternateClassName    : 'Ext.data.OData'
        ,alias                : 'proxy.odata'
    
        /**
         * @cfg {Boolean} appendId
         * Indicate if concat ID at URL request.
         */
        ,appendId            : true
    
        /**
         * @cfg {Boolean} batchActions
         * Indicate if execute the batch action of request.
         */
    
        ,batchActions        : false
    
        /**
         * Method using with create the url of Ajax.
         * @method buildUrl
         * @param {Object} request Request options.
         * @private
         */
        ,buildUrl : function(request)
        {
            var  me            = this
                ,operation    = request.operation
                ,records    = operation.records || []
                ,record        = records[0]
                ,format        = me.format
                ,url        = me.getUrl(request)
                ,id            = record && record.get('__metadata') ? record.get('__metadata').uri : operation.id;
    
            if(me.appendId && id)
            {
                url = Ext.util.Format.urlODataFormat(id);
            }
    
            if(format)
            {
                if(!url.match(/\.$/))
                {
                    url += '.';
                }
    
                url += format;
            }
    
            request.url = url;
    
            return me.callParent(arguments);
        }
    
        ,buildRequest : function(operation, callback, scope)
        {
            this.sortParam = (scope.remoteSort ? '$orderby' : null);
            this.filterParam = (scope.remoteFilter ? '$filter' : null);
    
            return Ext.data.proxy.Rest.superclass.buildRequest.apply(this, arguments);
        }
    
        /**
         * Method using with create of sorters.
         * @method encodeSorters
         * @param {Array} sorters Request sorters.
         * @private
         */
        ,encodeSorters : function(sorters)
        {
            var  min    = []
                ,length    = sorters.length
                ,i        = 0;
    
            for(; i<length; i++)
            {
                min[i] = sorters[i].property;
    
                if(sorters[i].direction.toLowerCase() == 'desc')
                {
                    min[i] += ' desc';
                }
            }
    
            return min.join(',');
        }
    
        /**
         * Method using with create of filters.
         * @method encodeFilters
         * @param {Array} filters Request filters.
         * @private
         */
        ,encodeFilters : function(filters)
        {
            var  filter    = []
                ,length    = filters.length
                ,sq        = '\''
                ,type    = ''
                ,i;
    
            for(i=0; i<length; i++)
            {
                type = filters[i].type || '';
    
                switch(type)
                {
                    case 'int':
                        type    = '';
                        sq        = '';
                        break;
                    case 'guid':
                        type    = 'guid';
                        sq        = '\'';
                        break;
                    default:
                        type    = '';
                        sq        = '\'';
                        break;
                }
    
                filter[i] = filters[i].property + ' eq ' + type + sq + filters[i].value + sq;
            }
    
            return filter.join(' and ');
        }
    },function()
    {
        /**
         * Override defaults property's of requests.
         */
        Ext.apply(this.prototype,{
            actionMethods : {
                 create : 'POST'
                ,read   : 'GET'
                ,update : 'MERGE'
                ,destroy: 'DELETE'
            }
            ,headers : {
                'Accept' : 'application/json'
            }
            ,pageParam    : undefined
            ,startParam    : '$skip'
            ,limitParam    : '$top'
            ,noCache    : false
        });
    });
    
    
    /**
     * Override of Ext.data.proxy.Server.
     * @extends Ext.data.proxy.Server
     * @author Maicon Schelter
     */
    Ext.override(Ext.data.proxy.Server,{
    
        /**
         * @cfg {Boolean} sorter
         * Indicate if send the sorters to server at proxy.
         */
        sorter : true
    
        /**
         * Method using to process the response request of server.
         * @method processResponse
         * @param {Boolean} success Success param.
         * @param {Object} operation Object operation of request.
         * @param {Object} request Request configuration.
         * @param {Object} response Object response of request.
         * @param {Object} callback Function target at finally.
         * @param {Object} scope Scope of callback function.
         */
        ,processResponse : function(success, operation, request, response, callback, scope)
        {
            var  me = this
                ,reader
                ,result;
    
            reader    = me.getReader();
            result    = reader.read(me.extractResponseData(response));
    
            Ext.apply(operation,{
                 response    : response
                ,resultSet    : result
            });
    
            if(success === true)
            {
                if(result.success !== false)
                {
                    operation.commitRecords(result.records);
                    operation.setCompleted();
                    operation.setSuccessful();
                }
                else
                {
                    operation.setException(result.message);
                    me.fireEvent('exception', this, response, operation);
                }
            }
            else
            {
                me.setException(operation, response);
                me.fireEvent('exception', this, response, operation);
            }
    
            if(typeof callback == 'function')
            {
                callback.call(scope || me, operation);
            }
    
            me.afterRequest(request, success);
        }
    
        /**
         * Create the request proxy params.
         * @method getParams
         * @param {Object} operation Object operation config.
         */
        ,getParams : function(operation)
        {
            var  me                = this
                ,params            = {}
                ,isDef            = Ext.isDefined
                ,groupers        = operation.groupers
                ,sorters        = operation.sorters
                ,filters        = operation.filters
                ,page            = operation.page
                ,start            = operation.start
                ,limit            = operation.limit
                ,simpleSortMode    = me.simpleSortMode
                ,pageParam        = me.pageParam
                ,startParam        = me.startParam
                ,limitParam        = me.limitParam
                ,groupParam        = me.groupParam
                ,sortParam        = me.sortParam
                ,filterParam    = me.filterParam
                ,directionParam    = me.directionParam;
    
            if(pageParam && isDef(page))
            {
                params[pageParam] = page;
            }
    
            if(startParam && isDef(start))
            {
                params[startParam] = start;
            }
    
            if(limitParam && isDef(limit))
            {
                params[limitParam] = limit;
            }
    
            if(groupParam && groupers && groupers.length > 0)
            {
                params[groupParam] = me.encodeSorters(groupers);
            }
    
            if(this.sorter && sortParam && sorters && sorters.length > 0)
            {
                if(simpleSortMode)
                {
                    params[sortParam]        = sorters[0].property;
                    params[directionParam]    = sorters[0].direction;
                }
                else
                {
                    params[sortParam] = me.encodeSorters(sorters);
                }
            }
    
            if(filterParam && filters && filters.length > 0)
            {
                params[filterParam] = me.encodeFilters(filters);
            }
    
            return params;
        }
    });
    
    
    /**
     * Override if Ext.data.AbstractStore.
     * @extends Ext.data.AbstractStore
     * @author Maicon Schelter
     */
    Ext.override(Ext.data.AbstractStore,{
    
        /**
         * @cfg {Boolean} autoRevert
         * Indicate if rever store modified record case ocurrer a error.
         */
        autoRevert : true
    
        /**
         * Method using to verify if are exists records modifieds in store.
         * @method isModified
         * @return {Boolean}
         */
        ,isModified : function()
        {
            var  me        = this
                ,toCreate  = me.getNewRecords()
                ,toUpdate  = me.getUpdatedRecords()
                ,toDestroy = me.getRemovedRecords()
                ,needsSync = false;
    
            if(toCreate.length > 0 || toUpdate.length > 0 || toDestroy.length > 0)
            {
                needsSync = true;
            }
    
            return needsSync;
        }
    
        /**
         * Method using case ocurrer a error in store sync.
         * @method onBatchException
         * @param {Object} batch Object batch options of request.
         * @param {Object} operation Object request config.
         * @private
         */
        ,onBatchException : function(batch, operation)
        {
            if(this.autoRevert)
            {
                if(this.operation)
                {
                    this.revertFailedOperation(operation);
                }
                else
                {
                    for(var i=0; i<batch.operations.length; i++)
                    {
                        this.revertFailedOperation(batch.operations[i]);
                    }
                }
            }
        }
    
        /**
         * Method using to revert store records case ocurrer a error in store sync.
         * @method revertFailedOperation
         * @param {Object} operation Object request options.
         */
        ,revertFailedOperation : function(operation)
        {
            var records = operation.getRecords();
    
            if(records.length == 0)
            {
                return;
            }
    
            var store = this;
    
            switch(operation.action)
            {
                case "create":
                    store.remove(records);
                    break;
    
                case "update":
                    Ext.each(records, function(item)
                    {
                        item.reject();
                    },this);
                    break;
    
                case "destroy":
                    store.removed = [];
                    store.loadRecords(records, {addRecords:true});
                    break;
            }
        }
    });
    
    
    /**
     * Override of Ext.data.Connection.
     * @extends Ext.data.Connection
     * @author Maicon Schelter
     */
    Ext.override(Ext.data.Connection,{
    
        /**
         * Method using to send and request ajax data.
         * @method request
         * @param {Object} options Object request options.
         */
        request : function(options)
        {
            options = options || {};
    
            var  me            = this
                ,scope        = options.scope        || window
                ,username    = options.username    || me.username
                ,password    = options.password    || me.password || ''
                ,async
                ,requestOptions
                ,request
                ,headers
                ,xhr
                ,expandParam;
    
            if(options.proxy && options.proxy.params && options.proxy.params.expand)
            {
                expandParam = options.proxy.params.expand;
            }
            else if(options.params && options.params.expand)
            {
                expandParam = options.params.expand;
                delete options.params.expand;
            }
    
            if(expandParam && options.method !== 'DELETE' && options.method !== 'MERGE')
            {
                if(Ext.isArray(expandParam))
                {
                    expandParam = expandParam.join(',');
                }
    
                options.url = Ext.urlAppend(options.url, ('$expand=' + expandParam));
            }
    
            if(me.fireEvent('beforerequest', me, options) !== false)
            {
                requestOptions = me.setOptions(options, scope);
    
                if(this.isFormUpload(options) === true)
                {
                    this.upload(options.form, requestOptions.url, requestOptions.data, options);
                    return null;
                }
    
                if(options.autoAbort === true || me.autoAbort)
                {
                    me.abort();
                }
    
                if((options.cors === true || me.cors === true) && Ext.isIe && Ext.ieVersion >= 8)
                {
                    xhr = new XDomainRequest();
                }
                else
                {
                    xhr = this.getXhrInstance();
                }
    
                async = options.async !== false ? (options.async || me.async) : false;
    
                if(username)
                {
                    xhr.open(requestOptions.method, requestOptions.url, async, username, password);
                }
                else
                {
                    xhr.open(requestOptions.method, requestOptions.url, async);
                }
    
                if(options.withCredentials === true || me.withCredentials === true)
                {
                    xhr.withCredentials = true;
                }
    
                if(options.headers)
                {
                    Ext.apply(options.headers, {'Accept':'application/json','Content-Type':'application/json'});
    
                    if(options.method !== 'DELETE' && options.method !== 'MERGE')
                    {
                        delete options.headers['If-Match'];
                    }
                }
                else
                {
                    Ext.apply(options, {headers:{'Accept':'application/json','Content-Type':'application/json'}});
                }
    
                try
                {
                    requestOptions.data = Ext.decode(requestOptions.data);
                }
                catch(ex)
                {
                    requestOptions.data = Ext.urlDecode(requestOptions.data);
                }
                finally
                {
                    if(requestOptions.data)
                    {
                        var type;
    
                        if(requestOptions.data.__metadata)
                        {
                            if(requestOptions.data.__metadata.etag)
                            {
                                Ext.apply(options.headers, {'If-Match':requestOptions.data.__metadata.etag});
                            }
    
                            if(requestOptions.data.__metadata.type)
                            {
                                type = requestOptions.data.__metadata.type;
                            }
                        }
                        else if(requestOptions.data.type)
                        {
                            type = requestOptions.data.type;
                            delete requestOptions.data.type;
                        }
                        else if(scope.params && scope.params.type)
                        {
                            type = scope.params.type;
                        }
    
                        delete requestOptions.data.__metadata;
    
                        if(!Ext.isEmpty(type))
                        {
                            Ext.apply(requestOptions.data,{'__metadata':{'type':type}});
                        }
                    }
    
                    requestOptions.data = Ext.encode(requestOptions.data);
                }
    
                headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
    
                request = {
                     id        : ++Ext.data.Connection.requestId
                    ,xhr    : xhr
                    ,headers: headers
                    ,options: options
                    ,async    : async
                    ,timeout: setTimeout(function()
                    {
                        request.timedout = true;
                        me.abort(request);
                    }, options.timeout || me.timeout)
                };
    
                me.requests[request.id]    = request;
                me.latestId                = request.id;
    
                if(async)
                {
                    xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me, [request]);
                }
    
                xhr.send(requestOptions.data);
    
                if(!async)
                {
                    return this.onComplete(request);
                }
    
                return request;
            }
            else
            {
                Ext.callback(options.callback, options.scope, [options, undefined, undefined]);
                return null;
            }
        }
    
        /**
         * Method using to treat ajax request returns.
         * @method onComplete
         * @param {Object} request Object request config.
         */
        ,onComplete : function(request)
        {
            var  me            = this
                ,options    = request.options
                ,result
                ,success
                ,response
                ,code
                ,etag;
    
            try
            {
                result = me.parseStatus(request.xhr.status);
            }
            catch(e)
            {
                result = {
                     success        : false
                    ,isException    : false
                };
            }
    
            success    = result.success;
            code    = request.xhr.status;
            etag    = request.xhr.getResponseHeader('Etag');
    
            if(success)
            {
                response = me.createResponse(request);
                me.fireEvent('requestcomplete', me, response, options);
    
                options.headers['If-Match'] = etag;
                Ext.callback(options.success, options.scope, [response, options]);
            }
            else
            {
                if(result.isException || request.aborted || request.timedout)
                {
                    response = me.createException(request);
                }
                else
                {
                    response = me.createResponse(request);
                }
    
    
    
                me.fireEvent('requestexception', me, response, options);
                Ext.callback(options.failure, options.scope, [response, options]);
            }
    
            Ext.callback(options.callback, options.scope, [options, success, response]);
    
            delete me.requests[request.id];
            return response;
        }
    });

  2. #2
    Sencha User maiconschelter's Avatar
    Join Date
    Nov 2008
    Location
    Brazil
    Posts
    15
    Vote Rating
    6
    maiconschelter is on a distinguished road

      0  

    Default OData proxy for ExtJS 4.1

    OData proxy for ExtJS 4.1


    File to download.
    OData.zip

  3. #3
    Sencha User
    Join Date
    Aug 2011
    Posts
    50
    Vote Rating
    16
    stevo.zilik will become famous soon enough

      0  

    Default


    Thanks mate, good work, running it with latest asp.net mvc 4 RC and looks great. However I'm wondering how do you go about paging? Specifically how is the store supposed to know how many records (pages) are there in total?

  4. #4
    Sencha User maiconschelter's Avatar
    Join Date
    Nov 2008
    Location
    Brazil
    Posts
    15
    Vote Rating
    6
    maiconschelter is on a distinguished road

      1  

    Default


    Quote Originally Posted by stevo.zilik View Post
    Thanks mate, good work, running it with latest asp.net mvc 4 RC and looks great. However I'm wondering how do you go about paging? Specifically how is the store supposed to know how many records (pages) are there in total?

    The OData protocol is provider the total page size to store, only using start and limit pages.

  5. #5
    Sencha User
    Join Date
    Aug 2011
    Posts
    50
    Vote Rating
    16
    stevo.zilik will become famous soon enough

      0  

    Default


    I see, got it working now.

    In case anyone stumbles upon the same problem in asp.net mvc 4 RC, this will resolve the issue:

    http://www.strathweb.com/2012/06/ext...eful-metadata/

  6. #6
    Sencha User
    Join Date
    Aug 2011
    Posts
    50
    Vote Rating
    16
    stevo.zilik will become famous soon enough

      1  

    Default Enhanced support

    Enhanced support


    Whilst working on my Asp.net Mvc 4 project, I enhanced the code with a couple of features, hope that's ok, please find attached.

    Features added:
    • support for all comparison operators (original code assumes 'eq')
    • support for 'or' logical operator to connect multiple filters (original code assumes 'and')
    • support for 'like' comparison operator (Note that this is outside of OData specification, but asp.net mvc web api can support this out-of-the-box)
    Samples how to add filters to store:

    Code:
    //initialize the store
    var store = Ext.create('Ext.data.Store', { .... });
    ...
    //set filters after user input e.g.:
    store.filters.clear();
    store.filters.addAll([{
                        property: 'Name',
                        value: 'abc'
                    },{
                        property: 'Address',
                        operator: 'ne',
                        logical: 'and',
                        value: 'efd'
                    },{
                        property: 'Website',
                        operator: 'like',
                        logical: 'or',
                        value: 'ghj'
                    }]);
    
    ...
    //refresh
    store.load();
    source OData.zip

  7. #7
    Sencha User
    Join Date
    Apr 2012
    Posts
    2
    Vote Rating
    0
    snowman2154 is on a distinguished road

      0  

    Default


    Where is Ext.util.Format.urlODataFormat?

  8. #8
    Sencha User maiconschelter's Avatar
    Join Date
    Nov 2008
    Location
    Brazil
    Posts
    15
    Vote Rating
    6
    maiconschelter is on a distinguished road

      0  

    Default


    Thanks for support stevo.zilik, excellent work.
    Using the urlODataFormat function to create a valid url ajax request, adding filters, orders and others. Ex.: http://services.odata.org/OData/OData.svc/Category(1)
    /Products?$top=2&orderby=name.

  9. #9
    Sencha Premium Member dawesi's Avatar
    Join Date
    Mar 2007
    Location
    Melbourne, Australia (aka GMT+10)
    Posts
    1,082
    Vote Rating
    41
    dawesi has a spectacular aura about dawesi has a spectacular aura about

      0  

    Default


    just wondering how this is different from the one bundled with ExtJS?
    Teahouse Training Company
    Official Certified Sencha Trainer

    Australia / New Zealand / Singapore / Hong Kong & APAC



    SenchaWorld.com - Sencha webinars, videos, etc
    SenchaForge.org - (coming soon)
    TeahouseHQ.com - Sencha ecosystem training portal

    Code Validation : JSLint | JSONLint | JSONPLint

  10. #10
    Sencha User
    Join Date
    Apr 2012
    Posts
    2
    Vote Rating
    0
    snowman2154 is on a distinguished road

      0  

    Default


    You mean the REST proxy included with Extjs? I would assume that it handles the oData url formatting and whatnot. There are specifics to oData that need to be handled.

    I don't believe there is an oData proxy packaged with Ext JS