I'm currently working at a MasterDetail application for my company, which provides expandable categorys represented as nodes.
The binding of the nodes and it's child nodes with a navigation property isn't a problem. However, if I want to search for a certain group node in the search field above, it only filters between the four highest nodes. It can search for the nodes on the first level, but it isn't able to find nodes if they're below the first level.
Binding of the tree:
<Tree
selectionChange="onSelectionChange"
id="list"
noDataText="{masterView>/noDataText}"
busyIndicatorDelay="{masterView>/delay}"
items="{path: '/GroupNodes',
parameters : {
expand: 'ChildGroupNodes',
navigation: {
'GroupNodes': 'ChildGroupNodes'
}
}
}">
<StandardTreeItem
title="{Stext}"
type="Navigation"
press="onSelectionChange"/>
</Tree>
onSearch:
onSearch: function(oEvent) {
if (oEvent.getParameters().refreshButtonPressed) {
this.onRefresh();
return;
}
var sQuery = oEvent.getParameter("query");
if (sQuery) {
this._oListFilterState.aSearch = [new Filter("Stext", FilterOperator.Contains, sQuery)];
} else {
this._oListFilterState.aSearch = [];
}
this._applyFilterSearch();
},
_applyFilterSearch:
_applyFilterSearch: function() {
var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter),
oViewModel = this.getModel();
this._oList.getBinding("items").filter(aFilters, "Application");
if (aFilters.length !== 0) {
oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText"));
} else if (this._oListFilterState.aSearch.length > 0) {
oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText"));
}
},
Filterstate in the onInit() function:
this._oListFilterState = {
aFilter: [],
aSearch: []
};
Metadata:
<EntityType Name="GroupNode" sap:content-version="1">
<Key>
<PropertyRef Name="Grpid"/>
</Key>
<Property Name="Grpid" Type="Edm.String" Nullable="false" MaxLength="8" sap:unicode="false" sap:label="Id Trainingsgruppe" sap:creatable="false" sap:updatable="false" sap:filterable="false"/>
<Property Name="Short" Type="Edm.String" MaxLength="12" sap:unicode="false" sap:label="Kürzel Trainingsgruppe" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
<Property Name="Stext" Type="Edm.String" MaxLength="40" sap:unicode="false" sap:label="Bezeichnung Trainingsgruppe" sap:creatable="false" sap:updatable="false" sap:filterable="false"/>
<Property Name="Begda" Type="Edm.DateTime" Precision="0" sap:unicode="false" sap:label="Beginndatum" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
<Property Name="Endda" Type="Edm.DateTime" Precision="0" sap:unicode="false" sap:label="Endedatum" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
<Property Name="Level" Type="Edm.Int32" sap:unicode="false" sap:label="Level" sap:creatable="false" sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
<Property Name="Parentid" Type="Edm.String" Nullable="false" MaxLength="8" sap:unicode="false" sap:label="ParentId" sap:creatable="false" sap:updatable="false" sap:filterable="false"/>
<NavigationProperty Name="ChildGroupNodes" Relationship="Z_HR_LSO_WORKCENTER_SRV.GroupNodeToParent" FromRole="FromRole_GroupNodeToParent" ToRole="ToRole_GroupNodeToParent"/>
<NavigationProperty Name="GroupToTrainingType" Relationship="Z_HR_LSO_WORKCENTER_SRV.GroupToTrainingType" FromRole="FromRole_GroupToTrainingType" ToRole="ToRole_GroupToTrainingType"/>
</EntityType>
We're working with OData V2, so there's no possibility to implement an FilterContains.All filter.
Is it even possible to filter through the child nodes of a sap.m.Tree in the front-end?
First of all, building the tree hierarchy via navigation is deprecated since 1.44. Instead, SAP recommends to leverage metadata annotations:
The use of navigation properties to build up the hierarchy structure is deprecated and it is recommended to use the hierarchy annotations [...].
Once the migration to the annotation approach is done, filter either client-side or server-side.
Client-side
The operationMode of the ODataTreeBinding (or the defaultOperationMode of the v2.ODataModel) needs to be "Client".
When calling .filter, the FilterType as the 2nd argument needs to be "Control".
Here is a working example: https://embed.plnkr.co/moTGOT
Server-side
For server-side filtering, only the FilterType "Application" and the operationMode: "Server" are supported. In that case, the server needs to respond to the $filter request with a ready-made tree structure. The same applies to paging requests for sibling and child nodes.
Limitations
suspended: true in the binding info is not yet supported (Issue #3161).
The above content applies to OData V2 only.
V4 ODataModel doesn't support tree binding at all yet (Issue #2728).
I think this may have something to do with the way you're binding the items, because I am able to filter on child nodes just fine in my example using your JS.
Check that this._oList.getItems() has all the items in it before you do the filter.
I'll post my code, so you can rebuild my project and get a feel for how it works. Let me know if you have more questions.
Page.view.xml
<mvc:View
controllerName="sap.m.sample.Tree.Page"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
<SearchField value="{json>/search}" search="onSearch"/>
<Tree
id="Tree"
items="{path: '/'}">
<StandardTreeItem title="{text}"/>
</Tree>
</mvc:View>
Page.controller.js
sap.ui.define(['sap/ui/core/mvc/Controller', 'sap/ui/model/json/JSONModel', 'sap/ui/model/Filter', 'sap/ui/model/FilterOperator'],
function(Controller, JSONModel, Filter, FilterOperator) {
"use strict";
var PageController = Controller.extend("sap.m.sample.Tree.Page", {
onInit: function(evt) {
// set explored app's demo model on this sample
var oModel = new JSONModel(jQuery.sap.getModulePath("sap.m.sample.Tree", "/Tree.json"));
this.getView().setModel(oModel);
var oJSONModel = new JSONModel();
this.getView().setModel("json", oJSONModel);
this._oList = this.byId("Tree");
this._oListFilterState = {
aFilter: [],
aSearch: []
};
},
onSearch: function(oEvent) {
if (oEvent.getParameters().refreshButtonPressed) {
this.onRefresh();
return;
}
var sQuery = oEvent.getParameter("query");
if (sQuery) {
this._oListFilterState.aSearch = [new Filter("text", FilterOperator.Contains, sQuery)];
} else {
this._oListFilterState.aSearch = [];
}
this._applyFilterSearch();
},
_applyFilterSearch: function() {
var aFilters = this._oListFilterState.aSearch.concat(this._oListFilterState.aFilter);
//oViewModel = this.getModel();
this._oList.getBinding("items").filter(aFilters, "Application");
// if (aFilters.length !== 0) {
// oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataWithFilterOrSearchText"));
// } else if (this._oListFilterState.aSearch.length > 0) {
// oViewModel.setProperty("/noDataText", this.getResourceBundle().getText("masterListNoDataText"));
// }
}
});
return PageController;
});
Tree.json
[
{
"text": "Node1",
"ref": "sap-icon://attachment-audio",
"nodes":
[
{
"text": "Node1-1",
"ref": "sap-icon://attachment-e-pub",
"nodes":[
{
"text": "Node1-1-1",
"ref": "sap-icon://attachment-html"
},
{
"text": "Node1-1-2",
"ref": "sap-icon://attachment-photo",
"nodes":[
{
"text": "Node1-1-2-1",
"ref": "sap-icon://attachment-text-file",
"nodes":[
{
"text": "Node1-1-2-1-1",
"ref": "sap-icon://attachment-video"
},
{
"text": "Node1-1-2-1-2",
"ref": "sap-icon://attachment-zip-file"
},
{
"text": "Node1-1-2-1-3",
"ref": "sap-icon://course-program"
}
]
}
]
}
]
},
{
"text": "Node1-2",
"ref": "sap-icon://create"
}
]
},
{
"text": "Node2",
"ref": "sap-icon://customer-financial-fact-sheet"
}
]
That's all you should need, but just incase you need these as well...
index.html
<!DOCTYPE HTML>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title>Tree - Basic</title>
<script id="sap-ui-bootstrap"
src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m"
data-sap-ui-theme="sap_belize"
data-sap-ui-xx-bindingSyntax="complex"
data-sap-ui-preload="async"
data-sap-ui-compatVersion="edge"
data-sap-ui-resourceroots='{"sap.m.sample.Tree": "./", "sap.ui.demo.mock": "mockdata"}'>
</script>
<!-- Application launch configuration -->
<script>
sap.ui.getCore().attachInit(function() {
new sap.m.App ({
pages: [
new sap.m.Page({
title: "Tree - Basic",
enableScrolling : true,
content: [ new sap.ui.core.ComponentContainer({
name : "sap.m.sample.Tree"
})]
})
]
}).placeAt("content");
});
</script>
</head>
<!-- UI Content -->
<body class="sapUiBody" id="content" role="application">
</body>
Component.js
sap.ui.define(['sap/ui/core/UIComponent'],
function(UIComponent) {
"use strict";
var Component = UIComponent.extend("sap.m.sample.Tree.Component", {
metadata : {
rootView : "sap.m.sample.Tree.Page",
dependencies : {
libs : [
"sap.m",
"sap.ui.layout"
]
},
config : {
sample : {
files : [
"Page.view.xml",
"Page.controller.js",
"Tree.json"
]
}
}
}
});
return Component;
});
Not sure if you managed to solve your problem, but I actually did this exact thing a few months ago.
In your controller, put the following function:
onSearch: function(oEvent) {
var searchVal = oEvent.getParameter("newValue");
var treeFilter = new sap.ui.model.Filter("Text", sap.ui.model.FilterOperator.Contains, searchVal);
var oBinding = this.getView().byId("tree").mBindingInfos.rows.binding;
oBinding.filter(treeFilter, FilterType.Application);
oBinding.expandToLevel(3);
},
And this should work straight away. Whatever you enter in the search box, it will populate the tree (up to 3 levels, but this can be changed) with your filter.
The onSearch function is of course executed on the livechange of the search field.
Related
I using a oData-Model that returns for .../User a list like that:
[
{
Id: 5
Name: "name",
Desc: "desc",
IsEditable: false,
Items: [
{ itemName: "Name1" },
{ itemName: "Name2" },
{ itemName: "Name3" },
{ itemName: "Name4" }
]
},
...
]
I bind this to my xml view like that:
<View binding="model>/User(5)">
<Text text="model>Name" />
<Text text="model>Desc" />
<List items="{model>Items}" >
<CustomListItem>
<input text="{model>itemName}" editable="{model>../IsEditable}" />
</CustomListItem>
</List>
</View>
So now i would like to disable the input by its parent "IsEditable" proppertie.
How i can make this happen?
To achieve this you have to edit your code like this:
(Relative binding in JSON Models without a "/", absolute with a "/")
<List items="{model>/Items}" >
<CustomListItem>
<Input value="{model>itemName}" editable="{model>/IsEditable}" />
</CustomListItem>
I have 2 weeks looking for an example to understand how the OData works.
I have defined in the Manifest.json my url with the OData service
{
"_version" : "1.7.0",
"sap.app" : {
"id" : "test",
"type" : "application",
"i18n" : "i18n/i18n.properties",
"applicationVersion": {
"version" : "1.0.0"
},
"title" : "{{appTitle}}",
"description" : "{{appDescription}}",
"sourceTemplate": {
"id" : "servicecatalog.connectivityComponent",
"version" : "0.0.0"
},
"dataSources" : {
"Test" : {
"uri" : "/sap/opu/odata/sap/ZMY_SERVICE_SRV/",
"type" : "OData",
"settings" : {
"odataVersion" : "2.0",
"localUri" : "localService/metadata.xml"
}
}
}
}..
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "test.i18n.i18n"
}
},
"Test": {
"type": "sap.ui.model.odata.v2.ODataModel",
"settings": {
"defaultOperationMode": "Server",
"defaultBindingMode": "TwoWay",
"defaultCountMode": "None"
},
"dataSource": "Test"
},
and in my Component.js within the Init method:
init: function() {
// call the base component's init function
UIComponent.prototype.init.apply(this, arguments);
// create the views based on the url/hash
this.getRouter().initialize();
// set the device model
this.setModel(models.createDeviceModel(), "device");
var sServiceUrl = this.getMetadata().getManifestEntry("sap.app").dataSources["Test"].uri;
var oModel = new sap.ui.model.odata.v2.ODataModel(sServiceUrl);
this.setModel(sabModel, "/Test");
sabModel.read(sServiceUrl, "Test");
}
I don´t want to use a Table or a List to load the OData from Backend. I want to load the information "manually" and based on what I got from the Backend, I want to navigate to one or another View.
Debugging the result in the navigator I see the following error: Console
Checking the Error Log I can see: Error Log
If I test the service in Backend works fine: OData Service
I got:
<?xml version="1.0"?><feed xml:base="http://Myserver:8000/sap/opu/odata/sap/ZMY_SERVICE_SRV/"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom">
<id>http://Myserver:8000/sap/opu/odata/sap/ZMY_SERVICE_SRV/TestSet</id>
<title type="text">TestSet</title>
<updated>2017-10-23T20:37:55Z</updated>
-<author>
<name/>
</author>
<link title="TestSet" rel="self" href="TestSet"/>
-<entry>
<id>http://Myserver:8000/sap/opu/odata/sap/ZMY_SERVICE_SRV/TestSet('1')</id>
<title type="text">TestSet('1')</title>
<updated>2017-10-23T20:37:55Z</updated>
<category scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" term="ZMY_SERVICE_SRV.Test"/>
<link title="Test" rel="self" href="TestSet('1')"/>
-<content type="application/xml">
-<m:properties xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<d:Pernr>1</d:Pernr>
<d:Nachn>TestUser</d:Nachn>
<d:Vorna>UserTest</d:Vorna>
<d:SavingDate m:null="true"/>
<d:Option/>
<d:MsgType/>
<d:MsgNumb/>
</m:properties>
</content>
</entry>
Thank you for your help!!! Any input is more than welcome!!
try this
var oModel = this.getModel("Test");//get the model
oModel.read("/TestSet", {
method: "GET",
success: function(data) {
alert(JSON.stringify(data));
},
error: function() {
}
});
Read the API documentation about "read" method:
https://sapui5.netweaver.ondemand.com/#/api/sap.ui.model.odata.v2.ODataModel
You should not specify the service URL in read method, because it automatically gets concatenated with "sPath", which you pass as a first argument. You should use the name of entityset (is defined in $metadata) you want to read starting with slash "/".
I've a shell container and on big screens i want to utilize full with of screen. i want to cover full area. how i can customize it.
I assume you are using XML for your views. Add the following attribute appWidthLimited="false" to the Shell tag.
When working with a manifest.json file and the UI5-framework instantiates a shell control, do the following (appWidthLimited="false" cannot be used as you don't have a xml containing a shell 'tag').
manifest.json
...
"sap.ui5": {
"config": {
"fullWidth": true
},
...
...
As per latest documentation, I referred to 1.48.X, and it's not there in the sap.ui5 anymore:
"sap.ui": {
"technology": "UI5",
"icons": {
"icon": "sap-icon://add-contact",
"favIcon": "icon/F1373_Approve_Purchase_Orders.ico",
"phone": "icon/launchicon/57_iPhone_Desktop_Launch.png",
"phone#2": "icon/launchicon/114_iPhone-Retina_Web_Clip.png",
"tablet": "icon/launchicon/72_iPad_Desktop_Launch.png",
"tablet#2": "icon/launchicon/144_iPad_Retina_Web_Clip.png"
},
"deviceTypes": {
"desktop": true,
"tablet": true,
"phone": false
},
"supportedThemes": [
"sap_hcb"
],
"fullWidth": true
},
For more info: https://openui5.hana.ondemand.com/#/topic/be0cf40f61184b358b5faedaec98b2da
Also, if you are using sap.m.Shell, then the above will not help.
For that you need to set the property appWidthLimited: false:
<script>
sap.ui.getCore().attachInit(function () {
new sap.m.Shell({
app: new sap.ui.core.ComponentContainer({
height: "100%",
name: "APPNAME"
}),
appWidthLimited: false
})
.placeAt("content");
});
</script>
It can be done either statically, via XML-template:
<mvc:View controllerName="letterboxing.widescreen.controller.index" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m">
<Shell id="shell" appWidthLimited="false">
<App id="app">
<pages>
<Page id="page" title="{i18n>title}">
<content></content>
</Page>
</pages>
</App>
</Shell>
</mvc:View>
Or dynamically via JS-controller, which will set appWidthLimited:false to the sap.m.Shell.
i'm using SAPUI5 Version gt 1.30 and I try to define the automatic model instantiation of two external services within the manifest.
My first question is, are more than one odata services allowed? Sorry, but i'cant find it out by the documentation.
The default datasource ("") works. But the second datasource ("HLA") not.
If more than once are allowed, please could somebody look for the right definition.
"sap.app":
"dataSources": {
"mainService": {
"uri": "path.xsodata/",
"type": "OData",
"settings": {
"odataVersion": "2.0",
"localUri": "localService/metadata.xml"
}
}
"secondService": {
"uri": "/path.......xsodata/",
"type": "OData",
"settings": {
"annotations": [],
"odataVersion": "2.0",
"localUri": ""
}
}
},
"sap.ui5":
"models": {
"i18n": {
"type": "sap.ui.model.resource.ResourceModel",
"settings": {
"bundleName": "....i18n.i18n"
}
},
"": {
"dataSource": "mainService",
"settings": {
"metadataUrlParams": {
"sap-documentation": "heading"
}
}
},
"HLA": {
"dataSource": "secondService",
"settings": {
"metadataUrlParams": {
"sap-documentation": "heading"
}
}
}
}
Thanks a lot!
Not sure if you have solved your problem, but I encountered the same problem today too. And I realise you need to use different notations when binding those two data sources to a control. I'm using XML view btw.
For the default "" model:
<Table id="table" inset="true" items="{/}" >
<columns>
<Column>
<Text text="ID"/>
</Column>
<Column>
<Text text="Name"/>
</Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Text text="{id}"/>
<Text text="{name}"/>
</cells>
</ColumnListItem>
</items>
</Table>
For the second model named "Users":
<Table id="userTable" inset="true" items="{Users>/}">
<columns>
<Column>
<Text text="ID"/>
</Column>
<Column>
<Text text="Last Name"/>
</Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Text text="{Users>empNumber}"/>
<Text text="{Users>lastName}"/>
</cells>
</ColumnListItem>
</items>
</Table>
It works for me :)
Of course you can have more than one data source, but it is recommended by the fiori guidelines to only use one.
Do you want your address to look like this path.......xsodata/ ?
Also I think you should remove the localUri from the second data source.
Because if there is no metadata document this might throw an arror.
"secondService": {
"uri": "/proxy/service.xsodata",
"type": "oData",
"settings": {
"odataVersion": "2.0"
}
}
What is the output of the debugger? Do you get an error?
Also your oData model should have a type.
"HLA": {
"dataSource": "secondService",
"type": "sap.ui.model.odata.v2.ODataModel"
}
I am using breezejs v.1.5.4 with OData Web Api controllers (and AngularJS v.1.4.0 if it makes a difference).
I have the following models (simplified):
public partial class Job
{
...
[Required]
[StringLength(128)]
[Index("IX_WorkDoneById")]
[Display(Name = "Work Done By Id")]
public string WorkDoneById { get; set; }
[ForeignKey("WorkDoneById")]
public virtual User WorkDoneBy { get; set; }
}
[DataContract]
public partial class User : IdentityUser
{
...
[Key]
[StringLength(128)]
[Display(Name = "Id")]
[DataMember]
public override string Id
{
get
{
return base.Id;
}
set
{
base.Id = value;
}
}
[InverseProperty("WorkDoneBy")]
[DataMember]
public virtual ICollection<Job> Jobs { get; set; }
}
When trying to get a Job information and expanding the WorkDoneBy, it works and I get the user information (i.e. the user is binded to the job). While when I try to get the Jobs associated with the user, I get an empty array. I inspected the network and the Jobs are transmitted with the server response but not attached to the user instance.
My JS query is like this:
var query = new breeze.EntityQuery()
.from("Users")
.expand("Jobs")
.where(new breeze.Predicate("Id", "eq", "Some long Guid"));
Any suggestions ??
Update 1
Also I am using datajs v.1.1.3 and odata service adapter.
Below is the metadata:
{
"metadataVersion": "1.0.5",
"namingConvention": "noChange",
"localQueryComparisonOptions": "caseInsensitiveSQL",
"dataServices": [
{
"serviceName": "odata/",
"adapterName": "odata",
"uriBuilderName": "odata",
"hasServerMetadata": true,
"jsonResultsAdapter": "OData_default",
"useJsonp": false
}
],
"structuralTypes": [
{
"shortName": "Job",
"namespace": "MyApp.Models",
"autoGeneratedKeyType": "None",
"defaultResourceName": "Jobs",
"dataProperties": [
{
"name": "JobId",
"dataType": "Guid",
"isNullable": false,
"defaultValue": "00000000-0000-0000-0000-000000000000",
"isPartOfKey": true,
"validators": [{ "name": "required" }, { "name": "guid" }]
},
{
"name": "WorkDoneById",
"dataType": "String",
"isNullable": false,
"defaultValue": "",
"validators": [{ "name": "required" }, { "name": "string" }]
}
],
"navigationProperties": [
{
"name": "WorkDoneBy",
"entityTypeName": "User:#MyApp.Models",
"isScalar": true,
"associationName": "MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner"
}
]
},
{
"shortName": "User",
"namespace": "MyApp.Models",
"autoGeneratedKeyType": "None",
"defaultResourceName": "Users",
"dataProperties": [
{
"name": "Id",
"dataType": "String",
"isNullable": false,
"defaultValue": "",
"isPartOfKey": true,
"validators": [{ "name": "required" }, { "name": "string" }]
}
],
"navigationProperties": [
{
"name": "Jobs",
"entityTypeName": "Job:#MyApp.Models",
"isScalar": false,
"associationName": "MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner"
}
]
}
],
"resourceEntityTypeMap":
{
"Jobs": "Job:#MyApp.Models",
"Users": "User:#MyApp.Models"
}
}
and this is breeze configuration:
var dataService = new breeze.DataService({
adapterName: "odata",
hasServerMetadata: false, // don't ask the server for metadata
serviceName: "odata",
uriBuilderName: "odata",
});
// create the metadataStore
var metadataStore = new breeze.MetadataStore();
// initialize the store from the application's metadata variable
metadataStore.importMetadata(Models.metaData);
// Apply additional functions and properties to the models
metadataStore.registerEntityTypeCtor("Job", Models.Job);
metadataStore.registerEntityTypeCtor("User", Models.User);
// Initializes entity manager.
this.entityManager = new breeze.EntityManager(
{ dataService: dataService, metadataStore: metadataStore }
);
Update 2
Metadata generated from the server odata/$metadata:
<edmx:Edmx Version="1.0">
<edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0">
<Schema Namespace="MyApp.Models">
<EntityType Name="Job">
<Key>
<PropertyRef Name="JobId"/>
</Key>
<Property Name="JobId" Type="Edm.Guid" Nullable="false"/>
<Property Name="WorkDoneById" Type="Edm.String" Nullable="false"/>
<NavigationProperty Name="WorkDoneBy" Relationship="MyApp.Models.MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner" ToRole="WorkDoneBy" FromRole="WorkDoneByPartner"/>
</EntityType>
<EntityType Name="User">
<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.String" Nullable="false"/>
<NavigationProperty Name="Jobs" Relationship="MyApp.Models.MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner" ToRole="Jobs" FromRole="JobsPartner"/>
</EntityType>
<Association Name="MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner">
<End Type="MyApp.Models.User" Role="WorkDoneBy" Multiplicity="0..1"/>
<End Type="MyApp.Models.Job" Role="WorkDoneByPartner" Multiplicity="0..1"/>
</Association>
<Association Name="MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner">
<End Type="MyApp.Models.Job" Role="Jobs" Multiplicity="*"/>
<End Type="MyApp.Models.User" Role="JobsPartner" Multiplicity="0..1"/>
</Association>
</Schema>
<Schema Namespace="Default">
<EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
<EntitySet Name="Jobs" EntityType="MyApp.Models.Job"/>
<EntitySet Name="Users" EntityType="MyApp.Models.User"/>
<AssociationSet Name="MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartnerSet" Association="MyApp.Models.MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner">
<End Role="WorkDoneByPartner" EntitySet="Jobs"/>
<End Role="WorkDoneBy" EntitySet="Users"/>
</AssociationSet>
<AssociationSet Name="MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartnerSet" Association="MyApp.Models.MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner">
<End Role="JobsPartner" EntitySet="Users"/>
<End Role="Jobs" EntitySet="Jobs"/>
</AssociationSet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
According to your configuration code, you appear to be using locally defined metadata
although it's a bit confusing because the metadata object shown earlier in your question is attached to a dataservice that says "hasServerMetadata": true. I don't know that this inner dataservice actually matters.
I'm not positive but I suspect that the problem is your association names; they are different for the two navigation properties:
// Job
...
"navigationProperties": [
{
"name": "WorkDoneBy",
...
"associationName": "MyApp_Models_Job_WorkDoneBy_MyApp_Models_User_WorkDoneByPartner"
}
]
// User
...
"navigationProperties": [
{
"name": "Jobs",
...
"associationName": "MyApp_Models_User_Jobs_MyApp_Models_Job_JobsPartner"
}
]
If you want Breeze to pair up these properties, the associationName must be the same string value. The value itself doesn't matter; just the fact that both property ends of the relationship have the same associationName. That's how Breeze learns that these properties are mated.
Try something nice and short that spells out the underlying relationship ... something like "User_Jobs" or "Job.WorkDoneBy_User.Jobs"
Update #1
It is a mystery how you got those different associationName values.
In a comment I asked that you look at the raw metadata coming from your OData source.
Here is an example from the "ODataBreezejsSample" that gets metadata from an OData v.3 source.
The OData nuget package is "Microsoft.AspNet.WebApi.OData" version="5.2.2". The sample uses the EdmBuilder class as explained in the documentation
There are two types - TodoList and TodoItem - that have a one-to-many relationship. The pertinent raw metadata XML is:
// TodoList
<NavigationProperty Name="TodoItems" Relationship="ODataBreezejsSample.Models.TodoList_TodoItems" ... />
// TodoItem
<NavigationProperty Name="TodoList" Relationship="ODataBreezejsSample.Models.TodoList_TodoItems" ... />
Note they have the same Relationship name: "ODataBreezejsSample.Models.TodoList_TodoItems"
Then I inspect the corresponding navigation properties in the client-side metadata that Breeze produces from this XML. Both properties share the associationName of "TodoList_TodoItems" ... equal to the Relationship name stripped of the namespace.
What kind of OData source (and what OData version) are you querying? Are you using the EdmBuilder class to generate the metadata?
Update #2
So you're using the "Microsoft ASP.NET Web API 2.2 for OData v4.0" v.5.6 nuget package! That means you're using OData v.4.
What a PITA!
This is a huge breaking change from v.5.5.x, the last of the OData v.3 nuget packages.
How could they make that big a leap and re-use the version numbers, especialy the major version digit? It boggles the mind.
To really confuse things, there are now two nuget package names that are just slightly different:
v3: https://www.nuget.org/packages/Microsoft.AspNet.WebApi.OData/
v4: https://www.nuget.org/packages/Microsoft.AspNet.OData/
Did you happen to notice the "WebApi" in the middle of the v.3 package name? I didn't at first.
The bad news is that their implementation of v.4 broke everything ... again ... including metadata. And ... again ... they failed to follow the OData spec, especially w/r/t navigation properties in metadata.
Consequently, Breeze does not yet work with Web API OData v.4 metadata ... and there are other problems as well.
We are in the process of working through the issues with the Microsoft OData team. Until then, you're options are to wait for us or go back to OData v.3.
Also critical: the datajs 3rd party client-side JavaScript library that we have all used for OData v.1-3 does not work with OData v.4. Like everyone else, you'll have to switch to the olingo library. It may be that the olingo library plays a constructive role in rationalizing navigation property metadata. I don't know and our expert on the subject is not available at the moment.
Yes ... this is a mess.
Rather then querying you can use fetchEntityByKey on manager. Which will fetch entity by key.
function getAJobDetail(){
return manager.fetchEntityByKey(
"Jobs", "Some long Guid", true)
.then(fetchSucceeded)
.fail(queryFailed);
function fetchSucceeded(data) {
var s = data.entity;}
function queryFailed(error) {
var msg = '[datacontext.js] Error retrieving data. ' + error.message;
//logError(msg, error);
throw error;
}
}
Note : this method will work only if that your key which is some Guid must be a primary key, else you have to use predicates and compare the fields then query the breeze.