Data Source Examples

In this article, we consider some of the most common examples demonstrating how to connect to various sources of data using a DevExtreme data layer. Regardless of the data source type, approaches to data reading and editing are the same. They are described in the Data Layer article.

In-memory Data

The most simple data layer is one that deals with in-memory arrays. DevExtreme provides an implementation of the Store interface for this purpose (ArrayStore), as well as convenient shortcuts for creating a DataSource from arrays, and a Query tool for custom queries (see Query Concept).

In this case, the data lifetime equals the lifetime of the application, but no additional setup activity is required. An in-memory DataSource is great for easily understanding examples and for prototyping. It can also be used in a real application; for example when you obtain a medium-sized array from a web service and then process it on the client side.

Here is the general form of creating a DataSource from an array.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: {
        type: "array",
        key: "id",
        data: [
            { id: 1, value: "Item 1" }
        ]
    }
});

This notation allows you to specify any configuration options for both the ArrayStore and DataSource. However, more compact forms are available for read-only cases (see Creating DataSource).

Local Data

For working with HTML5 Web Storage (known as window.localStorage), the data layer provides the LocalStore. It functions exactly as the ArrayStore described in the previous section, and also ensures that the data is persisted in the browser's localStorage, immediately or at regular intervals.

To create this kind of a DataSource, use the following code.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: {
        type: "local",
        name: "MyLocalData",
        key: "id"
    }
});

The name configuration option is required to scope the data and distinguish it from other localStorage contents. There are two LocalStore options controlling how often the underlying array is persisted: flushInterval and immediate.

OData

OData is a universal open protocol for consuming data APIs. The DevExtreme data layer provides a special Store implementation to access OData web services (ODataStore).

Use ODataStore to access one OData entity specified by the URL, or the ODataContext object to communicate with an entire OData service.

See Also

Using ODataContext

This object represents a whole OData service, it creates a number of ODataStore instances inside, so you can access separate entities. In addition, the ODataContext includes get() and invoke() methods used to invoke service operations, and the objectLink() helper method to link entities.

To create an ODataContext instance, call the ODataContext constructor with the required configuration object.

JavaScript
var context = new DevExpress.data.ODataContext({
    url: "http://www.example.com/Northwind.svc",
    errorHandler: function(error) {
        alert(error.message);
    },
    entities: {
        Categories: { 
            key: "CategoryID", 
            keyType: "Int32" 
        },
        MyCustomers: { 
            name: "Customers",
            key: "CustomerID", 
            keyType: "String" 
        }
    }
});

In the example above, ODataContext is created to access two entities (Categories and Customers) from the service located at the http://www.example.com/Northwind.svc URL.

Each sub-object of the entities configuration object defines a name and configuration settings for an ODataStore within this context instance. Two ODataStore objects are impicitly created: context.Categories to access http://www.example.com/Northwind.svc/Categories and context.MyCustomers accessing http://www.example.com/Northwind.svc/Customers. Note how in the second case we specified different names for the store and the entity by using additional name parameter.

Now, you can create a DataSources to load data.

JavaScript
var categoriesSource = new DevExpress.data.DataSource(context.Categories);

The following example illustrates how create the same DataSource providing the additional configuration.

JavaScript
var categoriesSource = new DevExpress.data.DataSource({
    store: context.Categories,
    pageSize: 5,
    sort: "CategoryName"
});    

To perform data modification, use Store objects directly:

JavaScript
context.Categories
    .update(1, { CategoryName: "Beverages" })
    .done(doneCallback)
    .fail(failCallback);

Key Types

When specifying keys in the ODataStore configuration, and in the entities options for the ODataContext, specify the appropriate key types as well. The following key types are supported out of the box: String, Int32, Int64, and Guid.

In most cases, the key expression is a single property.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "/url/to/service",
    key: "CategoryID",
    keyType: "Int32"
});

For compound keys consisting of multiple properties, the following syntax is used.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "/url/to/service",
    key: [ "OrderID", "ProductID" ],
    keyType: {
        OrderID: "Int32",
        ProductID: "Int32"
    } 
});

If you need to use a key type that is not supported by default, use the odata.keyConverters utility object to register your own key type.

JavaScript
DevExpress.data.utils.odata.keyConverters["MyType"] = function(value) { 
    return value + "MT"; //returns an URL component for 'value'
};

Edm Literals

OData defines some primitive data types which cannot be represented in JavaScript, for example Int64. To work with such values, use the EdmLiteral class. For the information on primitive data types, refer to the OData documentation.

JavaScript
dataSource.filter("Distance", "<", new DevExpress.data.EdmLiteral("100000L"));

The code snippet above shows a filter expression involving the Int64 Distance property.

The next example shows how to load an entity with the Int64 key.

JavaScript
store.byKey(new DevExpress.data.EdmLiteral("123L")).done(doneCallback);

GUIDs

GUID (Globally Unique Identifier) is another common data type you may encounter while working with OData services. The DevExtreme data layer includes the Guid class, which enables you to generate new GUIDs and work with existing GUIDs.

To create a Guid instance, call the Guid constructor. If you pass a string value specifying a GUID to the constructor, the created Guid instance will hold the specified value.

JavaScript
var guid = new DevExpress.data.Guid("bd330029-8106-6d2d-5371-f27325155e99");

If you call the constructor without arguments, a new GUID will be generated.

JavaScript
var guid = new DevExpress.data.Guid();

Associations

Consider the following ODataContext.

JavaScript
var context = new DevExpress.data.ODataContext({
    url: "http://www.example.com/Northwind.svc",
    entities: {
        Categories: { 
            key: "CategoryID", 
            keyType: "Int32" 
        },
        Products: { 
            key: "ProductID", 
            keyType: "Int32" 
        }
    }
});

