Data Layer

Overview

The DevExtreme framework includes a data layer, which is a set of complementary components that enable you to read and write data. The core data layer objects are DataSource and Stores.

The DataSource is intended to simplify working with data, whether it be an in-memory array, a local browser storage, remote service, or any custom storage. The DataSource is a stateful object that keeps sorting, grouping, filtering, and keeps data transformation options and applies them each time data is loaded. It also provides events intended to handle changing data and the state.

The DataSource underlying data access logic is isolated in a Store. Unlike the DataSource, a Store is a stateless object implementing a universal interface for reading and modifying data. Each Store contains the same set of methods. For more information about Stores refer to the What Are Stores section of this article.

The following picture illustrates how the DataSource and Stores interact.

Data Layer

See Also

You can find a number of step-by-step examples in the Data Layer Tutorials section.

Asynchrony

All Data transferring operations in the data layer are asynchronous, whether it be an access to a remote service or to local data. It is made to support the universal Store interface, regardless of the source of data you access. These operations return the jQuery.Deferred promise. Use methods of the returned promise to specify callbacks - done() for successful operation completion and fail() for operation failure.

JavaScript
dataSource.load()
    .done(function(result) {
        //success callback
    })
    .fail(function(error) {
        //error callback
    });

When implementing custom data access logic, you are not required to create the jQuery.Deferred object within a function implementing a data access operation. It is enough to return an object compatible with jQuery.Deferred.

Creating DataSource

To create a DataSource instance, call the DataSource constructor and pass the required configuration object to this constructor. This section covers the ways to associate the required data with the DataSource instance being created. You can use one of these options to create a DataSource instance.

From Array

Pass the required array to the DataSource constructor.

JavaScript
var dataSource = new DevExpress.data.DataSource(array);

If you need to specify other DataSource configuration options, use another form.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    // array of data
    store: array,

    // additional configuration options
    sort: "name",
    pageSize: 10
});

In this case, the array is passed to the store option of the configuration object.

From url

If you pass a string to the DataSource constructor, the DataSource will access the data returned by the AJAX request to the URL specified by this string.

JavaScript
var dataSource = new DevExpress.data.DataSource("http://www.example.com/dataservices/jsondata");

You may also use a JSONP callback parameter supported by jQuery.ajax().

JavaScript
var dataSource = new DevExpress.data.DataSource("http://www.example.com/dataservices/jsonpdata?callback=?");

From Store

Pass a Store instance to the DataSource constructor.

JavaScript
var store = new DevExpress.data.ArrayStore(data);
var dataSource = new DevExpress.data.DataSource(store);

If you need to specify other DataSource options, use the following code.

JavaScript
var store = new DevExpress.data.ArrayStore(array);
var dataSource = new DevExpress.data.DataSource({
    sort: "name",
    pageSize: 10,
    store: store
});

In this example, the Store instance is passed to the store configuration option.

A Store can be specified implicitly. Two possibilities are described above (From Array and From Url). In addition, you can use the following.

  • Add the properties implementing the required data access logic to the DataSource configuration object. In this case, the CustomStore will be automatically created within the DataSource. The properties should correspond to the CustomStore configuration options.

    JavaScript
    var dataSource = new DevExpress.data.DataSource({
        //DataSource configuration
        sort: "name",
        pageSize: 10,
        //data access logic
        load: function(loadOptions) {
            return array;
        },
        byKey: function(key) {
            return array[key];
        },
        ...
    });
  • Pass a Store configuration object. Besides the required Store configuration options, this object should contain the type property that specifies which Store object will be created within the DataSource.

    JavaScript
    var dataSource = new DevExpress.data.DataSource(
        store: {
            type: "array",
            data: array
        }
    });

    The following Store types are available.

What Are Stores

Store is a universal data access interface accepted within the DevExtreme data layer. It includes the following methods required for reading and editing data.

  • load(options) - Loads data. This function takes on an object specifying sorting, grouping, filtering and data transformation options.
  • insert(values) - Adds a new item with passed values.
  • remove(key) - Removes the data item specified by the key.
  • update(key, values) - Sets the new values to the data item specified by the key.
  • byKey(key, extraOptions) - Loads the data item specified by the key. The extraOptions argument takes on additional implementation options for getting the required item.
  • totalCount(options) - Enables you to get the total count of items that satisfy the specified conditions without loading them. The options object can contain filter and group fields that specify filtering and grouping options respectively.

