DevExtreme Angular - 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.
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.
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.
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.
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.
var categoriesSource = new DevExpress.data.DataSource(context.Categories);
The following example illustrates how create the same DataSource providing the additional configuration.
var categoriesSource = new DevExpress.data.DataSource({ store: context.Categories, pageSize: 5, sort: "CategoryName" });
To perform data modification, use Store objects directly:
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.
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.
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.
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.
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.
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.
var guid = new DevExpress.data.Guid("bd330029-8106-6d2d-5371-f27325155e99");
If you call the constructor without arguments, a new GUID will be generated.
var guid = new DevExpress.data.Guid();
Associations
Consider the following ODataContext.
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.
var productSource = new DevExpress.data.DataSource({ store: context.Products, expand: [ "Category" ] });
The expand option is also supported by the byKey method.
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.
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.
context.invoke("MyAction", { param: "value" });
To invoke an operation and get its return value, use the get() method.
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.
var context = new DevExpress.data.ODataContext({ entities: { "GetSomeValue": { } } }); new DevExpress.data.DataSource({ store: context.GetSomeValue, // operation parameters customQueryParams: { operationParam: "value" } });
Custom Sources
Custom data access logic can be implemented using the CustomStore class. A developer should implement all data access operations in the CustomStore.
var myStore = new DevExpress.data.CustomStore({ load: function(loadOptions) { // . . . }, byKey: function(key, extra) { // . . . }, update: function(values) { // . . . }, . . . }); var dataSource = new DevExpress.data.DataSource({ store: myStore });
The DataSource supports a more brief syntax without introducing an explicit CustomStore instance.
var dataSource = new DevExpress.data.DataSource({ load: function(loadOptions) { // . . . }, byKey: function(key, extra) { // . . . }, update: function(values) { // . . . }, . . . });
For example, the following synthetic implementation generates an infinite read-only list:
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 instantly return a result. In the next example, we connect to a remote web service, and therefore functions 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.
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 function accepts 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 implemenation. For example, in case of the DataGrid, the load function should also return the total count of received records.
See Also
Load Data in Raw Mode
Loading data in raw mode allows you to configure the CustomStore more easily. You can use it only if all data shaping operations are supposed to be performed on the client. In raw mode, the load function should get raw, unprocessed data from the server, and the CustomStore will perform data shaping automatically, without any input from you. To switch to the raw mode, assign "raw" to the loadMode option.
var store = new DevExpress.data.CustomStore({ loadMode: "raw", load: function() { return $.getJSON("url/to/the/resource"); } });
Note that you are not required to implement the byKey and totalCount functions in raw mode, since they will be evaluated based on the results of the load function. If, however, you do implement them, your implementation will take precedence over the default one.
Once loaded, data is stored in the cache. If you need to clear the cache at some point, call the clearRawDataCache() method.
store.clearRawDataCache();
To switch data caching off, assign false to the cacheRawData option. Note that in this case, the CustomStore will reload all data on every call of the load, byKey and totalCount functions.
var store = new DevExpress.data.CustomStore({ // ... cacheRawData: false });
Since the CustomStore loads all data in raw mode at once, we do not recommend using it with large amounts of data. If you notice a decrease in the CustomStore performance in raw mode, consider delegating some or all data shaping operations to the server and implementing the remaining operations in the load function yourself.
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.
var store = new DevExpress.data.ODataStore({ url: "http://www.example.com", jsonp: true });
If you have technical questions, please create a support ticket in the DevExpress Support Center.