Why are my Breeze.js entities not creating ko.observables?
Asked Answered
C

1

4

I am using Breeze.js without the server side components and have the entities being created on the client side with the following code. Per Ward's request I have simplified everything and am including more information. My MetaDataStore Configuration function-

function configureMetadataStore(metadataStore) {
        metadataStore.addEntityType({
            shortName: 'Manufacturer',
            namespace: 'StackAndReach',
            autoGeneratedKeyType: breeze.AutoGeneratedKeyType.Identity,
            dataProperties: {
                id: { dataType: DT.Int64, isPartOfKey: true },
                name: { dataType: DT.String },
                website: { dataType: DT.String},
                approved: {dataType: DT.boolean},
                user_id: { dataType: DT.Int64 }
            }
        });
    }

My JSON response from my server

{"id":"141","name":"Trek","website":"http:\/\/www.trekbikes.com\/","approved":"1","user_id":"3"}

My config code from my datacontext (the whole setup minus the lack of server metadata is set up after John Papa's courses)

var entityQuery = breeze.EntityQuery,
        manager = configureBreezeManager();
function configureBreezeManager() {
        breeze.NamingConvention.camelCase.setAsDefault();
        var ds = new breeze.DataService({
            serviceName: config.remoteServiceName,
            hasServerMetadata: false
        });

        var mgr = new breeze.EntityManager({dataService : ds});
        model.configureMetadataStore(mgr.metadataStore);
        return mgr;
    }

When the models are pulled down the data is there, but the data is not wrapped in ko.observables and the ko.observables/ko.computed in the init functions are not in the models that are passed out of the queries. How can I ensure the model's data is wrapped in ko.observables and the ko.computeds are added?

Cnemis answered 26/7, 2013 at 22:38 Comment(5)
Verify that bike_Initializer is being called on the expected objects? (Also, curses to the nonsense bicycle tire sizes ..)Eicher
It is not being called, and I have no idea why. Don't get me started on the tire size silliness!!Cnemis
Have you added a jsonResultsAdapter to your project?Combat
No...can you point me in the direction of the documentation on that?Cnemis
It is pulling the info and parsing it correctly from the api, it is just not wrapping it in ko.observables and running the initialization routine I have specified. Do you need a jsonResultsAdapter to wrap it in ko.observable?Cnemis
T
11

This answer is as much a tutorial on analyzing the problem as answer to the question.

Step 1 - Simplify and Isolate

Let's simplify radically until we find the missing step. Let's start with the simplest entity type you have ... one with 2 to 5 properties max. Don't have one? Make one up. Cut down the Manufacturer to just "id" and "name". We're trying to get the mechanics down first.

You aren't using any breeze components on the server. Fine. Pick a server endpoint that delivers that test entity data. Have that endpoint deliver a JSON array with just one instance. Show us the JSON that are arriving on the client ... the entire JSON payload, exactly as it arrives on the wire. It should be brief; if it isn't, you haven't simplified enough.

THEN we can figure out whether you need a JsonResultsAdapter and what it should look like if you do.

Show us the exact sequence by which you populate the metadataStore with an EntityType, ctor, and initializer. Frankly, I'd rather have no ctor or initializer until we've got the first one working.

How do you make sure that the EntityManager you create is using that store? We need to see your configuration code and how you new-up the EntityManager and use it to query the endpoint.

If you follow my suggestion there won't be much code. Twenty lines maybe. And the JSON feed should be about 10 lines. If you can't hit these numbers, you haven't simplified enough.

Step 2 - Review the simpler example

Now that you've re-worked the example, I have a better idea where to look.

Two things leap out at me:

  1. The JSON result from the server
  2. The camelCase naming convention

JSON from the Server

Let's pretty print the JSON results you provided and discuss them:

{
  "id": "141",
  "name": "Trek",
  "website": "http:\/\/www.trekbikes.com\/",
  "approved": "1",
  "user_id": "3"
}

Breeze won't know what to do with that JSON object because it lacks type information. To Breeze its just an arbitrary object, perhaps the result of a projection.

Compare that to the JSON result of a query hitting the DocCode Web API. Here's the URL generated for the query:

>http://localhost:47595/breeze/northwind/Suppliers/?$top=1

and here's the (abbreviated) JSON result

[
   {
      "$id":"1",
      "$type":"Northwind.Models.Supplier, DocCode.Models",
      "SupplierID":1,
      "CompanyName":"Exotic Liquids"
   }
]

By default a Breeze client expects data that have been serialized with JSON.NET, the default serializer in ASP.NET.

A JSON.NET payload is either a node or an array of nodes. JSON.NET adds its own $id and $type properties to each node.

I want to focus your attention on the $type property which you may recognize as the full name (class-with-namespace, assembly-name) of a .NET type.

You could get away without the $id property.

The $id is an auto-incrementing serialization key. Often the same object appears multiple times in a payload. Instead of repeating the contents, JSON.NET substitutes a simple node like {$ref: #} where # refers to the $id of an earlier node. This approach both reduces the payload size and breaks circular references.

But Breeze is really looking forward to that $type property. That is how it connects the JSON object/node to a type in your metadata. If your manufacturer example node had one, it might like this:

"$type": "StackAndReach.Manufacturer, MyModel"

I don't know how you're serializing data on your server. It would seem you are using something other than JSON.NET.

That is cool. I'm just telling you how Breeze works by default; it is very .NET friendly. But Breeze doesn't need .NET. It is a pure JavaScript library. You just have to tell it what you want.

Use toType(...)

The simplest thing you can do is add toType to your query.

var query = breeze.EntityQuery.from('Manufacturers')
                  .where( ... )
                  .toType( 'Manufacturer' );

In this way, you state explicitly that the top level node(s) returned by the 'Manufacturers' endpoint contain data for the Manufacturer type that you described in metadata.

I'll bet this works for you right away (once you fix the Naming Convention problem described below).

This is an effective approach but it has several drawbacks. I'll mention two:

  1. You have to remember to add it to every query.

  2. It only works for top-level entities; if won't work for nested entities such as are returned when you apply the .expand() clause.

I prefer to teach the Breeze client how to interpret the JSON results on its own ... with a custom JsonResultsAdapter.

Custom JsonResultsAdapter

Check out the Breeze Edmunds Sample in which a Breeze client consumes data from the Edmunds Vehicle Information service.

The Edmunds server sends a completely different kind of JSON payload in response to queries. Here is a snippet:

{
   "makeHolder":[
      {
         "id":200347864,
         "models":[
            {
               "link":"/api/vehicle/am-general/hummer",
               "id":"AM_General_Hummer",
               "name":"Hummer"
            }
         ],
         "name":"AM General",
         "niceName":"amgeneral",
         "manufacturer":null,
         "attributeGroups":{

         }
      },
      ... more ...
   ]
}

No $type there either. What did the Breeze developer do? He wrote a custom Breeze JsonResultsAdapter and it's in the file app/jsonResultsAdapter.js.

I'm not going to reproduce that file here although it is only 40 lines. I want you to read the jsonResultsAdapter documentation, pull down the Edmunds sample, and read it for yourself.

I will summarize what it does and how it works. Breeze calls your jsonResultsAdapter first when it receives a JSON payload and again as it processes each node in that payload. Your job is tell Breeze how to treat that node which you do by tweaking the node itself and returning a meta object that describes the node.

Here's a snippet:

>if (node.id && node.models) {
    // move 'node.models' links so 'models' can be empty array
    node.modelLinks = node.models;
    node.models = [];
    return { entityType: "Make"  }
}

There are three activities in this snippet:

  1. identifying what the node is about (the if ...)
  2. adjusting the node values (for whatever reasons make sense to you)
  3. composing and returning the "meta" object result.

Focus on #3. That's where the developer told Breeze "Turn this node into a Make entity.

Structural type matching

You might say, "Hey Ward, the Manufacturer entity type matches the JSON object structure exactly. Breeze should recognize it as a Manufacturer."

Breeze does not divine the entity type by matching the type structure. Nor do I think it should ... because different types often share the same structure. For example: I have a StatusCode and ProductCode entity types that are both { id: int, name: string}. We have plenty of other enhancements to work on; coping with type ambiguity is not high on our list.

Naming Convention

Finally, let's return to the other problem that I saw.

Your configureBreezeManager method begins:

breeze.NamingConvention.camelCase.setAsDefault();

You've changed the default Naming Convention from "same-on-client-and-server" to "pascalCase-on-client/CamelCase-on-server".

By switching to the camelCase convention, you're telling Breeze that a client-side property foo should be sent to the server as Foo.

Is that the right thing to do? It would be if your server expected CamelCase property names. But, based on the property names in your JSON payload, the server expects CamelCase too. The property names are identical on client and server. Bad things will happen if breeze sends a manufacturer with a Name property value instead of a name property value.

Leave the breeze default, "do nothing" convention in place. Don't override it. Remove that pascalCase convention line from your configureBreezeManager.

Saving changes

We've been talking about query results. We haven't talked at all about how you're going to save changes back to the server.

I'm sure you have your own protocol (something ReST-like?) and serialization format. That is a completely different discussion. Let's not get into that in this Stack Overflow question. I'm just alerting you to the probability that you'll be scratching your head about this one pretty soon.

Twentyfourmo answered 28/7, 2013 at 5:40 Comment(6)
I have tried to address all your requests in re-doing the original question as it would have been too long for comments.Cnemis
I used the .toType() command and you are correct that it worked perfectly. Thanks for your help. I'll keep you updated as I work on the php backend to make this more transparent.Cnemis
Thanks for this awesome, detailed answer! Do you know what changes (starting from a fresh New WebAPI project) causes JSON.NET to not include $type during serialization?Tsunami
News to me! That's scary. Do you have a repro?Twentyfourmo
I don't off hand.. I was able to figure out that I had to change (from defaults) the following settings on the json.net formatter: ReferenceLoopHandling=Serialize, PreserveReferencesHandling=Objects,TypeNameHandling=Objects. What's interesting is that the HotTowel template project does not show any of these settings changed.Tsunami
found where this happens in breeze core code - BreezeConfig.CreateJsonSerializerSettings().Tsunami

© 2022 - 2024 — McMap. All rights reserved.