DevExtreme includes the following Store implementations for the most common data access scenarios.

  • ArrayStore - provides access to an in-memory array
  • LocalStore - provides access to HTML5 web storage
  • ODataStore - provides access to a remote OData service
  • CustomStore - a Store that enables you to implement your own data access logic

You can easily implement your own Stores. As a reference, we created two custom Store implementations to support two well known data libraries: BreezeJS and JayData.

Reading Data

Consider data reading possibilities of the data layer on an example of an in-memory array.

JavaScript
var dataSource = new DevExpress.data.DataSource([
    { name: "item1" },
    { name: "item2" },
    { name: "item3" }
]);

The information given in this section is applicable to any data source. We use in-memory data for the sake of simplicity.

To load data, call the dataSource.load() method.

JavaScript
dataSource.load()
    .done(function(result) {
        // 'result' contains the array associated with the DataSource
    })
    .fail(function(error) {
        // handle error
    });

The DataSource allows you to specify initial data shaping options (sort, filter, etc.).

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: [
        { name: "Charlie", value: 10 },
        { name: "Alice", value: 20 },
        { name: "Bob", value: 30 }
    ],
    filter: [ "value", ">", 15 ],
    sort: { field: "name", desc: true }
});

In the ArrayStore, LocalStore and ODataStore data shaping operations are applied in the following order: Filtering -> Sorting -> Selection -> Grouping -> Paging -> Item mapping -> Post processing.

As for the CustomStore, data shaping options are passed to the load(options) method, which gives you an opportunity to process them in any order you need.

Paging

By default, the DataSource loads items by pages. Page size is determined by the pageSize configuration option of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    pageSize: 30,
    . . .
});

The current page index can be accessed and modified via the pageIndex() and pageIndex(newIndex) methods respectively.

JavaScript
dataSource.pageIndex(1); // Switch to the next page
dataSource.load();

To disable paging, assign false to the paginate configuration option of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    paginate: false,
    . . .
});
See Also

See a step-by-step example in the Paging Tutorial.

Sorting

DataSources and Stores use sort expressions to specify sorting conditions and direction.

For an example, declare a Person class.

JavaScript
var Person = function(firstName, lastName, city) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.address = {
        city: city
    };
};
Person.prototype.fullName = function() {
    return this.firstName + " " + this.lastName;
};

After the Person class is created, declare a data array holding several instances of this class.

JavaScript
var data = [
    new Person("John", "Smith", "San Francisco"),
    new Person("John", "Doe", "New York"),
    new Person("Xavier", "Gomez", "Denver"),
    new Person("Xavier", "Lee", "New Mexico")
];

Create a simple DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource(data);

The sorting condition can be specified using the sort configuration option or the sort(sortExpr) method of the DataSource. The value assigned to the option or passed to the method is a sorting expression, which can have one of the following formats.

  • A single expression

    JavaScript
    dataSource.sort("lastName");
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'lastName'
    });

    To sort data in the descending order, pass an object containing the getter (field or selector - they are equivalents) and desc properties to the sort method.

    JavaScript
    dataSource.sort({ getter: "lastName", desc: true });
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'lastName' in the descending order
    });
  • Several expressions

    JavaScript
    dataSource.sort(
        "fullName", 
        { getter: "address.city", desc: true }
    );
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'fullName' and then by 'address.city' in the descending order
    });

    You can pass an array of expressions instead of using several arguments.

    JavaScript
    dataSource.sort([
        "firstName",
        "lastName",
        { getter: "address.city", desc: true }
    ]);
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'firstName', then by 'lastName',
        // and then by 'address.city' in the descending order
    });

Stores support the same sort expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        sort: [
            { getter: "firstName", desc: true },
            "lastName"
        ]
    })
    .done(function (result) {
        // 'result' contains the 'data' array items sorted by 'firstName' in the descending order and then by 'lastName'
    });
See Also

See a step-by-step example in the Sorting Tutorial.

Filtering

To consider the filter expression syntax, begin by creating a sample DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource([
    { name: "First item", value: 5 },
    { name: "Second item", value: 7 },
    { name: "Last item", value: 3 }
]);

