breezejs:非标量导航属性是只读的
breezejs: Nonscalar navigation properties are readonly
我有以下元数据:
var entityTypeParent = {
shortName: 'ParentItemType',
namespace: 'MyNamespace',
autoGeneratedKeyType: Identity,
defaultResourceName: 'ParentItemTypes',
dataProperties: {
id: { dataType: DT.Int32, isPartOfKey: true }
},
navigationProperties: {
users: {
entityTypeName: 'User',
isScalar: false,
associationName: 'ParentItem_User'
}
}
};
var entityTypeUser = {
shortName: 'User',
namespace: 'MyNamespace',
autoGeneratedKeyType: Identity,
defaultResourceName: 'Users',
dataProperties: {
loginName: { dataType: DT.String, isPartOfKey: true },
displayText: {},
parentItemId: {
dataType: DT.Int32
}
},
navigationProperties: {
agendaTask: {
entityTypeName: 'ParentItemType',
associationName: 'ParentItem_User',
foreignKeyNames: ['parentItemId']
}
}
};
在UI我有一个控件(KendoMulti-Select),它绑定到ParentItemType.users属性(AngularJS绑定),它允许 select 列表中的用户(使用 breeze-kendo 网桥和 'webApiOData' 适配器检索)。
选择用户会导致 setNpValue (https://github.com/Breeze/breeze.js/blob/397b2a02aa2173175c304eb1b37332f1656db6f5/src/a35_defaultPropertyInterceptor.js#L287) 中出现 "Error: Nonscalar navigation properties are readonly - entities can be added or removed but the collection may not be changed." 异常。
如果我将定义更改为 isScalar eq true。我在 https://github.com/Breeze/breeze.js/blob/397b2a02aa2173175c304eb1b37332f1656db6f5/src/a35_defaultPropertyInterceptor.js#L298
处遇到异常
context.newValue 是一个用户实体数组。
这是我的元数据定义中某处的错误吗?其实我只是想在我的 ParentItem 中有多个用户。
Breeze版本:1.5.4
不,这不是您的元数据定义中的错误。这是设计使然。非标量导航属性由 Breeze 管理。 Multi-select 控件尝试获取值数组的所有权并将其替换为新数组。 Breeze 不允许这样做,因为这将是灾难性的,会破坏您的实体。
Multi-select 控件有点麻烦。您需要利用控件触发的事件在用户进行选择时更新导航 属性 并根据所选值手动创建关联实体,并在用户取消选择时删除它们。我没有 KendoUI 的示例,但是 FWIW,这是 DevExpress TagBox 和以下实体模型的 Angular2 示例。
模型(主题)1<--->n SubjectVehicleAssociation n<--->1 LookupItem(车辆)
涉及一些工作。开箱即用的 multi-select 控件仅适用于基本数组。
<dx-tag-box [dataSource]="adapter.dataSource"
displayExpr="description"
[value]="adapter.value"
[searchEnabled]="true"
(onSelectionChanged)="adapter.selectionChanged($event)">
</dx-tag-box>
在组件中创建适配器实例:
let valueConverter: ITagBoxValueConverter<SubjectVehicleAssociation, { code: string, longValue: string }> = {
match: (item: SubjectVehicleAssociation, lookupItem: { code: string, longValue: string }) => item.abbreviation === lookupItem.code,
seed: (item: SubjectVehicleAssociation, lookupItem: { code: string, longValue: string }) => {
item.description = lookupItem.longValue;
item.abbreviation = lookupItem.code;
return item;
}
};
this.adapter = new TagBoxAdapter(SubjectVehicleAssociation,
() => Promise.resolve(this.vehicleAssociationsLookup),
this.modelSubjectVehicleRelation,
'associations',
valueConverter);
以及适配器实现:
import DataSource from 'devextreme/data/data_source';
import * as _ from 'lodash';
import { Entity, EntityState } from 'breeze-client';
export interface ITagBoxValueConverter<T extends Entity, W> {
match(value1: T, value2: W): boolean;
seed(target: T, source: W): T;
}
export class TagBoxAdapter<T extends Entity, W> {
dataSource: DataSource;
constructor(private itemType: { new (): T },
private data: (searchValue?: string, maxSearchResults?: number) => Promise<W[]>,
private model: any,
private valueExpr: string,
private valueConverter: ITagBoxValueConverter<T, W>,
private maxSearchResults: number = 20) {
this.initializeDataSource();
}
get value(): T[] {
return _.get<T[]>(this.model, this.valueExpr, []);
}
selectionChanged(e: { addedItems: T[], removedItems: T[] }) {
e.addedItems.forEach(item => {
if (item.entityAspect && item.entityAspect.entityState.isDeleted()) {
// Real entity, needs to be resurrected
item.entityAspect.rejectChanges();
} else {
// Placeholder entity, needs to be attached first
this.model.entityAspect.entityManager.attachEntity(item, EntityState.Added);
this.value.push(item);
}
})
e.removedItems.forEach(item => {
item.entityAspect.setDeleted();
})
};
private initializeDataSource() {
let sourceData = (searchValue: string, maxSearchResults: number) => {
return this.data(searchValue, maxSearchResults).then((results) => {
return results.map(dataItem => {
// Find existing association entity if exists
let item = _.find(this.value, item => this.valueConverter.match(item, dataItem));
if (item) return item;
// Not associated yet, return placeholder association entity
return this.valueConverter.seed(new this.itemType(), dataItem);
})
});
};
this.dataSource = new DataSource({
// The LoadOptions interface is defined wrong it's lacking the search properties
load: (loadOptions: any) => {
let searchValue = loadOptions.searchValue ? loadOptions.searchValue : "";
return sourceData(searchValue, this.maxSearchResults).then(data => data.filter(item => !_.intersection(this.value, [item]).length));
},
byKey: (key) => {
return sourceData(key, 1).then((data) => _.find(data, key));
}
});
}
}
我有以下元数据:
var entityTypeParent = {
shortName: 'ParentItemType',
namespace: 'MyNamespace',
autoGeneratedKeyType: Identity,
defaultResourceName: 'ParentItemTypes',
dataProperties: {
id: { dataType: DT.Int32, isPartOfKey: true }
},
navigationProperties: {
users: {
entityTypeName: 'User',
isScalar: false,
associationName: 'ParentItem_User'
}
}
};
var entityTypeUser = {
shortName: 'User',
namespace: 'MyNamespace',
autoGeneratedKeyType: Identity,
defaultResourceName: 'Users',
dataProperties: {
loginName: { dataType: DT.String, isPartOfKey: true },
displayText: {},
parentItemId: {
dataType: DT.Int32
}
},
navigationProperties: {
agendaTask: {
entityTypeName: 'ParentItemType',
associationName: 'ParentItem_User',
foreignKeyNames: ['parentItemId']
}
}
};
在UI我有一个控件(KendoMulti-Select),它绑定到ParentItemType.users属性(AngularJS绑定),它允许 select 列表中的用户(使用 breeze-kendo 网桥和 'webApiOData' 适配器检索)。
选择用户会导致 setNpValue (https://github.com/Breeze/breeze.js/blob/397b2a02aa2173175c304eb1b37332f1656db6f5/src/a35_defaultPropertyInterceptor.js#L287) 中出现 "Error: Nonscalar navigation properties are readonly - entities can be added or removed but the collection may not be changed." 异常。
如果我将定义更改为 isScalar eq true。我在 https://github.com/Breeze/breeze.js/blob/397b2a02aa2173175c304eb1b37332f1656db6f5/src/a35_defaultPropertyInterceptor.js#L298
处遇到异常context.newValue 是一个用户实体数组。
这是我的元数据定义中某处的错误吗?其实我只是想在我的 ParentItem 中有多个用户。
Breeze版本:1.5.4
不,这不是您的元数据定义中的错误。这是设计使然。非标量导航属性由 Breeze 管理。 Multi-select 控件尝试获取值数组的所有权并将其替换为新数组。 Breeze 不允许这样做,因为这将是灾难性的,会破坏您的实体。
Multi-select 控件有点麻烦。您需要利用控件触发的事件在用户进行选择时更新导航 属性 并根据所选值手动创建关联实体,并在用户取消选择时删除它们。我没有 KendoUI 的示例,但是 FWIW,这是 DevExpress TagBox 和以下实体模型的 Angular2 示例。
模型(主题)1<--->n SubjectVehicleAssociation n<--->1 LookupItem(车辆)
涉及一些工作。开箱即用的 multi-select 控件仅适用于基本数组。
<dx-tag-box [dataSource]="adapter.dataSource"
displayExpr="description"
[value]="adapter.value"
[searchEnabled]="true"
(onSelectionChanged)="adapter.selectionChanged($event)">
</dx-tag-box>
在组件中创建适配器实例:
let valueConverter: ITagBoxValueConverter<SubjectVehicleAssociation, { code: string, longValue: string }> = {
match: (item: SubjectVehicleAssociation, lookupItem: { code: string, longValue: string }) => item.abbreviation === lookupItem.code,
seed: (item: SubjectVehicleAssociation, lookupItem: { code: string, longValue: string }) => {
item.description = lookupItem.longValue;
item.abbreviation = lookupItem.code;
return item;
}
};
this.adapter = new TagBoxAdapter(SubjectVehicleAssociation,
() => Promise.resolve(this.vehicleAssociationsLookup),
this.modelSubjectVehicleRelation,
'associations',
valueConverter);
以及适配器实现:
import DataSource from 'devextreme/data/data_source';
import * as _ from 'lodash';
import { Entity, EntityState } from 'breeze-client';
export interface ITagBoxValueConverter<T extends Entity, W> {
match(value1: T, value2: W): boolean;
seed(target: T, source: W): T;
}
export class TagBoxAdapter<T extends Entity, W> {
dataSource: DataSource;
constructor(private itemType: { new (): T },
private data: (searchValue?: string, maxSearchResults?: number) => Promise<W[]>,
private model: any,
private valueExpr: string,
private valueConverter: ITagBoxValueConverter<T, W>,
private maxSearchResults: number = 20) {
this.initializeDataSource();
}
get value(): T[] {
return _.get<T[]>(this.model, this.valueExpr, []);
}
selectionChanged(e: { addedItems: T[], removedItems: T[] }) {
e.addedItems.forEach(item => {
if (item.entityAspect && item.entityAspect.entityState.isDeleted()) {
// Real entity, needs to be resurrected
item.entityAspect.rejectChanges();
} else {
// Placeholder entity, needs to be attached first
this.model.entityAspect.entityManager.attachEntity(item, EntityState.Added);
this.value.push(item);
}
})
e.removedItems.forEach(item => {
item.entityAspect.setDeleted();
})
};
private initializeDataSource() {
let sourceData = (searchValue: string, maxSearchResults: number) => {
return this.data(searchValue, maxSearchResults).then((results) => {
return results.map(dataItem => {
// Find existing association entity if exists
let item = _.find(this.value, item => this.valueConverter.match(item, dataItem));
if (item) return item;
// Not associated yet, return placeholder association entity
return this.valueConverter.seed(new this.itemType(), dataItem);
})
});
};
this.dataSource = new DataSource({
// The LoadOptions interface is defined wrong it's lacking the search properties
load: (loadOptions: any) => {
let searchValue = loadOptions.searchValue ? loadOptions.searchValue : "";
return sourceData(searchValue, this.maxSearchResults).then(data => data.filter(item => !_.intersection(this.value, [item]).length));
},
byKey: (key) => {
return sourceData(key, 1).then((data) => _.find(data, key));
}
});
}
}