Assume that each Product entity is connected to a Category via the Product.Category navigation property.

Navigation properties are usually deferred and are not loaded automatically together with the owning entity. To load both entities at once, use the expand load options extension, specific for ODataStore.

JavaScript
var productSource = new DevExpress.data.DataSource({
    store: context.Products,
    expand: [ "Category" ]
});

The expand option is also supported by the byKey method.

JavaScript
context.Products.byKey(1, { expand: [ "Category" ] });

Another task is updating a navigation property or inserting a new entity with a navigation property, in other words creating links between entities. To accomplish this, use the objectLink(entityAlias, key) method of the ODataContext.

In the following example, the Category property of the Product entity with the key 1 is changed to the Category with the key 2.

JavaScript
context.Products.update(1, {
    Category: context.objectLink("Categories", 2)
});

Invoking Service Operations

In addition to entites, OData services may expose service operations. The ODataContext class supports this capability. For the information on service operations, refer to the OData documentation.

To invoke an operation which does not return any value, use the invoke() method.

JavaScript
context.invoke("MyAction", { param: "value" });

To invoke an operation and get its return value, use the get() method.

JavaScript
context.get("GetSomeValue", { param: "value" });

One interesting case is a service operation which supports querying on top of it. In this case, the operation may be treated as a read-only entity, and input parameters can be passed to the customQueryParams extension of the DataSource load options.

JavaScript
var context = new DevExpress.data.ODataContext({
    entities: {
        "GetSomeValue": { 
        }
    }
});

new DevExpress.data.DataSource({
    store: context.GetSomeValue,

    // operation parameters
    customQueryParams: {
        operationParam: "value"
    }
});

Custom Sources

Any custom data access logic can be implemented using the CustomStore class. In the CustomStore, all data access operations must be implemented by a developer.

JavaScript
var myStore = new DevExpress.data.CustomStore({
    load: function(loadOptions) {
        // . . .
    },
    byKey: function(key, extra) {
        // . . .
    },
    update: function(values) {
        // . . .
    },
    . . .  
});

var dataSource = new DevExpress.data.DataSource({
    store: myStore
});

DataSource supports a more brief syntax, without introducing an explicit CustomStore instance.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    load: function(loadOptions) {
        // . . .
    },
    byKey: function(key, extra) {
        // . . .
    },
    update: function(values) {
        // . . .
    },
    . . .
});

As an example, consider the following synthetic implementation, which generates a read-only infinite list:

JavaScript
var infiniteListSource = new DevExpress.data.DataSource({
    load: function(loadOptions) {
        var result = [ ];
        for(var i = 0; i < loadOptions.take; i++)
            result.push({ id: 1 + loadOptions.skip + i });            
        return result;
    },
    byKey: function(key) {
        return { id: key };
    }
});

In this example, load and byKey functions are synchronous, that is they return a result right away. In the next example, we will connect to a remote web service and therefore functions will return jQuery.Deferred promises.

Connect to RESTful Service

Assume that you have a web service published at a certain URL, for example http://www.example.com/service/entity1. This web service implements CRUD operations on data (Create, Read, Update, Delete) and follows the HTTP request conventions listed below.

  • GET http://www.example.com/service/entity1 request returns a list of all entities
  • GET http://www.example.com/service/entity1/123 request returns a single entity identified by the 123 key
  • POST http://www.example.com/service/entity1 adds a new entity built from the values passed in HTTP request body
  • PUT http://www.example.com/service/entity1/123 updates an entity identified by the 123 key with the values passed in HTTP request body
  • DELETE http://www.example.com/service/entity1/123 deletes an entity identified by the 123 key

Such services can have their own URL conventions and additional query-string parameters, they can use different HTTP methods, and different implementation of HTTP request body handlers. That is why the DevExtreme data layer does not provide a ready-to-use component to communicate with these services. However, the CustomStore class enables you to easily utilize any service.

For the service type described above, you can apply the following simple custom DataSource implementation.

JavaScript
new DevExpress.data.CustomStore({

    load: function(loadOptions) {
        return $.getJSON(SERVICE_URL);
    },

    byKey: function(key) {
        return $.getJSON(SERVICE_URL + "/" + encodeURIComponent(key));
    },

    insert: function(values) {
        return $.post(SERVICE_URL, values);
    },

    update: function(key, values) {
        return $.ajax({
            url: SERVICE_URL + "/" + encodeURIComponent(key),
            method: "PUT",
            data: values
        });
    },

    remove: function(key) {
        return $.ajax({
            url: SERVICE_URL + "/" + encodeURIComponent(key),
            method: "DELETE",
        });
    }

});

Note that all user functions return the result of the jQuery AJAX call, which is compatible with the jQuery.Deferred promise. In fact, you may use any promise-compatible object to connect to any asynchronous data storage; for example - to an HTML5 File API and not necessarily to HTTP endpoints.

The load functions accept a bag of loadOptions, which allows you to send various data shaping options (such as sorting, filtering, paging, etc.) to a remote storage.

Note that certain widgets have peculiarities in the CustomStore implementation. For example, in case of the DataGrid, the load function should also return the total count of received records.

See Also

Note On Same-Origin Policy

One common pitfall that occurs during communication with remote web services from JavaScript is the Same-Origin Policy. It is a security restriction enforced by web browsers that do not directly allow HTTP communication between different domains (not even between endpoints located at two different ports of the same website).

To consume a web service from JavaScript, the web service has to support the Cross-Origin Resource Sharing feature, also known as CORS.

For read-only access, instead of CORS, a web service may support the JSONP (JSON with padding) technique. Built-in DevExtreme, Data Store classes support JSONP. For example, to connect to an OData service with JSONP support, use the jsonp configuration option.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "http://www.example.com",
    jsonp: true
});