Filtering conditions are specified as a filter expression, which is a set of binary filter expressions separated by group operators. The filtering conditions can be defined via the filter configuration option or the filter(filterExpr) method of the DataSource.

See Also

See a step-by-step example in the Filtering Tutorial.

Binary Filter Operations

To specify a binary filter operation, use an array containing the following items.

  1. The getter by which data items are filtered.
  2. The comparison operator. The available operators are: "=", "<>", ">", ">=", "<", "<=", "startswith", "endswith", "contains", "notcontains".
  3. The value to which the getter value is compared.

To specify filtering conditions, call the filter(filterExpr) method of the DataSource. If a filter expression consists of a single binary operation, you can either pass an array of the items described above, or you can pass those items as separate arguments.

JavaScript
dataSource.filter("value", ">", 3);
dataSource.load().done(function(result) {
    //'result' contains the "First item" and "Second item" items
});
JavaScript
dataSource.filter("name", "contains", "st");
dataSource.load().done(function(result) {
    //'result' contains the "First item" and "Last item" items
});

The second item of the filter expression array is optional. You can specify only the getter and the value. The default operator is "=".

JavaScript
dataSource.filter("value", "=", 3);

is equal to

JavaScript
dataSource.filter("value", 3);

Group Filter Operations

To combine several binary operations, use group operators "and" or "or".

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    "and",
    [ "value", "<", 7 ]
]);
dataSource.load().done(function(result) {
    //'result' contains the "First item" item
});
JavaScript
dataSource.filter([
    [ "value", "<", 4 ],
    "or",
    [ "value", ">", 6 ]
]);
dataSource.load().done(function(result) {
    //'result' contains the "Second item" and "Last item" items
});

If neighboring filter expressions are not separated by a group operator, the "and" operator is implied.

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    "and",
    [ "value", "<", 7 ]
]);

is equal to

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    [ "value", "<", 7 ]
]);

The operator priority depends on the implementation of the underlying Store, or you can define the operator priority enclosing the required expression into square brackets.

JavaScript
dataSource.filter([
    ["name", "notcontains", "Second"],
    "and", //calculated second
    [
        [ "value", "<", 4 ],
        "or", //calculated first
        [ "value", ">", 6 ]
    ]
]);

Custom Filter Conditions

A filter expression can also be a function that takes on an item object and returns a Boolean value that determines whether an item satisfies the filtering condition or not.

JavaScript
dataSource.filter(function(itemData) {
    return itemData.value > 3 && itemData.name != "Second item";
});
dataSource.load().done(function(result) {
    //'result' contains the "First item" item
});

Stores support the same filter expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        filter: [
            [ "value", "<", 5 ],
            "or",
            [ "value", ">", 5 ]
        ]
    })
    .done(function (result) {
        // 'result' contains the "Second item" and "Last item" items
    });

Search Api

In addition to filtering capabilities, the DataSource provides the search API. It is a convenient way to augment filtering by text search. Search API consists of three options: searchExpr, searchOperation, and searchValue.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: [
        { firstName: "John", lastName: "Doe", birthYear: 1970 },
        . . .
    ],
    filter: [ "birthYear" , "<", 2000 ],
    searchExpr: function(dataItem) {
        return dataItem.firstName + " " + dataItem.lastName;
    }
});

dataSource.searchValue("doe");
dataSource.load().done(function(result) {
    //'result' contains items whose birthYear is less then 2000 and firstName or lastName contain "doe"
});

The default search operation is "contains". It can be changed by providing the searchOperation configuration option of the DataSource. You can also modify the search API options via the appropriate methods: searchExpr(expr), searchOperation(op), and searchValue(value).

See Also

See a step-by-step example in the Searching Tutorial.

Data Transformation

The DataSource includes several ways to customize the resulting array structure, which allows you to modify each item as well as process the entire array.

Select Expressions

The DataSource supports the select option intended to specify the structure of an item of the array being loaded. The DataSource passes the value of this option to the underlying Store when loading data. Therefore, this data transformation can be performed on the server side if the Store supports it.

To consider a select expression syntax, begin by creating an array of sample data.

JavaScript
var data = [
    {
        firstName: "John",
        lastName: "Smith",
        city: "San Francisco"
    },
    {
        firstName: "Xavier",
        lastName: "Lee",
        city: "New York"
    },
    {
        firstName: "Maria",
        lastName: "Gomez",
        city: "Denver"
    }
];

