The Edmunds "Not My Server" Sample

BreezeJS is a pure JavaScript library that can manage data from any HTTP source. This sample application reads the "Make" and "Model" data from the Edmunds.com Vehicle Information service and translates those data into Breeze entities.

  • no ASP.NET
  • no OData or Web API
  • no EntityFramework
  • no SQL Server
  • no metadata from the server
  • no IQueryable

Just some HTML and JavaScript on the client talking to a public 3rd party API that is completely out of our control.

You’ll find “Edmunds” among the samples in the full download.

Prep It

The breeze scripts are not included directly in this sample. You must copy them into the sample before you run it.

Windows users can simply execute the getLibs.cmd command file which copies the files from a source directory in the samples repository into the scripts folder.

Not a Windows user? It's really simple.

  • Open the getLibs.cmd command file in a text editor.
  • Notice that it copies a small number of source files, starting from  ..\..\build\libs\, into the  scripts  folder.
  • Do the same manually.

Run it

Locate the index.html file. Double-click it. The app loads in your default browser.

The app presents a list of automobile "Makes" returned by a request to the Edmunds.com Vehicle Information service API.

Click a checkbox next to a "Make", and the application fetches the related "Model" information from the Edmunds model service. In the following image, the app displays the "Aston Martin" models, their names, sizes, and styles.

Edmunds sample running

Toggling the checkbox will show and hide the "Models". The app only gets the Model data for a Make the first time; subsequent displays are filled from Model entities in the Breeze EntityManager cache.

A pop-up toast in the lower right corner tells you if the app requested data from the service or retrieved the Models from cache.

Type a few letters in the "Filter by Make ..." text box; the app reduces the list to just the makes with that letter combination somewhere in the name.

The app is deliberately super-simple. We want you to concentrate on what it takes to get data from 3rd party data sources and turn them into Breeze entity graphs.

Once you have entities, you can apply all of your Breeze knowledge to insert, edit, and delete them. Breeze raises property change events, tracks changes, and validates as it does for entities from any other source.

Dependencies

This is a pure client-side sample. Yes it's shipped as a Visual Studio ASP.NET web project for the convenience of our many .NET Breeze developers. But that doesn't really matter. You can grab and relocate the HTML (index.html), CSS (Content) and JavaScript (app, Scripts) files, edit them with the IDE of your choice, and host them wherever you wish. You can ignore the Web.config.

The only 3rd party dependencies are JavaScript libraries:

And breeze.js of course.

However, this app only includes the Breeze JavaScript files which we extracted from the runtime zip. We did not install the breeze.webapi NuGet package because we only want the JavaScript; we don't need any of the Breeze.NET components.

Inside the app

Everything of interest is in one HTML file and five JavaScript application files.

HTML

The index.html file loads CSS, 3rd party libraries, and the application scripts.

The UI is a single "view" defined within the <body> tag. The HTML is marked up with AngularJS.js binding "directives":

<div>
  <input class="filter" data-ng-model="searchText" type="search" placeholder="Filter by Make ..." />
</div>
<div data-ng-show="!makes.length" class="make">... loading makes ...</div>
<div data-ng-repeat="make in makes | filter:makeFilter" class="make">
    <input type="checkbox" data-ng-model="make.showModels" data-ng-click="getModels(make)" />
    
   <span data-ng-show="make.isLoading"> ... (loading models)...</span>
    <table data-ng-show="make.showModels" class="models">
        <tr data-ng-repeat="model in make.models | orderBy: 'name'">
            <td class="modelName"></td> 
            <td class="modelSize"></td>
            <td class="modelStyle"></td>
        </tr>
    </table>
</div>

Highlights:

  • Filter textbox bound to the controller's searchText property.

  • Message text displayed while loading "Make" data, bound to the length of the controller's makes array.

  • A "repeater" <div> enclosing a template for each "Make" in the makes array.

  • A checkbox for toggling the display of "Models" for the given "Make". It's bound to the make's showModels field.

  • An AngularJS "interpolation" to show the name of the "Make"

  • A table to display the "Models" for the current make

  • A "repeater" table row for each Model; the interpolations for model property appear in each cell.

This is standard AngularJS fare.

Application JavaScript

The client-side experience is driven by five short application JavaScript files. Each file is dedicated to a simple purpose:

controller - Display vehicle data in the view and respond to user gestures (clicks and key strokes). The controller delegates data access chores to the datacontext.

datacontext - Uses Breeze to request "Make" and "Model" data from the Edmunds service and turn those data into AngularJS-ready entities. It delegates to model.js for metadata about make and model entity types. It requires the jsonResultsAdapter.js to transform Edmunds data into instances of those types

model - Defines make and model entity types using the Breeze metadata API.

