Many applications display lots of selectable forms (drop-down lists, comboboxes, etc., etc.) each populated with a list of options whose values come from the database.
You know you'll need these "lookup lists" almost immediately. They tend to be static anyway so why not cache them? The colors, status codes, units-of-measure are unlikely to change during a user's session.
You'd like to retrieve them from the database and cache them when the application starts.
Unfortunately, there could be a lot of them; a typical medium-sized application might have twenty or thirty. You want the application to start quickly and twenty or thirty separate calls to the server might take too long even if you can make those calls asynchronously in parallel. Each list is short. It would be nice to fetch all of the lists in one shot. Here's how.
Find the example code for this topic in the queryTests module of the DocCode sample
Let's assume that you're using a Breeze Web API controller on the server and that it's talking to the Entity Framework.
These assumptions aren't important. What matters is that you can create a single method on the server that returns an object whose properties are the lookup lists. The object can be anonymous; the list elements should be entities known to the client.
The NorthwindController in the DocCode sample is a Breeze Web API controller with such a method:
[HttpGet] public object Lookups() // returns an object, not an IQueryable { var regions = _contextProvider.Context.Regions; var territories = _contextProvider.Context.Territories; var categories = _contextProvider.Context.Categories; return new {regions, territories, categories}; }
It returns an anonymous object with properties initialized by queries for three different entity lists {regions, territories, categories} ; serialization of the object triggers execution of the queries, populating the properties.
Someday you may think about caching the serialized "lookups" object in the cloud but this approach will do for now.
Let's keep it simple on the client. We'll assume you have datacontext with a single EntityManager. Let's add a getLookups method:
var getLookups = function () { return EntityQuery .from('Lookups') .using(manager) .execute() .then(querySucceeded) .catch(queryFailed); };
This is just like other Breeze query methods you've written before ... except, perhaps, for the from(...) clause. You're used to seeing the name of an entity collection as the argument; a name such as "Customers" or "Orders" would be typical. Here the argument is 'Lookups" ... the name we gave to the GET action method on the controller [1]. Here is querySucceeded:
function querySucceeded(data) { datacontext.lookups = data.results[0]; // datacontext was defined earlier in the module. }
The first element of the query results is a JavaScript object representing the anonymous object from the server. It has three properties, {regions, territories, categories}, each returning an array of Region, Territory and Category entities.
These are real entities in the manager cache. The lookups object that holds these lists is not an entity; it's an arbitrary JavaScript object. Breeze took a look at it on the way in and didn't recognize it. Breeze doesn't mind; it just passes it along in the query result ... in the same way that it returns the results of a projection query. But before returning it, Breeze inspects its contents. In each of the three list properties Breeze finds instances of entity types that it recognizes from metadata. Remember what we stipulated early on: the members of the lookup lists are entities on the server and therefore (thanks to metadata) they are entities on the Breeze client.
Here's a live code, AngularJS version of what we're talking about .
The following "plunker" only works with modern browsers (IE10+, FF, Chrome).
You might want to fetch these lookup lists before you release the UI to user input. Start by adding an initialize method [2] to your datacontext like the following:
var datacontext = { ... lookups: null, // set by private getLookups method initialize: initialize, ... } function initialize() { return getLookups(); }
You might start your application with a bootstrapper which waits until datacontext initialization is finished before setting up the UI
dataservice.initialize() .then(go) .catch(failedInitialization); function go() { // setup views, view models, routing, etc. // and start the magic }
Now you have primed your datacontext (and your application) with the lists you'll need throughout the user session. Your ViewModels can get them from the datacontext and deliver them to their Views as in this Knockout-inspired example:
app.vm.productEditor = (function (datacontext) { var vm = { product: ko.observable(), categories: ko.observableArray(); activate: activate, ..., }; ... return vm; function activate() { ... vm.categories(datacontext.lookups.categories); } })(app.datacontext);
... which is later bound to a combobox (an HTML select element) as seen in this fragment from a productEditorView
<div data-bind="with: product"> <input data-bind="value:name" /> <label>Category</label> <select data-bind="options: $parent.categories, optionsText: 'categoryName', value: category" </select> ... </div>
[1] You may have thought that the string argument had to be the name of an entity collection. Not so. An entity collection name is the conventional argument to a from(...) clause. What matters is that it match the name of a GET action method on your controller. Look again at the NorthwindController and you'll find the Customers and Orders methods. We could have called them Bob and Sue to demonstrate our fine sense of humor. We restrained ourselves and stuck with conventional naming.
[2] Sometimes you have more than one asynchronous operation to perform before the datacontext initialization is complete. These operations might proceed in sequence:
var initialize = function () { return doFirst() .then(getLookups) .then(doLast); };
... or you might be able to do them in parallel:
var initialize = function () { return Q.all([getLookups(), doSomethingInParallel()]); };
... or you can write any combination of the above. Whatever you do, you return a single promise which, when fully and successfully resolved, signals that datacontext initialization is complete.