You can define the select option value via the configuration object passed to the DataSource constructor.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: data,
    select: "lastName"
});

To modify the select option, pass the new option value to the select(expr) method of the DataSource.

JavaScript
dataSource.select("firstName", "city");
dataSource.load();

A selection expression can be a getter, several getters, or a function.

  • A single getter

    JavaScript
    dataSource.select("lastName");
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { lastName: "Smith" },
        { lastName: "Lee" },
        { lastName: "Gomez" },
    ]
  • Several getters

    JavaScript
    dataSource.select("firstName", "lastName");
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { firstName: "John", lastName: "Smith" },
        { firstName: "Xavier", lastName: "Lee" },
        { firstName: "Maria", lastName: "Gomez" },
    ]

    You can pass an array of getters instead of using several arguments.

    JavaScript
    dataSource.select(["firstName", "lastName"]);
  • A function

    A select expression can also be a function that takes on an item object and returns the transformed object.

    JavaScript
    dataSource.select(function(dataItem) {
        return {
            fullName: dataItem.firstName + " " + dataItem.lastName,
            address: dataItem.city
        };
    });
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { fullName: "John Smith", address: "San Francisco" }
        { fullName: "Xavier Lee", address: "New York" }
        { fullName: "Maria Gomez", address: "Denver" }
    ]

Stores support the same select expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        select: [ "firstName", "lastName" ]
    })
    .done(function (result) {
        //'result' contains the resulting array
    });

Additional data transformation can be performed with the map and postProcess configuration options of the DataSource.

Item Mapping

The map configuration option of the DataSource enables you to modify each item of the loaded array. A function passed to this option takes an initial item as a parameter and returns the processed item. This function is performed within the DataSource and is not passed to the underlying Store.

Begin with the creation of a sample array.

JavaScript
var data = [
    {
        firstName: "John",
        lastName: "Smith",
        city: "San Francisco"
    },
    {
        firstName: "Xavier",
        lastName: "Lee",
        city: "New York"
    },
    {
        firstName: "Maria",
        lastName: "Gomez",
        city: "Denver"
    }
];

Assign the required function to the map configuration option of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: data,
    map: function(itemData) {
        return {
            fullName: itemData.firstName + " " + itemData.lastName,
            address: {
                city: itemData.city
            }
        }
    }
});

The dataSource in the example above will convert the initial data items into the following ones.

[
    {
        fullName: "John Smith",
        address: {
            city: "San Francisco"
        }
    },
    {
        fullName: "Xavier Lee",
        address: {
            city: "New York"
        }
    },
    {
        fullName: "Maria Gomez",
        address: {
            city: "Denver"
        }
    }
]

Post Processing

You may often need to process loaded data in a way, which does not include simple sorting, grouping, filtering or data item transformation. For instance, you may need to group items by a date they were created (Today, This week, Long time ago). The postProcess configuration option of the DataSource will help you perform this task. Assign a function that implements the required post processing algorithm to this option. The function takes on the loaded array as a parameter and returns the processed array.

The following example illustrates how to perform the task mentioned above using the postProcess option.

var data = [
    { 
        subject: "First message", 
        message: "This message has been recieved at 12/09/2013", 
        date: new Date(2013, 08, 11) 
    },
    { 
        subject: "Today message", 
        message: "This message has been recieved today",
        date: new Date() 
    },
    ...
];

Create a DataSource instance and assign the required function to the postProcess configuration option.

JavaScript
function groupByDate(data) {
    var MS_PER_DAY = 24 * 60 * 60 * 1000,
        result = [],
        todayItems = [],
        thisWeekItems = [],
        otherItems = [];

    // Fill intervals
    $.each(data, function() {
        var daysAgo = ($.now() - this.date.getTime()) / MS_PER_DAY;

        if(daysAgo < 1)
            todayItems.push(this);                
        else if(daysAgo < 7)
            thisWeekItems.push(this);                
        else
            otherItems.push(this);                
    });

    // Construct final result
    if(todayItems.length)
        result.push({ key: "Today", items: todayItems });

    if(thisWeekItems.length)
        result.push({ key: "This week", items: thisWeekItems });

    if(otherItems.length)
        result.push({ key: "Long time ago", items: otherItems });

    return result;
}