jsonResultsAdapter - Tells breeze how to convert the JSON results returned from the Edmunds service into make and model entity instances.

logger - Logs application activity messages to the console log and to "toastr" which presents those messages as color-coded "toasts" rising from the lower right corner of the screen.

Let's look at some highlights from selected scripts.

controller.js

This file defines the application module, `app, and the controller for the application's only view, index.html.

The app module is the only object added to the global namespace; it serves as both module and application namespace. As you look at other the other application JavaScript files, you'll see that each adds itself as a "service" to the app module through one of the AngularJS configuration methods: controller, factory, or value.

Note how the controller factory method relies on AngularJS to inject the application's datacontext and logger services at runtime.

app.controller('EdmundsCtrl', function ($scope, datacontext, logger) {
    ...
} 

You'll see this pattern repeated in the other scripts that define components with application service dependencies.

The 'EdmundsCtrl' controller governs the view through the AngularJS $scope variable which it configures with properties and methods. The makes array is one of those properties; it holds the "Make" entities fetched from the Edmunds service by the getMakes method:

function getMakes() {
    datacontext.getMakes().then(succeeded).catch(queryFailed);

    function succeeded(results) {
        $scope.makes = results;
        $scope.$apply();
        logger.info("Fetched " + results.length + " Makes");
    }
};

See how it delegates to the datacontext.getMakes method which returns a promise. When the Edmumnds service returns successfully, the controller replaces the makes array with the results of the service call. These results are make entities, ready for binding.

The getModels method is a little trickier:

function getModels(make) {

    if (!make.showModels) {
        return; // don't bother if not showing
    } else if (make.models.length > 0) {
        // already in cache; no need to get them
        logGetModelResults(true /*from cache*/);
    } else {
        getModelsFromEdmunds()
    }
    ...
}

For the selected "Make" it will do one of the following:

  • nothing if the "Show models" checkbox is unchecked.

  • get the models from cache via the "Make"'s models navigation property if its models have already been loaded.

  • fetch the models from the Edmunds service if this is the first request for models for this "Make".

The third choice - fetching models - is like getMakes in that it delegates to a datacontext for asynchronous data access.

datacontext.js

The datacontext is a client-side application "service" responsible for fetching data from the remote Edmonds services and turning those data into Breeze entities.

These will be AngularJS entities so we have to configure Breeze accordingly. Breeze makes Knockout-ready entities by default. The following line tells Breeze to use its native "backingStore" modeling adapter instead (the adapter appropriate for AngularJS entities).

breeze.config.initializeAdapterInstance(
    "modelLibrary", "backingStore", true);

The Edmunds Vehicle Information service is a 3rd party service. That service defines its own proprietary API over which we have no control. It won't send us metadata. It will send us data in a non-standard, proprietary JSON format.

We can't change the service. We can't make it adapt to Breeze. We have to adapt to it.

We begin by creating a Breeze DataService object that describes some high-level characteristics of the Edmunds service. Then we create an EntityManager to targets that service.

var ds = new breeze.DataService({
    serviceName: serviceName, // the URL endpoint
    hasServerMetadata: false, // the service won't give us metadata
    useJsonp: true,           // request data using the JSONP protocol
    jsonResultsAdapter: jsonResultsAdapter
});

var manager = new breeze.EntityManager({dataService: ds});

Note that the Edmunds service is hosted hosted in a different domain than our application. The browser's Same Origin Policy prevents us from accessing the service through regular AJAX calls. We'll only GET data so we can use the JSONP protocol to circumvent that policy (Edmunds supports that protocol).

The manager will need metadata to create, materialize, and manage Edmunds data as Breeze entities. The remote Edmunds service won't give us metadata. So with this next line of initialization code, we'll get metadata via the local model service which we wrote in JavaScript and arranged for AngularJS to inject into the datacontext.

model.initialize(manager.metadataStore);

The datacontext is finally ready to make Edmunds service calls. Here's the request for vehicle "Makes":

function getMakes() {
    var parameters = makeParameters();
    var query = breeze.EntityQuery
        .from("vehicle/makerepository/findall")
        .withParameters(parameters);
    return manager.executeQuery(query).then(returnResults);
}

Edmunds doesn't support IQueryable so we won't be constructing queries with the Breeze query verbs such as where, orderBy, and expand.

We begin by constructing a query targeting the Edmunds "vehicle/makerepository/findall" endpoint that returns all vehicle "Makes".

The Edmunds API expects some query parameters in the request which we add using the Breeze withParameters syntax. We encapsulated the required parameters themselves in the makeParameters method for use in all of our Edmunds service calls:

var parameters = {
    fmt: "json",
    api_key: "z35zpey2s8sbj4d3g3fxsqdx"
    // Edmund throttles to 4000 requests per API key
    // get your own key: http://developer.edmunds.com/apps/register
};

The balance of the datacontext is standard Breeze and JavaScript. Let's move on to the model.js application script.

model.js

We can't get Breeze metadata from the Edmunds service. We can define the metadata on the client.

The calling datacontext extracts the metadataStore from its EntityManager and passes it into the model.initialize method. The initialize method adds to that store the descriptions for the application's two entity types, "Make" and "Model".

Although you will find individual methods documented in the Breeze API (see especially EntityType and DataType, we have not yet described the Metadata Definition API properly. We ask that you rely on your intuition as you read this code ... your intuition will usually be correct. Please post questions to StackOveflow tagged with "breeze".

jsonResultsAdapter.js

The format of the JSON payload from the Edmunds service is proprietary and idiosyncratic.

The author of this sample read the definitions for each service response (e.g, the "Make_Repository" response) on the Edmunds website and translated that information into a JsonResultsAdapter. That adapter guides Breeze as it "materializes" Make and Model entities from Edmunds JSON response data.

If this application were more complicated, involving a rich entity model and many different service calls, we'd probably want to write more than one JsonResultsAdapter ... perhaps one for each Edmunds service endpoint. But this app is very simple; we can get away with just one JsonResultsAdapter for all entity types and service calls.

A JsonResultsAdapter has two methods. The first extracts the true data contents - the "dataset" - from the "envelope" of the response. The Edmunds service is typical in this respect: the fetched dataset is in the results property as we see here.

extractResults: function (data) {
    var results = data.results;
    ...
    return results && (results.makeHolder || results.modelHolder);
},

We're only interested in "Make" and "Model" datasets which Edmunds identifies by "...Holder" properties.

Breeze calls the second adapter method, visitNode, for each node in the dataset. A 'node" is a JavaScript object containing data for a single thing.

For example, when we request "Make" data, the dataset will be an array of "Make" nodes. The "Make" node is an object with all the properties that came over the wire in its JSON representation.

Each "Make" node could have sub-nodes representing related information. In our simple example, we only consider the top level "Make" and "Model" nodes.

This visitNode implementation handles both "Make" and "Model" nodes. It distinguishes between the two types by looking for the type's "fingerprint".

The "fingerprint" is whatever it takes to distinguish one type from all others. For example, we identify a "Model" node by the presence of a makeId property ... a unique characteristic of a "Model" node.

The code to prepare a "Model" node is in the second "IF" block. The visitor's job is to manipulate node data until the properties and values of the node align with the properties and data types defined for the corresponding entity type. This often involves some shuffling of properties and transformation of property values as we see in this code extract:

// move 'node.make' link so 'make' can be null reference
node.makeLink = node.make;
node.make = null;
...

The Model.make node property is a string, a URL link to the parent "Make" object on the Edmunds service. The Model.make entity property is supposed to be a navigation property that returns the model's parent Make entity. These are two entirely different values for properties of the same name. So the vistor relocates the URL string to a new property called makeLink and "nulls-out" the node's make property. Breeze will set the model's corresponding entity property later to the parent Make entity.

Here's a different example. The "Model" node has a collection of strings, each defining a "Vehicle Style". The visitor combines these strings into a single, comma-separated string so that the information fits into a single entity vehicleStyles property:

var styles = node.categories && node.categories["Vehicle Style"];
node.vehicleStyles = styles && styles.join(", ");

Finally, the vistor returns an object with a single property, entityType that (a) tells Breeze to turn this node into an entity and (b) tells Breeze what kind of entity to make.

return { entityType: "Model" };

Breeze takes over from there. It reads the (revised) node and moves selected node data into a newly created entity instance.

Note that Breeze only sets values for the data properties defined in metadata for that entity type. Most of the node data from the Edmunds service are discarded in this sample application.

Learn more about JsonResultsAdapter in the Breeze documentation.

Service glitches

Of course you'll need a good network connection to reach the Edmunds service. The Edmunds service is pretty reliable but occasionally fails to respond properly. You'll see a red error toast when that happens. The app is too simple to auto-recover; simply trying the operation again is usually sufficient.

Edmunds throttles requests, limiting each API key to 4,000 requests daily. In the unlikely event that sample users exceed this threshold, you can get your own key from Edmunds and replace the one baked into the sample in the datacontext.js.

Wrap up

We hope you enjoyed this sample. We've tried to keep it very simple and focused on the most basic mechanics of working with 3rd party services.

We demonstrated a two entity model in which the entities are related to each other. The application relies on the entity navigation property from "Make" to "Model" to get models from cache and display them in a table beneath each "Make".

We didn't show how you can edit "Makes" and "Models". We didn't show how to add validation rules to the metadata. And we didn't show how to save changed entities back to the service (which you can't do with the Edmunds service anyway). These are capabilities we'll demonstrate in future samples.