I don't understand something about collection navigation properties.
First, I went through metadata debug, all checks out. As per documentation, I expected the navigation collection property to be observable, e.g. I can get the collection via
Foo.Bar() // bar is KO observable array
However, I can't get it working like that.
My query:
new breeze.EntityQuery()
.from('Classes')
.where('ClassId', '==', id)
.expand('LessonOfClasses')
Results to:
[
{
"$id":"1",
"$type":"Model.Class, Model",
"ClassId":24,
"LessonOfClasses":[
{
"$id":"2",
"$type":"Model.LessonOfClass, Model",
"class_lesson_id":30,
"class_id":24,
"lesson_id":40,
"Class":{
"$ref":"1"
},
"Lesson":null
},
// other instances of LessonOfClass...
]
// other properties of Class...
}
[
Which looks fine to me. However, when I run the query the result is:
var classObject = data.results[0];
classObject.LessonOfClasses // returns normal array
classObject.LessonOfClasses() // error: not a function
Figured out when knockout isn't available as a dependency, Breeze results to using plain JSON objects.
The problem is I'm using knockout as a require.js dependency and the global ko reference wasn't set.
requirejs(['knockout'], function(ko) {
window.ko = ko;
})
Breeze is looking strictly for ko, as opposed to durandal which I think looks for knockout.
Related
I'm having trouble with a knockout model that is not binding on a subscribed update. I have a C# MVC page that delivers a model to the template which is parsed to Json and delivered raw as part of a ViewModel assignment for ko.applyBindings. I have a subscription to an observable that calls a method to perform an update of the viewModel's data. Irrelevant stuff pulled out and renamed for example usage:
var myViewModel = function (data) {
var self = this;
self.CurrentPage = ko.observable();
self.SomeComplexArray= ko.observableArray([]);
self.Pager().CurrentPage.subscribe(function (newPage) {
self.UpdateMyViewModel(newPage);
});
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
ko.mapping.fromJS(data, {}, self);;
});
};
When I perform logging, I can see all of the data, and it all looks correct. The same method is used to produce both the initial model and the updated model. I've used this technique on other pages and it worked flawlessly each time. In this case however, I'm looking for it to bind/update SomeComplexArray, and that's just not happening. If I attempt to do it manually, I don't get a proper bind on the array I get blank. I'm wondering if there is something obvious that I'm doing wrong that I'm just flat out missing.
Edit: I don't know that ko.mapping can be pointed to as the culprit. Standard model changes are also not affecting the interface. Here is something that is not working in a bound sense. I have a p element with visible bound to the length of the array and a div element with a click bound to a function that pops items off of SomeComplexArray. I can see in the console log that it is performing its function (and subsequent clicks result in 'undefined' not having that function). However, the p element never displays. The initial array has only 2 items so a single click empties it:
<p data-bind="visible: SomeComplexArray().length === 0">nothing found</p>
<div data-bind="click: function() { UpdateArray(); }">try it manually</div>
-- in js model
self.UpdateArray = function () {
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
console.log(self.SomeComplexArray().pop());
console.log(self.SomeComplexArray());
});
Edit 2: from the comment #Matt Burland, I've modified how the pop is called and the manual method now works to modify the elements dynamically. However, the ko.mapping is still not functioning as I would expect. In a test, I did a console.log of a specific row before calling ko.mapping and after. No change was made to the observableArray.
I created a test of your knockout situation in JSFiddle.
You have to call your array function without paranthesis. I tested this part:
self.UpdateArray = function () {
self.SomeComplexArray.pop();
};
It seems to be working on JSFiddle side.
I'm not really sure why, but it would seem that ko.mapping is having difficulty remapping the viewmodel at all. Since none of the fields are being mapped into self my assumption is that there is an exception occurring somewhere that ko.mapping is simply swallowing or it is not being reported for some other reason. Given that I could manually manipulate the array with a helpful tip from #MattBurland, I decided to backtrack a bit and update only the elements that needed to change directly on the data load. I ended up creating an Init function for my viewModel and using ko.mapping to populate the items directly there:
self.Init = function (jsonData) {
self.CurrentPage(0);
self.Items(ko.mapping.fromJS(jsonData.Items)());
self.TotalItems(jsonData.TotalItems);
// More stuff below here not relevant to question
}
The primary difference here is that the ko.mapping.fromJS result needed to be called as a function before the observableArray would recognize it as such. Given that this worked and that my controller would be providing an identical object back during the AJAX request, it was almost copy/past:
self.UpdateMyViewModel= function (newPage) {
var postData = { PageNumber: newPage };
$.post('/Article/GetMyModelSearchByPage', postData, function (data) {
self.Items(ko.mapping.fromJS(JSON.parse(data).Items)());
});
};
This is probably not ideal for most situations, but since there is not a large manipulation of the viewModel occurring during the update this provides a working solution. I would still like to know why ko.mapping would not remap the viewModel at the top level, but in retrospect it probably would have been a disaster anyway since there was "modified" data in the viewModel that the server would have had to replace. This solution is quick and simple enough.
Does someone know if there is a way to create a OData Service that is bindable to an SAPUI5 TreeTable element?
According to the example from here:
https://sapui5.netweaver.ondemand.com/sdk/#test-resources/sap/ui/table/demokit/TreeTable.html
The recursive structure of the data is a list. For example:
//Define some sample data
var oData = {
root:{
name: "root",
description: "root description",
checked: false,
0: {
name: "item1",
description: "item1 description",
checked: true,
...
I expected to solve that problem with an navigation at the service side. Like
Element with attributes and one attribute points to a list of Elements.
But this would not be the same.
I also detected this thread, but the answer is one year old:
http://scn.sap.com/thread/3389546
All in all, is the answer still true? Or is there a solution, so that the data binding works?
You can use treebinding from ODataModel. First of all, prepare backend. You need to cycle navigation in your entity set. For example, you have entity set ItemCollection and you add navigation property ItemNavigation which refers to ItemCollection. After just bind tree to your TreeTable:
var oTreeTable = this.getView().byId("myTreeTable");
oTreeTable.bindRows({
path : '/ItemCollection',
properties : {
navigation : {
'ItemCollection' : 'ItemNagigation'
}
}
});
Should work fine but be aware to use it with Tree control. TreeTable loads navigation item by click on its parent and Tree loads the whole tree at once.
In UI5, is it possible to bind a single attribute of a single entity to a control property if your model is an OData? Binding works ok if you bind an aggregation to an entity set but does not seem to work with properties to entities. Say I have an entity set called TestSet. Each "Test" has attribute Key and Name. I'd like to bind the title of a table to the Name of one of the entities on that set.
What's wrong with the following code?
createContent: function(oController) {
jQuery.sap.require("sap.ui.table.Table");
var oTable = new sap.ui.table.Table({title: "{/TestSet('01')/Name}"});
oTable.setModel(new sap.ui.model.odata.ODataModel("/path/to/root/of/odata/"));
oTable.bindProperty("title", "/TestSet('01')/Name");
return oTable;
},
OData works ok when tested in isolation. /TestSet returns set of Test entities and /TestSet('01') returns one of those entities.
I've tested binding to /Name, /TestSet('01')/Name, etc. Nothing seems to work.
You can effect a property binding like this by binding the control to the specific element (hierarchy, as it were, is aggregation->element->property). So taking your example, you could do this:
var oTable = new sap.ui.table.Table({
title : "{Name}"
});
and then when you do this:
oTable.bindElement("/Products(0)");
the HTTP call is made by the OData model mechanism and the value appears in the table's title property.
Here's a running example, using Northwind.
According to the developer guide ...
Requests to the back end are triggered by list bindings, element bindings, and CRUD functions provided by the ODataModel. Property bindings do not trigger requests.
Thus, instead of trying to bind data directly on the properties of the target control with an absolute path, leverage ContextBinding (aka. "Element Binding") on the parent control or on the target control itself, and then bind the data on the properties of the target control or even further on child controls with a relative path (> instead of >/ in the path).
We can bind a single entity either in JS (e.g. if entity keys are required) or in XML views.
In JS
Instead of hardcoding the key predicate of the entity type, make sure to create the binding path together with key(s) via v2.ODataModel.createKey() dynamically after $metadata is loaded.
const oTable = new Table({ // Required from "sap/ui/table/Table"
title: "{Name}", // No absolute but relative binding path (no '/')
});
const myODataModel = /*...*/;
await myODataModel.metadataLoaded();
const bindingPath = myODataModel.createKey("/TestSet", { // See stackoverflow/a/47016070
key1: "...",
key2: 1234,
}); // Returns e.g. "/TestSet(key1='test1',key2='1234l')"
oTable.bindElement({ // Triggers a request and resolves the relative {Name} property.
path: bindingPath,
parameters: {
expand: "...",
select: "...",
// ... for more options see sap/ui/model/odata/v2/ODataContextBinding
},
events: { // Event handlers can be also assigned
dataRequested: e => { /*...*/ },
dataReceived: e => { /*...*/ },
change: e => { /*...*/ },
},
});
Note: bindElement and bindObject are same APIs just with different names. See also GitHub issue#3000.
In XML views
In XML views, single object / entity can be bound via binding in any controls. But creating keys dynamically (with createKey as shown above) is not possible. Hence, use binding only in combination with a <NavigationProperty> name, where keys are not required to be defined.
<table:Table xmlns:table="sap.ui.table"
binding="{
path: 'ToThatSingleEntity',
parameters: {
expand: '...',
select: '...'
},
events: {
dataRequested: '.onDataRequested',
dataReceived: '.onDataReceived',
change: '.onDataFromModelChanged'
}
}"
title="{FromThatSingleEntity}">
binding="{/TestSet(<keys>)}" is also possible but, again, the keys need to be then hardcoded.
I am 3 months into learning KnockoutJS and it has been great so far. However, I am facing an issue with binding.
This is the scenario:
I am using MVC with KO.
MVC model is passed down to the view, converted into a knockout object and pushed into the viewModel variable:
var data = ko.mapping.fromJS(#Html.Raw(Json.Encode(Model)));
var viewModel = new HP.ViewModels.CertificationPathViewModel(data);
ko.applyBindings(viewModel);
Within viewModel, I reference the MVC model as self.data:
ViewModels.CertificationPathViewModel = (function (data) {
var self = ViewModels.BaseEntityViewModel.apply(this, [data]);
// some other code
return { Data: self.Data, };
}
ViewModels.BaseEntityViewModel = (function (data) {
var self = this;
self.data = ko.observable(data);
// other code
return { Data: self.data, };
}
On the view, I data-bind like this:
<div id="drpControl" data-bind="CustomDropdown: Data().BusinessUnits.SelectedGroup, optionSettings: { CustomOptions: Data().Units.Groups, CustomOptionsCaption: '-- Select Group --' }"></div>
I try to update the self.data after an ajax call. I return the entire MVC model object and attempt to replace self.data like this :
self.data(updatedModel)
My expectation is that KO will take care of the update and no extra binding is needed. It works great for simple binding (ex. Value: Data().Something) but it doesn't work for complex binding (ex. value: Data().BusinessUnits.SelectedGroup ).
The controls that have complex binding are still bound to the old model, so KO doesn't know what to pass back next time I submit an ajax request.
Is this a limitation of KO, or I am not doing something properly?
Thanks
the ko.mapping plugin changes every property on self.data into an observable. During your update, you need to remap the updated data.
Since you didn't actually post your code, just unformatted snippets I can't help a whole bunch, but you should start by changing this line: self.data(updatedModel) to this:
ko.mapping.fromJS(updatedModel, self.data);
see the Knockout.JS mapping documentation
Protip for stack overflow - include your full code, to the extent that it's possible. Also, if you can, make a jsfiddle that reproduces your problem.
I've loaded an ASP.NET MVC viewModel into KnockoutJS using ko.mapping.fromJS(Model).
My viewModel looks something like this:
public IEnumerable<FunkyThing>funkyThings;
public FunkyThing selectedFunkyThing;
Each FunkyThing has a string property funkyThingName.
The mapping worked fine and I can see all the funky things in the table with their names.
I want to add a quick refresh button. So I've created a simple button and then data bound the buttons click to a knockout function refresh which looks something like this:
model.refresh= function () {
var url = '#Url.Action(MVC.FunkyThings.RefreshJSON())';
$.getJSON(url, function (returnedData) {
ko.mapping.fromJS(returnedData, {}, model.funkyThings);
});
The refresh function is succesfully called which in turn calls the RefreshJSON method on the server. The server passes back JSON data - an updated array of funkyThings, which I can see within chrome when I hover over returnedData in chrome's debugger.
However unfortunately after the mapping function has been called the bindings break:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: funkyThingName is not defined;
Bindings value: text: funkyThingName
And I'm not sure why...?
Is model.funkyThings an observable? If it is, then you can try passing it into the mapping method as a function:
ko.mapping.fromJS(returnedData, {}, model.funkyThings());
Failing that, are you sure that the structure of the JSON returned by the refresh method is correct?
Ah, if you're getting a JSON string back, so you need to call the fromJSON method of the mapping plugin:
ko.mapping.fromJSON(returnedData, {}, model.funkyThings);
You might need the parenthesis on funkyThings here too, but try it without first.
If the returned object is in the correct format and also the binding is well done you
just need to do:
model.FunkyThings(returnedData).