var dataSource = new DevExpress.data.DataSource({
    store: data,
    postProcess: groupByDate
});

The DataSource in the example above groups the items by the time period. The resulting array will look like the following.

[
    {
        key: "Today",
        items: [
            //today items
        ]
    },
    {
        key: "This week",
        items: [
            //this week items
        ]
    },
    {
        key: "Long time ago",
        items: [
            //items elder then a week
        ]
    }
]

NOTE: The function passed to the postProcessed option is performed within the DataSource and is not passed to the underlying Store.

Grouping

In some cases, you may need to group data by certain criteria. An item of a grouped array is an object containing two fields:

  • key - a group key;
  • items - an array of items belonging to the group.
JavaScript
var groupedArray = [
    {
        key: "A",
        items: [ "Amelia", "Andrew"]
    }        
    {
        key: "B",
        items: [ "Betty", "Benjamin"]
    }
];

The DevExtreme data layer (DataSource and Stores) supports grouping. The group expression syntax is identical to the sort expression syntax.

Consider grouping on the following examples. Begin with the creation of a sample DataSource.

JavaScript
var data = [
    { name: "Amelia", birthYear: 1991, gender: "female" },
    { name: "Benjamin", birthYear: 1983, gender: "male" },
    { name: "Andrew", birthYear: 1991, gender: "male" },
    { name: "Daniela", birthYear: 1983, gender: "female" },
    { name: "Lee", birthYear: 1983, gender: "male" },
    { name: "Betty", birthYear: 1983, gender: "female" }
];
var dataSource = new DevExpress.data.DataSource(data);

The group expression is stored in the group option of the DataSource. You can get and modify the option value via the group() and group(groupExpr) methods respectively. To group the given array by 'birthYear', call the group(groupExpr) method and pass "birthYear" as an argument.

JavaScript
dataSource.group("birthYear");
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

The return value of the function that processes the loaded data contains the following array.

[
    {
        key: 1983,
        items: [
            { name: "Benjamin", birthYear: 1983, gender: "male" },
            { name: "Daniela", birthYear: 1983, gender: "female" },
            { name: "Lee", birthYear: 1983, gender: "male" },
            { name: "Betty", birthYear: 1983, gender: "female" }
        ]
    },
    {
        key: 1991,
        items: [
            { name: "Amelia", birthYear: 1991, gender: "female" },
            { name: "Andrew", birthYear: 1991, gender: "male" }
        ]
    }
]

To group by custom criteria, pass a function to the group(groupExpr) method.

JavaScript
dataSource.group(function(dataItem) {
    return dataItem.birthYear < 1990 ? "Born before 1990" : "Born after 1990";
});
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

In this case, the 'result' array includes the following items.

[
    {
        key: "Born after 1990",
        items: [
            { name: "Amelia", birthYear: 1991, gender: "female" },
            { name: "Andrew", birthYear: 1991, gender: "male" }
        ]
    },
    {
        key: "Born before 1990",
        items: [
            { name: "Benjamin", birthYear: 1983, gender: "male" },
            { name: "Daniela", birthYear: 1983, gender: "female" },
            { name: "Lee", birthYear: 1983, gender: "male" },
            { name: "Betty", birthYear: 1983, gender: "female" }
        ]
    }
]

Multi-level grouping is also supported. Pass several getters or an array of getters to the group(groupExpr) method to load an array containing subgroups.

JavaScript
dataSource.group("birthYear", "gender");
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

The loaded array looks like the following.

[
    {
        key: 1983,
        items: [
            {
                key: "female",
                items:[
                    { name: "Daniela", birthYear: 1983, gender: "female" },
                    { name: "Betty", birthYear: 1983, gender: "female" }
                ]
            },
            {
                key: "male",
                items:[
                    { name: "Benjamin", birthYear: 1983, gender: "male" },
                    { name: "Lee", birthYear: 1983, gender: "male" }
                ]
            }
        ]
    },
    {
        key: 1991,
        items: [
            {
                key: "female",
                items:[
                    { name: "Amelia", birthYear: 1991, gender: "female" }
                ]
            },
            {
                key: "male",
                items:[
                    { name: "Andrew", birthYear: 1991, gender: "male" }
                ]
            }
        ]
    }
]

