Breeze Version: 1.5.3
I'm experiencing something similiar to some older questions on SO but it seems like this "bug" is reoccurring:
I have a 1-To-Many unidirectional navigation property which is not populated. I have checked the metadata and the response from the server. I've even debugged into breeze and the node (or rawEntity) seems to be perfect.
I've tried to track it down and came to the conclusion, that it happens, because no "inverse"-Property is found for my Navigation Property and the mergeRelatedEntities-Function returning without updating the target Entity:
function mergeRelatedEntities(mc, navigationProperty, targetEntity, rawEntity) {
var relatedEntities = mergeRelatedEntitiesCore(mc, rawEntity, navigationProperty);
if (relatedEntities == null) return;
var inverseProperty = navigationProperty.inverse;
if (!inverseProperty) return;
var originalRelatedEntities = targetEntity.getProperty(navigationProperty.name);
originalRelatedEntities.wasLoaded = true;
relatedEntities.forEach(function (relatedEntity) {
if (typeof relatedEntity === 'function') {
mc.deferredFns.push(function () {
relatedEntity = relatedEntity();
updateRelatedEntityInCollection(relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
});
} else {
updateRelatedEntityInCollection(relatedEntity, originalRelatedEntities, targetEntity, inverseProperty);
}
});
}
Older Posts:
Non scalar navigation properties are not populating with "nodb" conception
and
Breeze (1.4.5) unidirectional one-to-many: navigation collection not populated
Edited 11. May 2015
Okay I start to understand what Ward meant with the unmapped properties (by finding a similar question from 2 years ago: Handling calculated properties with breezejs and web api)
What I have so far:
function iUIConfigConstructorTool() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigConstructorAppl() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigConstructorWidget() {
this.ConfigToCurrentUSetting = null;
};
function iUIConfigInitializer(uiConfigObject) {
// initializing other properties
};
this.manager.metadataStore.registerEntityTypeCtor("Tool", iUIConfigConstructorTool, iUIConfigInitializer);
this.manager.metadataStore.registerEntityTypeCtor("Appl", iUIConfigConstructorAppl, iUIConfigInitializer);
this.manager.metadataStore.registerEntityTypeCtor("Widget", iUIConfigConstructorWidget, iUIConfigInitializer);
This does what I want. Is there a way, to do this over the metamodel from the server? Because I define my calculated properties on the server and the metamodel is delivered by the server, I don't want to change the client-side implementation if I add a new Navigation-Property. So I'd need something like a flag in the metamodel to tell breeze, that this property needs to be filled as it comes over the wire without ForeignKeys etc.
Maybe in other words: We are doing "sub queries" on the server side (e.g. find Customers with it's Orders but only up to a certain Date) for each queried object and deliver this to breeze (in a separate property than the real orders-property of the Customer). Our problem is: How do we unpack this sub-query because there is no direct connection in metadata but we need the connection for the logic.
Please update your question with:
how you obtained/created metadata
metadata for the two endpoints (just the nav props, pk prop, and fk props will do)
the exact query expression
Of course a repro in plunker would be most welcome.
update 7 May 2015
If I understand your comment correctly, the navigation property (properties) in question are to be maintained by you (w/ server-supplied info), not by Breeze.
That leads me to suggest that you maintain them as unmapped properties rather than mapped, navigation properties. Does that make sense?
Related
I am setting properties in client-side Breeze entities. I see the EntityInfo UnmappedValuesMap collection, which I'd like to use. It would be great if I could get a few custom client values returned in the Unmapped collection to avoid adding these everywhere.
I am initializing the metastore with:
store.registerEntityTypeCtor("UserInfo", null, userInfoInitializer);
function userInfoInitializer(userinfo) {
userinfo.creatingId = ko.observable(0);
...
I was hoping 'creatingId' would get passed to server. But nothing extra appears in the net traffic.
I don't think it matters, but on the server I am using Breeze.ContextProvider.
Are there flags somewhere that govern this behavior? Thanks for any guidance.
If you wanted to add a 'creatingId' to every entity type you could do something like this:
metadataStore.getEntityTypes().forEach(
function(entityType) {
var ctor = function () {
this.creatingId = 0;
};
metadataStore.registerEntityTypeCtor(entityType.name, ctor, null);
});
Note- using a constructor instead of an initializer. This will ensure the value appears in the unmapped values collection. Don't worry, breeze will make this an observable property on your entity.
I was able to follow the instruction on adding data, that part was easy and understandable. But when I tried to follow instructions for editing data, I'm completely lost.
I am following the todo sample, which works quite well, but when I tried to add to my own project using the same principle, nothing works.
in my controller, I have the following:
function listenForPropertyChanged() {
// Listen for property change of ANY entity so we can (optionally) save
var token = dataservice.addPropertyChangeHandler(propertyChanged);
// Arrange to remove the handler when the controller is destroyed
// which won't happen in this app but would in a multi-page app
$scope.$on("$destroy", function () {
dataservice.removePropertyChangeHandler(token);
});
function propertyChanged(changeArgs) {
// propertyChanged triggers save attempt UNLESS the property is the 'Id'
// because THEN the change is actually the post-save Id-fixup
// rather than user data entry so there is actually nothing to save.
if (changeArgs.args.propertyName !== 'Id') { save(); }
}
}
The problem is that any time I change a control on the view, the propertyChanged callback function never gets called.
Here's the code from the service:
function addPropertyChangeHandler(handler) {
// Actually adds any 'entityChanged' event handler
// call handler when an entity property of any entity changes
return manager.entityChanged.subscribe(function (changeArgs) {
var action = changeArgs.entityAction;
if (action === breeze.EntityAction.PropertyChange) {
handler(changeArgs);
}
});
}
If I put a break point on the line:
var action = changeArgs.entityAction;
In my project, it never reaches there; in the todo sample, it does! It completely skips the whole thing and just loads the view afterwards. So none of my callback functions work at all; so really, nothing is subscribed.
Because of this, when I try to save changes, the manager.hasChanges() is always false and nothing happens in the database.
I've been trying for at least 3 days getting this to work, and I'm completely dumbfounded by how complicated this whole issue has been for me.
Note: I'm using JohnPapa's HotTowel template. I tried to follow the Todo editing functionality to a Tee.. and nothing is working the way I'd like it to.
Help would be appreciated.
The whole time I thought the problem was in the javascript client side end of things. Turned out that editing doesn't work when you created projected DTOs.
So in my server side, I created a query:
public IQueryable<PersonDTO> getPerson(){
return (from _person in ContextProvider.Context.Queries
select new PersonDTO
{
Id = _person.Id,
FirstName = _person.FirstName,
LastName = _person.LastName
}).AsQueryable();
}
Which just projected a DTO to send off to the client. This did work with my app in fetching data and populating things. So this is NOT wrong. Using this, I was able to add items and fetch items, but there's no information that allowed the entitymanager to know about the item. When I created an item, the entitymanager has a "createEntity" which allowed me to tell the entitymanager which item to use.. in my case:
manager.createEntity(person, initializeValues);
Maybe if there was a "manager.getEntity" maybe that would help?
Anyways, I changed the above query to get it straight from the source:
public IQueryable<Person> getPeople(){
return ContextProvider.Context.People;
}
Note ContextProvider is:
readonly EFContextProvider<PeopleEntities> ContextProvider =
new EFContextProvider<PeopleEntities>();
So the subscribe method in the javascript checks out the info that's retrieved straight from the contextual object.. interesting. Just wish I didn't spend 4 days on this.
i'm displaying a server calculated value to the enduser by using propertyChanged event.
i was using breeze 1.4.8 and i'm using the productivity stack (ms sql, web api, ef)
It was working fine.
Recently i've updated to 1.4.12 and i recognized that this event doesn't get fired anymore.
The property "A_ProvisionTotal" gets calculated serverside only.
<snip>
var token = vm.transaction.entityAspect.propertyChanged.subscribe(propertyChanged);
function propertyChanged(propertyChangedArgs) {
var propertyName = propertyChangedArgs.propertyName;
if (vm.transaction.tblEmployees.CalculationMethod == "A" && propertyName == "A_ProvisionTotal")
logSuccess('Provision neuberechnet' + '<br/>' + 'Aktuell: ' + $filter('number')(vm.transaction.Provision, 2), true);
</snip>
Let me know if this is a known regression and if you need more snippets.
A couple of thoughts for how you could accomplish your desired functionality.
The entity could remember the last calculated value in a private field. Then whenever the recalculation gets triggered, you can compare the new value to the last calculated value and if there is no change, ignore the new calculated value.
Alternatively, you could define the properties involved in your calculation as ES5 properties in the entity ctor function and then trigger the calculation in the setter of the relevant properties, when they get set with a new value. More information here: http://www.breezejs.com/documentation/extending-entities#es5-property. ES5 properties are convenient if you want to build behavior such as your calculation into setters.
Update 3
This is not a bug - see the response to this post that describes this as a documented and deliberate behavior.
Update 2 June 2014
I overlooked a key fact in your question ... one that only became clear to me after I looked at the code you included in your comments. Let me extract the key pieces for other readers:
Your test issues a query, then saves an unrelated change to the server (where the property-of-interest is updated server-side), then checks if that telltale property-of-interest raises propertyChanged when the save result is merged back into cache.
var query = EntityQuery.from("Orders").where('id', 'eq', 10248);
query.using(em).execute().then(querySucceeded).then(checkPropertyChanged).fin(start);
// querySucceed receives order 10248, updates an unrelated property (so you can save),
// wires up a propertyChanged listener and saves, returning the saveChanges promise
function checkPropertyChanged(saveResults) {
var saved = saveResults.entities[0];
// this passes because the server-side change to `Freight` was returned
ok(saved && saved.Freight() === 1200.00,
"freight got changed serverside");
// this fails because Breeze did not notify you that the `Freight` had changed
ok(notifications[0].propertyName === "Freight",
"notified serverside change of Freight Property");
}
Summarizing, you expected that a property change on the server would trigger a propertyChanged notification on the client when the entity data are re-retrieved from the server as a by-product of saveChanges.
Do I have that right?
Our documentation was not clear on whether the merge of query, save, and import entity results would raise propertyChanged.
I discussed internally and confirmed that these operations SHOULD raise propertyChanged. I also wrote another (somewhat simpler) test that reveals the bug you discovered: that merged save results may not raise propertyChanged.
We'll look into it and tell you when we've fixed it. Thanks for discovering it.
Original
We have regression tests that show that the Breeze EntityAspect.propertyChanged event is raised in v.1.4.12. For example, you can see it at work in the DocCode sample, "basicTodoTests.js"; scroll to: "Breeze propertyChanged raised when any property changes".
Can you confirm that it really is a Breeze failure? Perhaps the property you are changing is not actually an entity property? Sometimes you think you are changing an entity (e.g, your Transaction entity) but the thing whose property you changed isn't actually an entity. Then the problem is that the data you thought would be mapped to a Transaction was not ... and you can start looking for that quite different problem.
In any case, I suggest that you write a small test to confirm your suspicion ... most importantly for yourself ... and then for us. That will help us discover what is different about your scenario from our scenarios. We'll fix it if you can find it. Thanks.
Actually, I'm not sure that this is a bug. Property change events DO get fired during a save merge but the property name parameter is documented as being 'null' when fired as a result of a save.
http://www.breezejs.com/sites/all/apidocs/classes/EntityAspect.html#event_propertyChanged
From the API Docs for the 'propertyName' parameter returned by EntityAspect.propertyChanged:
The name of the property that changed. This value will be 'null' for operations that replace the entire entity. This includes queries, imports and saves that require a merge. The remaining parameters will not exist in this case either.
What may have happened between 1.4.8 and 1.4.13 is that we actually implemented our design spec more carefully and probably introduced your breaking behavior. ( which we should have documented as such but likely missed).
Update by Ward
I updated the DocCode test which first confirmed the behavior described in your question and then confirmed the documented behavior.
We do regret that we apparently neglected to implement the documented behavior earlier and that we didn't mention the breaking change in our release notes (since updated).
Here's that test:
asyncTest("propertyChanged raised when merged save result changes a property", 3, function () {
var em = newTodosEm();
var todo = em.createEntity('TodoItem', {Description: "Saved description" });
em.saveChanges().then(saveSucceeded).catch(handleFail).finally(start);
ok(todo.entityAspect.isBeingSaved, "new todo is in the act of being saved");
// This change should be overwritten with the server value when the save result is returned
// even though the entity is in an Added state and the MergeStrategy is PreserveChanges
// because save expects to merge server values into an entity it is saving
todo.Description("Changed on client before save returns");
var descriptionChanged = false;
todo.entityAspect.propertyChanged.subscribe(function (changeArgs) {
// Watch carefully! The subscription is called twice during merge
// 1) propertyName === "Id" (assigned with permanent ID)
// 2) propertyName === null (WAT?)
// and not called with propertyName === "Description" as you might have thought.
// Actually 'null' means "merged a lot of properties"
// Documented: http://www.breezejs.com/sites/all/apidocs/classes/EntityAspect.html#event_propertyChanged
// The reason for this: don't want to fire a ton of events on whole entity load
// especially when merging many entities at the same time.
if (changeArgs.propertyName === null || changeArgs.propertyName === 'Description') {
descriptionChanged = true;
}
});
function saveSucceeded(saveResult) {
var saved = saveResult.entities[0];
// passes
equal(saved && saved.Description(), "Saved description",
"the merge after save should have restored the saved description");
// fails
ok(descriptionChanged,
"should have raised propertyChanged after merge/update of 'Description' property");
}
});
I'm experimenting with BreezeJS with Web API using the BreezeControllerAttribute. How should calculated properties on an entity be exposed? The only way I've found to do this reliably is to create an intermediate DTO that inherits from the entity or use a projection. Normally I would use a readonly property for this scenario, but those appear to be ignored.
When Breeze maps JSON property data to entities, it ignores properties that it does not recognize. That's why your server class's calculated property data are discarded even though you see them in the JSON on the wire.
Fortunately, you can teach Breeze to recognize the property by registering it as an unmapped property. I'll show you how. Let me give some background first.
Background
Your calculated property would be "known" to the Breeze client had it been a property calculated by the database. Database-backed properties (regular and calculated) are picked up in metadata as mapped properties.
But in your case (if I understand correctly) the property is defined in the logic of the server-side class, not in the database. Therefore it is not among the mapped properties in metadata. It is hidden from metadata. It is an unmapped instance property.
I assume you're not hiding it from the serializer. If you look at the network traffic for a query of the class, you can see your calculated property data arriving at the client. The problem is that Breeze is ignoring it when it "materializes" entities from these query results.
Solution with example
The solution is to register the calculated property in the MetadataStore.
I modified the entityExtensionTests.js of the DocCode sample to include this scenario; you can get that code from GitHub or wait for the next Breeze release.
Or just follow along with the code below, starting with this snippet from the Employee class in NorthwindModel.cs:
// Unmapped, server-side calculated property
[NotMapped] // Hidden from Entity Framework; still serialized to the client
public string FullName {
get { return LastName +
(String.IsNullOrWhiteSpace(FirstName)? "" : (", " + FirstName)); }
}
And here is the automated test in entityExtensionTests.js
test("unmapped property can be set by a calculated property of the server class", 2,
function () {
var store = cloneModuleMetadataStore(); // clones the Northwind MetadataStore
// custom Employee constructor
var employeeCtor = function () {
//'Fullname' is a server-side calculated property of the Employee class
// This unmapped property will be empty for new entities
// but will be set for existing entities during query materialization
this.FullName = "";
};
// register the custom constructor
store.registerEntityTypeCtor("Employee", employeeCtor);
var fullProp = store.getEntityType('Employee').getProperty('FullName');
ok(fullProp && fullProp.isUnmapped,
"'FullName' should be an unmapped property after registration");
var em = newEm(store); // helper creates a manager using this MetadataStore
var query = EntityQuery.from('Employees').using(em);
stop(); // going async
query.execute().then(success).fail(handleFail).fin(start);
function success(data) {
var first = data.results[0];
var full = first.FullName();
// passing test confirms that the FulllName property has a value
ok(full, "queried 'Employee' should have a fullname ('Last, First'); it is "+full);
}
});
What you need to do is in this small part of the test example:
var yourTypeCtor = function () {
this.calculatedProperty = ""; // "" or instance of whatever type is is supposed to be
};
// register your custom constructor
store.registerEntityTypeCtor("YourType", yourTypeCtor);
I have two entities in my EDM, Application and Address pretty much like the following:
class Application
{
ICollection<Address> Addresses { get; set; }
}
class Address { }
On the client, I create an instance of each and try to add the address instance to the Application.addresses collection:
var address = addressType.createEntity(...);
var application = applicationType.createEntity(...);
application.addresses.push(address);
Unfortunately, I get a runtime exception saying: "Unable to get value of the property 'name': object is null or undefined".
I tracked the exception back to the checkForDups function in breeze.debug.js#9393-9404 (v1.2.8):
function checkForDups(relationArray, adds) {
// don't allow dups in this array. - also prevents recursion
var inverseProp = relationArray.navigationProperty.inverse;
var goodAdds = adds.filter(function(a) {
if (relationArray._addsInProcess.indexOf(a) >= 0) {
return false;
}
var inverseValue = a.getProperty(inverseProp.name);
return inverseValue != relationArray.parentEntity;
});
return goodAdds;
}
As it happens, my entities are in a one-to-many unidirectional relationship (without an inverse navigation property); as a result at runtime relationArray.navigationProperty.inverse is undefined and so the error when trying to access the name property.
Adding a simple check fixes the problem, and allows adding to the collection:
if (!inverseProp) {
return true;
}
So after all this the question is: is this a bug or is it simply that Breeze does not support one-to-many unidirectional?
Edit As of v Breeze 1.3.5, available now (June 4 2013), this has been fixed.
Edit: Ok, this IS a bug, but I couldn't get the fix in for this current release. I will try to get it in the following release.
The fix that you suggested, which was a good idea, actually only hides the issue.
The real problem is that breeze does not have sufficient metadata for the case where we have a unidirectional navigation in the 1->n direction (i.e. not in the n->1 direction). Because of this, duplicate entity checking in the navigation collection will not work and automatic hookup of children to parents will be missing as well.
The simplest work around until we get the fix in is to simply make it a bidirectional navigation. Note that unidirectional navigation in the other direction works just fine.
This is likely a bug
We do have an example in our DocCode sample project of a unidirectional navigation between OrderDetails and Products. But in that case we allow navigation from a OrderDetail -> Product (1-1) but not from Product -> OrderDetails (1-n).
Your case appears to be the opposite, i.e. allowing 1-n but disallowing the corresponding 1-1. I will create some tests and if I can repro this, it will be fixed in the next release.
I will post back here when that occurs. ( and thx for finding it :)