Change tracking
Breeze entities are self-tracking. Each entity keeps its own change state, and remembers the values it had before you changed it. That information lives in the entity's entityAspect. This page introduces the parts of it that concern change tracking; see Inside the entity for the rest.
How properties are tracked
Breeze 3 has one model library, the backing store. For each data and navigation property in the metadata, it defines an accessor property on the entity type's prototype, and it keeps the values in a private store on each instance. As a result, an ordinary assignment is tracked:
order.freight = 12.5; // tracked
order.setProperty('freight', 12.5); // same thingA tracked change can:
- record the old value in
entityAspect.originalValues - change
entityAspect.entityStatefromUnchangedtoModified - validate the new value, if
validateOnPropertyChangeis on (it is by default) - raise
entityAspect.propertyChangedandEntityManager.entityChanged
Validation is much the most expensive of these — see Performance if you are setting properties in bulk.
Breeze tracks only the properties in the metadata, and those include any unmapped properties you register. A property you add to an entity yourself (order.note = 'x') is an ordinary JavaScript property, and Breeze ignores it. Changing an unmapped property raises events, but doesn't make an Unchanged entity Modified.
Setting a property to its current value isn't a change, so nothing happens.
EntityState
entity.entityAspect.entityState tells you where the entity stands:
| State | Meaning |
|---|---|
Added | New, in the cache, not yet in the database |
Unchanged | In the cache, unchanged since it was queried or last saved |
Modified | In the cache, with pending changes |
Deleted | In the cache, marked for deletion |
Detached | Not in any cache |
An EntityState has methods that test for one state (isAdded(), isUnchanged(), isModified(), isDeleted(), isDetached()) and for useful combinations (isAddedOrModified(), isUnchangedOrModified(), isAddedModifiedOrDeleted()).
Breeze updates the state as things happen:
| Action | New state |
|---|---|
| Arrives in the cache from a query | Unchanged |
| You set one of its properties | Modified |
| Saved successfully | Unchanged |
Deleting
To delete an entity, mark it:
order.entityAspect.setDeleted();This doesn't destroy the object, and it doesn't touch the database. The entity stays in the cache as Deleted until you save. A successful save deletes it on the server, and detaches it from the cache. Deleting an Added entity detaches it immediately, because there is nothing on the server to delete.
Reverting
Changing a value back by hand doesn't undo the change; the entity stays Modified:
const name = customer.companyName; // entity is Unchanged
customer.companyName = 'Something else'; // Modified
customer.companyName = name; // still ModifiedrejectChanges restores the original values and returns the entity to Unchanged:
customer.companyName = 'Something else';
customer.entityAspect.rejectChanges();
// companyName is back to its original value; entityState is UnchangedTo revert every pending change in the cache, call em.rejectChanges(). It returns the entities it reverted.
propertyChanged
An entity's entityAspect.propertyChanged event fires whenever one of its tracked properties changes:
const token = order.entityAspect.propertyChanged.subscribe(args => {
args.entity; // the order
args.propertyName; // e.g. 'freight', or 'location.city' for a complex property
args.oldValue;
args.newValue;
});The arguments are a PropertyChangedEventArgs. Some operations change many properties at once: rejectChanges, or a query or save that merges new values into a cached entity. For those, Breeze raises a single propertyChanged with propertyName set to null.
It doesn't report state changes
entityState belongs to the EntityAspect, not to the entity, so a change from Unchanged to Modified doesn't raise propertyChanged. To hear about state changes, listen to entityChanged on the manager.
Unsubscribe when you are done
A subscription keeps its handler reachable from the entity. If the handler refers to a view or a component, then that object can't be garbage-collected while the entity is alive. subscribe returns a token. Pass it to unsubscribe once the listener is no longer needed:
order.entityAspect.propertyChanged.unsubscribe(token);If you are subscribing to many entities, subscribe once to the manager's entityChanged instead.
Using RxJS?
Every event on this page is also available as an observable from breeze-client/rxjs — entityChanged$(em), hasChanges$(em) and the rest — which tear down with the rest of your subscriptions. See RxJS.
EntityManager.entityChanged
The manager raises entityChanged for every change to an entity in its cache. Its arguments are an EntityChangedEventArgs:
entityActionsays what happenedentityis the entity it happened to — optional, becauseClearhas no single entityargsholds thepropertyChangedarguments, when the action is a property change
import { EntityAction } from 'breeze-client';
const token = em.entityChanged.subscribe(({ entityAction, entity, args }) => {
if (!entity) {
forgetEverything(); // only `Clear` arrives without one
return;
}
if (entityAction === EntityAction.PropertyChange) {
console.log(`${entity.entityType.shortName}.${args!.propertyName} changed`);
} else if (entityAction === EntityAction.EntityStateChange) {
console.log(`now ${entity.entityAspect.entityState.name}`);
}
});Test entity, not the action
Clear is the one action that arrives without an entity. Checking if (!entity) first both handles it and narrows the type, so the branches below need no !. Comparing entityAction instead would not narrow — EntityChangedEventArgs is a plain interface, not a discriminated union.
The EntityAction values are:
| EntityAction | When |
|---|---|
Attach | an entity was added or attached (addEntity, attachEntity, createEntity) |
AttachOnQuery | a query attached an entity |
AttachOnImport | an import attached an entity |
Detach | an entity was detached |
MergeOnQuery | a query merged new values into a cached entity |
MergeOnImport | an import merged new values into a cached entity |
MergeOnSave | a save merged the server's values into a cached entity |
PropertyChange | a property changed |
EntityStateChange | the entityState changed |
AcceptChanges | acceptChanges was called |
RejectChanges | rejectChanges was called |
Clear | the manager was cleared (entity is undefined) |
isAttach(), isDetach() and isModification() group them — isDetach() covers both Detach and Clear, which is the tidiest way to catch "this entity is no longer in the cache".
The order they arrive in
One operation usually raises more than one. EntityStateChange is raised by the state transition itself, and anything raised by the operation around that transition follows it — except Detach, which is raised from inside the transition and so comes first:
| what you did | what you get, in order |
|---|---|
createEntity / attachEntity | EntityStateChange, Attach |
| set a property on an Unchanged entity | EntityStateChange, PropertyChange |
| set another property on it | PropertyChange |
setDeleted() on an Unchanged entity | EntityStateChange |
setDeleted() on an Added entity | Detach, EntityStateChange |
detachEntity | Detach, EntityStateChange |
acceptChanges() on a Modified entity | EntityStateChange, AcceptChanges |
acceptChanges() on a Deleted entity | Detach, EntityStateChange, AcceptChanges |
rejectChanges() | EntityStateChange, RejectChanges |
em.clear() | Clear |
The entity's entityState is already final when any of them fires, so reading it off the entity gives the same answer whichever one you handle. The order matters only if you are sequencing side effects between two of them.
PropertyChange and entityAspect.propertyChanged are one-to-one: neither is ever raised twice for one change, and the entity's own event always comes before the manager's.
Noticing a deletion
There is no EntityAction.Delete. A deletion shows up as an EntityStateChange whose entity is now Deleted:
em.entityChanged.subscribe(({ entityAction, entity }) => {
if (entityAction === EntityAction.EntityStateChange
&& entity!.entityAspect.entityState.isDeleted()) {
// this entity is marked for deletion on the next save
}
});Deleting an Added entity detaches it instead
An entity that was never saved has nothing to tell the server about, so setDeleted() on an Added entity removes it from the cache outright — it ends up Detached, never Deleted, and raises Detach rather than the EntityStateChange above. To catch both, handle entityAction.isDetach() as well.
hasChanges and hasChangesChanged
em.hasChanges(); // any Added, Modified or Deleted entities?
em.hasChanges('Order'); // ... of this type (or an array of types)
em.getChanges(); // the changed entities
em.getChanges(['Order', 'OrderDetail']);hasChangesChanged fires only when the answer to hasChanges() flips, which makes it a good event to bind a Save button to:
em.hasChangesChanged.subscribe(({ hasChanges }) => {
saveButton.disabled = !hasChanges;
});With RxJS, hasChanges$ does the same and starts with the current value, so a button bound to it is right before anything has changed.
Validation errors
Breeze validates property values against rules from the metadata and rules you add (see Validation). Each EntityAspect holds the entity's current validation errors. When errors are added or removed, it raises validationErrorsChanged:
order.entityAspect.validationErrorsChanged.subscribe(({ entity, added, removed }) => {
// added and removed are arrays of ValidationError
});The manager has a validationErrorsChanged event too, for every entity in its cache.
By default, Breeze validates entities before saving them. If any entity fails, it sends none of them. Client-side validation is for the user's benefit; it doesn't replace validation on the server.
Turning events off
BreezeEvent.enable turns an event off or on, for an object and everything beneath it:
import { BreezeEvent } from 'breeze-client';
BreezeEvent.enable('propertyChanged', em, false); // every entity in em
BreezeEvent.enable('entityChanged', em, false);
BreezeEvent.enable('propertyChanged', em, true); // back onInstead of a boolean, the third argument can be a function that receives the object and returns a boolean. Breeze calls it each time the event fires.
Next
Once you have pending changes, save them.