Stores support the same group expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        group: "birthYear"
    })
    .done(function (result) {
        // 'result' contains the 'data' array grouped by the 'birthYear' property value.
    });
See Also

See a step-by-step example in the Grouping Tutorial.

Data Modification

For read-write access to data, use a Store directly in addition to the DataSource. You have two opportunities to obtain a Store instance.

Create a Store instance explicitly.

JavaScript
var store = new DevExpress.data.ArrayStore({
    key: "id",
    data: [
        {
            id: 1,
            value: "value 1"
        },
        {
            id: 2,
            value: "value 2"
        }
    ]
});

Note that it is necessary to specify the key expression to identify data items.

To load data from this Store either call store.load(loadOptions) or wrap this Store with the DataSource.

If you already have an existing DataSource, you can obtain the underlying Store instance via the store() method.

Insertion

To insert a new item to the Store, call the store.insert(values) method.

JavaScript
store.insert({
        id: 3,
        value: "value 3"
    })
    .done(function(values, key) {
        //handle success
    })
    .fail(function(error) {
        //handle error
    });

Some Stores can generate a key value if you have not specified it in the values object. You may access it via the key argument of the done callback.

Update

To update a data item specified by the key, use the store.update(key, values) method.

JavaScript
store.update(1, { value: "new value" })
    .done(function(values) {
        //handle successfull updating
    })
    .fail(function(error) {
        //handle error
    });

Note that the second argument of the update(key, values) method contains only the properties whose values should be changed in the data item, but not the entire item, because it will be merged with the original item object.

In a common scenario, before updating, you need to load the required item using the store.byKey(key) method.

Removing

To remove a data item, call the store.remove(key) method.

JavaScript
store.remove(1)
    .done(function(key) {
        //handle success
    })
    .fail(function(error) {
        //handle error
    });

Events and Change Tracking

DataSource and Stores support events. The DataSource events are used to track the data loading state. They are used internally by the DevExtreme widgets. You can also use them for your purposes. Stores also raise events before and after each operation (loading, update, insertion, etc.).

Use one of the following approaches to handle events.

Assign a Handler to a Configuration Option

To attach a handler for a certain event to a DataSource or Data Store, use the corresponding configuration option. The options that take on event handling functions have names starting with on.

The following example demonstrates how to use a configuration option to handle an event.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    sort: "name",
    pageSize: 10,
    store: store,
    onChanged: function() {
        // Executed after the DataSource has successfully loaded data
    }
});

Attach Several Handlers Using one Method

To attach several event handlers to a DataSource or Data Store, use the on method. This method has two overloads.

  • on(eventName, eventHandler)
    This method allows you to provide several handlers for an event.

    JavaScript
    dataSource
        .on("changed", handler1)
        .on("changed", handler2)
  • on(events)
    This method allows you to subscribe to several events with one method call.

    JavaScript
    dataSource
        .on({
            "changed": handler1,
            "loadError": handler2
        })

You can find a full list of events in the Events section: DataSource events and Store events (for example, ArrayStore events).

To unsubscribe from the events that you handled using the on() method, use the off() method. This method has two overloads.

  • off(eventName, eventHandler)
    This method allows you to detach a specific event handler.

    JavaScript
    dataSource
        .off("changed", handler1)
        .off("changed", handler2)
  • off(eventName)
    This method allows you to detach all event handlers from a specific event.

    JavaScript
    dataSource
        .off("changed")

Example

Usually, a data-bound application contains a list view displaying list data, and an edit view used to edit a data item. Both these views access the same shared Store. After you update a Store item modified in the edit view, the Store should inform the list view that the data has been modified. For this purpose, among other events the Store supports the modified event, which is raised after each data modification. To automatically update the list view, you may subscribe to this event, which enables you to determine whether or not the data should be reloaded.

Store Events

The following example illustrates how to implement a ViewModel for a list view in a DevExtreme application. In this example, when the modified event is raised, the shouldReload flag is set to true, which causes the reloading of data when the list view is being shown.

JavaScript
// A shared Store declaration
MyApplication.store = new DevExpress.data.CustomStore(...);

// The list view model declaration
MyApplication.list = function() {

    // A flag indicating whether or not data needs to be reloaded.
    var shouldReload = false,
        // A DataSource associated within the shared Store.
        dataSource = new DevExpress.data.DataSource(MyApplication.store);

    // A function handling the 'modified' event of the Store
    function handleStoreModification() {
        shouldReload = true;
    }

    // Subscribe to the 'modified' event of the Store
    MyApplication.store.on("modified", handleStoreModification);

    // A function handling the 'viewShown' event of the view
    function handleViewShown() {
        if(shouldReload) {
            shouldReload = false;
            dataSource.load();
        }
    }

    // A function handling the 'viewDisposing' event of the view
    function handleViewDisposing() {
        //Unsubscribes from the modified event of the Store.
        MyApplication.store.off("modified", handleStoreModification);
    }

    // Exports the list view model
    return {
        dataSource: dataSource,
        viewShown: handleViewShown,
        viewDisposing: handleViewDisposing
    };
}

Handling Errors

DevExtreme provides the errorHandler utility property to help you handle errors that occur within the entire data layer. Assign the error handling function to this property. This function takes on the JavaScript Error object as a parameter.

JavaScript
DevExpress.data.errorHandler = function(error) {
    console.log(error.message);
};

A Store enables you to handle errors that occur only within itself. To handle Store errors, assign an error handling function to the handleError configuration option of the Store.

JavaScript
var store = new DevExpress.data.LocalStore({
    name: "MyStore",
    errorHandler: function(error) {
        console.log(error.message);
    }
};

DevExtreme also provides an ability to handle errors that occur during operation execution. All DevExtreme data transfer operations are asynchronous and return the jQuery.Deferred promise object allowing you to specify both success and error callbacks. To handle the operation error, pass the error handling function to the fail() method of the returned object.

The following example illustrates how to handle errors that occurred during the execution of the load operation of a DataSource.

JavaScript
dataSource.load()
    .done(function(result) {
        // process result
    })
    .fail(function(error) {
        console.log(error.message);
    });

Query Concept

Besides DataSource and Stores, DevExtreme data layer has one more useful concept named Query. Query is an abstract chainable interface which provides functionality to evaluate data queries.

There are several implementations of this interface, and they are used internally in some Stores as part of the load() method. While Query is considered internal instrument, one implementation, which is a wrapper over a JavaScript array, is particularly interesting, and can be used in the application code.

Here is an example:

JavaScript
var processedArray = DevExpress.data.query(inputArray)
    .filter([ [ "value", ">=", 10 ], "and", [ "value", "<=", 90 ]])
    .sortBy("lastName")
    .thenBy("firstName")
    .select("lastName", "firstName", "value")
    .toArray();

The full list of supported methods is given in the Query API reference.

Array Query can come handy when implementing a CustomStore. Consider an example of connecting to a remote web service which does not support filtering on its side, so it is done in the JavaScript code.

JavaScript
var store = new DevExpress.data.CustomStore({
    load: function(loadOptions) {
        var deferred = $.Deferred();

        // load data from the remote service
        $.getJSON("http://www.example.com/service.url").done(function(data) {

            // perform filtering on the client side
            var query = DevExpress.data.query(data);                
            if(loadOptions.filter)
                query = query.filter(loadOptions.filter);

            deferred.resolve(query.toArray());
        });

        return deferred.promise();
    }    
});

Getters And Setters

One of the basic data layer concepts is a Getters and Setters concept. Getters are essential tools used to specify sorting, grouping, filtering, and data transformation rules.

A getter is a function that returns a value of the predefined property of an object passed to the getter as a parameter. A setter is a function that assigns the new value to the predefined property of an object passed to the setter as a parameter. The new value is specified via the second parameter of a setter.

Create the person object to consider how to get and set its property values using getters and setters.

JavaScript
var person = {
  firstName: "John",
  lastName: "Smith",
  birthDate: new Date(1985,10,15),
  getBirthYear: function(){
    return this.birthDate.getFullYear();
  },
  address: {
    zipCode: 90007,
    state: "CA",
    city: "Los Angeles",
    getAddress: function(){
      return this.zipCode + " " + this.state + " " + this.city;
    }
  }
};

Creating Getters And Setters

DevExtreme includes methods used to create a getter and setter for the property specified as a string.

JavaScript
var getter = DevExpress.data.utils.compileGetter("firstName");
var setter = DevExpress.data.utils.compileSetter("firstName");

NOTE: In this section, we will use the compileGetter and compileSetter utility functions to demonstrate the use of getters and setters separately. However, in practice you do not need to use them when specifying sorting, filtering, etc. Use only the values passed to these methods. They too should be named "getter" and "setter".

Consider some examples to understand how getters and setters work.

To get a value of the person.firstName property, call the created getter and pass the person object to it.

JavaScript
// the getter returns "John" and assigns it to the 'name' variable
var name = getter(person); 

To assign a new value to the person.firstName property, call the created setter and pass the person object and the new value to it.

JavaScript
// assigns "Michael" to 'person.firstName'
setter(person, "Michael"); 

The property for which you want to create a getter or a setter is specified via a string passed to the compileGetter or the compileSetter method. The property string can have one of the following formats.

  • A property name
    Specifies a top-level property.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("firstName");
    var setter = DevExpress.data.utils.compileSetter("firstName");
    // the getter returns "John" and assigns it to the 'name' variable
    var name = getter(person);
    // assigns "Michael" to 'person.firstName'
    setter(person, "Michael");
  • A second and higher level property
    Specifies a second and higher level property.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("address.city");
    var setter = DevExpress.data.utils.compileSetter("address.city");
    // the getter returns "Los Angeles" and assigns it to the 'city' variable
    var city = getter(person); 
    // assigns "San Francisco" to 'person.address.city'
    setter(person, "San Francisco"); 
  • A method name
    Specifies the method whose value should be returned by the getter.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("getBirthYear");
    // the getter returns 1985 and assigns it to the 'year' variable
    var year = getter(person); 
  • An array of property and method names
    If you pass an array of strings to the compileGetter function, the compiled getter will return an object containing the specified properties. If an item of the array passed to compileGetter represents a second or higher level property, the getter will create the appropriate subobjects within the returned object.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter(["firstName", "lastName", "address.city", "getBirthYear"]);
    var personData = getter(person); 

    The specified getter called for the person object will return the following object.

    {
        firstName: "John",
        lastName: "Smith",
        getBirthYear: 1985,
        address: {
            city: "Los Angeles"
        }
    }

    Note that the returned object contains the getBirthYear property that holds the value returned by the method with the same name.

  • A function
    If you pass a function to compileGetter, this function will be executed each time the getter is called. The function should have a parameter that takes on an object passed to the getter.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter(function(dataItem) {
      return dataItem.firstName + " " + dataItem.lastName;
    });
    // the getter returns "John Smith" and assigns it to the 'fullName' variable
    var fullName = getter(person); 
  • "this"
    In this case, the getter returns the object itself. This getter is used to sort or group an array of items containing a simple value.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("this");
    // the getter returns "A text" and assigns it to the 'text' variable
    var text = getter("A text"); 

Getter And Setter Calling Options

If you assign the name of a method to a compileGetter, the getter normally returns the value returned by this method. If you need to get a reference to the method instead of its value, pass an object with the functionAsIs property set to true to the getter as a second parameter.

JavaScript
var getter = DevExpress.data.utils.compileGetter("address.getAddress");
// the getter returns a reference to the 'person.address.getAddress' function and assigns it to the 'getAddressFunction' variable
var getAddressFunction = getter(person, { functionsAsIs: true }); 

If the getter returns an object containing several functions, the functionsAsIs option affects all functions contained in this object.

When you use a setter to assign a new value to a property containing an object, the object held in the property is replaced by the new one by default.

JavaScript
var setter = DevExpress.data.utils.compileSetter("address");
setter(person, {
    city: "San Francisco",
    street: "Stanford Ave"
});

In the example above, the object held in the person.address property will contain only the city and street properties after the setter will be called. Other members will be lost.

If you need to merge the new object passed to the setter with the object held in the property, pass an options object containing the merge property set to true to the setter.

JavaScript
var setter = DevExpress.data.utils.compileSetter("address");
setter(
    person, 
    {
        city: "San Francisco",
        street: "Stanford Ave"
    }, 
    { merge: true }
);

In this case, the address object will contain the following properties.

{
    zipCode: 90007,
    state: "CA",
    city: "San Francisco",
    street: "Stanford Ave",
    getAddress: function() {
      return this.zipCode + " " + this.state + " " + this.city;
    }
}