Knockout mapping is not updating my model - asp.net-mvc

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.

Related

knockout js binding limitation on complex view model

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.

Event listener for multiple elements - jQuery

In the ASP MVC page I'm currently working on, the values of three input fields determine the value of a fourth. Zip code, state code, and something else called a Chanel Code will determine what the value of the fourth field, called the Territory Code, will be.
I just started learning jQuery a couple weeks ago, so I would first think you could put a .change event that checks for values in the other two fields and, if they exists, call a separate method that compares the three and determines the Territory code. However, I'm wondering if there is a more elegant way to approach this since it seems like writing a lot of the same code in different places.
You can bind a callback to multiple elements by specifying multiple selectors:
$(".field1, .field2, .field3").click(function() {
return field1 +
field2 +
field3;
});
If you need to perform specific actions depending on which element was clicked, another option would be to create a function which performs the actual computation and then invoke that from each callback.
var calculate = function() {
return field1 +
field2 +
field3;
};
And then invoke this function when on each click:
$(".field1").click(function() {
// Perform field1-specific logic
calculate();
});
$(".field2").click(function() {
// Perform field2-specific logic
calculate();
});
// etc..
This means that you do not repeat yourself.
This works for me
jQuery(document).on('scroll', ['body', window, 'html', document],
function(){
console.log('multiple')
}
);
Adding another possibility, just in cased this may help someone. This version should work on dynamically created fields.
$("#form").on('change', '#Field1, #Field2, #Field3', function (e) {
e.preventDefault();
console.log('something changed');
});

Binding stops updating

I've been trying to create a custom binding for updating flot charts and it seems like it works when it first loads, but as I navigate around it quits.
Here's the scenario, I have a list view on one page (this is in jQuery Mobile) with little thumb nails of graphs, next to the graph is a slider that is bound to a property of the same view model that causes the graph points to be recalculated. When you click on one of the list items, it moves to another page that shows a much larger version of the graph and lets you change the value by typing in a textbox (later, you'll be able to click directly on the graph). The binding looks something like this:
ko.bindingHandlers["plot"] = {
init: function (element, valueAccessor, allBindingsAccessor) {
var qe = $(element);
var page = qe.closest("div[data-role='page']");
page.bind("pageshow", function () {
ko.bindingHandlers["plot"].update(element, valueAccessor);
});
},
update: function (element, valueAccessor, allBindingsAccessor) {
var qe = $(element);
var page = qe.closest("div[data-role='page']");
var curr = $.mobile.activePage;
var val = ko.utils.unwrapObservable(valueAccessor());
var data = val.plotData();
if(data && page.prop("id") == curr.prop("id")) {
var marker = val.markerData();
var opt = val.chartOptions();
opt.yaxis.show = opt.xaxis.show = !qe.hasClass("graphThumb");
marker.points.radius = opt.yaxis.show ? 5 : 3.5;
$.plot(qe, [
data,
marker
], opt);
}
}
};
The init handler sets it up to draw the graph on a page show because flot doesn't work right when drawing to a non-visible div. The update will check if the currently displayed page is the same as the one with the binding and redraw the graph as required.
For the graphs in the list view, they are immediately draw by the update method and work correctly. However, for the initially hidden pages, the function to draw the graph fires, the graph draws, but the updates will no longer work. Then, worse, when you go back to the initial page, the function bound to the pageshow event fires, redraws the graphs, but now they've also quit updating.
The view model looks something like this:
var viewModel = (function () {
this.current = ko.observable(0);
this.plotData = ko.computed(function () {
var points = [];
// a bunch of calculations that depend on the value of current of this and other viewModels in a collection
return points;
}
}
I can stick a break point in the computed plotData and see that it is getting update correctly. It just that those updates aren't trigger the binding handler.
The HTML binding looks something like this:
<!-- the first, visible page -->
<div data-role="page" id="index">
<ul data-role="listview" data-bind="foreach: factors">
<li data-bind="attr: {id: listId}">
<a data-bind="attr: {href: idLink}">
<div class="graphThumb" data-bind="plot: $data"></div>
</a>
</li>
</ul>
</div>
<!-- hidden details pages -->
<!-- ko foreach: factors -->
<div data-role="page" data-bind="attr: { id: id }">
<div class="graphPlaceHolder" data-bind="plot: $data"></div>
</div>
<!-- /ko -->
Update: I changed my binding slightly because I realized that I can just call the update on the pageshow event handler, which simplifies things, but doesn't fix the problem. It seems that doing that won't make knockout update it's dependencies for the binding.
Update: another update, assigning val.plotData() to a variable didn't work, neither did including it in my if statement. However, I have another computed observable that depends on the the current value and another property of the parent view model that I could retrieve and add to my if statement that works! However, my solution is probably to specific to be generally useful. The short story is that knockout will reassess the dependencies of a binding with each update, so you need to make sure that it is evaluating something important regardless of any conditional logic or it will stop updating.
So I can wrap up and mark this question as answered, I will briefly summarize my experience.
Custom binding are implemented the same was as computed properties in Knockout (according to the KO docs), and one thing that computed properties do is reassess which properties they are dependent on every time they are executed. What this means, is if you have a conditional in computed property (or a custom binding), only the properties accessed in the branch of the condition that actually gets executed will be monitored for changes by knockout. So, for example, if you have a property like this:
var myComputedProperty = ko.computed(function() {
if(this.myBool()) {
$("#someElement").text(this.foo());
}
else {
$("#someElement").text(this.bar());
}
});
KO will keep track of the value of myBool and recalculate the property if it changes, but, if myBool is true, it will also track foo, if myBool is false, it will also track bar, but it won't track both - because it doesn't need to. Most of the time this works just fine.
In my case it failed because I had a conditional that wasn't part of the view model (and therefore wasn't observable) and I needed it to keep track of the view model properties regardless of whether or not the condition evaluated to true or false. So I had something that looked like this:
if(page.prop("id") == curr.prop("id")) {
$("#someElement").text(this.foo());
}
Here the comparison is between the id of the page that the binding lives on and the $.mobile.activePage provided by jQuery Mobile (and, obviously not observable). If those id match, then knockout will update the binding when foo changes. However, if they don't, then knockout will lose the dependency on foo and even if the id do match at some later time, it will have lost the dependency and won't reevaluate when foo changes.
The way around this is to ensure that any properties that need to be tracked are evaluated regardless of the condition. So, something like this should solve the general case:
if(this.foo() && page.prop("id") == curr.prop("id")) {
$("#someElement").text(this.foo());
}
As to why I needed the condition at all is because flot gets very confused when it tries to draw a graph to a non-visible div, so I need to skip drawing the graph when it wasn't the current page.

JQM - Inject dynamic content at load time only

I'm trying to dynamically populate a select tag at load time (latest jQM version) using a custom template filling function.
If the fn is called in the "pagebeforechange" event, the select tag is properly initialized. Since this event is called on every page transition, I thought of moving the fn to the 'pageinit' event. This does not work, presumably because the DOM is not yet fully available. How can I coerce jQM to inject content in a page only once? Currently, I am using a kludge. There surely must be a smarter way. Thanks for any suggestions.
$(document).bind('pageinit', function () {
InitSelTagTest("#selActTag", "tplTag"); // Does not work.
});
$(document).bind("pagebeforechange", function (e, data) {
if ($("#selActTag").children().size() === 0) {
InitSelTagTest("#selActTag", "tplTag"); // Kludge, but it works
}
});
function InitSelTagTest(el,tpl) { // Append all tags to element el
var lstAllTags = JSON.parse($("#hidTag").val()); // Create tag array
// Retrieve html content from template.
var cbeg = "//<![" + "CDATA[", cend = "//]" + "]>";
var rslt = tmpl(tpl, { ddd: lstAllTags }).replace(cbeg, ").replace(cend,");
$(el).html(rslt).trigger("create"); // Add to DOM.
}
EDIT
In response to Shenaniganz' comment, it seems that the "pagebeforecreate" event could do the trick ie.
$("#pgAct").live("pagebeforecreate", function () {
// Populate tag select. Works. Traversed only once.
InitSelTag("#selActTag", "tplTag");
});
I'm not sure I fully understand your question but I'll throw a few things out there and you let me know if I can extend further.
To make something trigger only once on page load you can try to implement a regular JQuery $(document).ready(function(){}) aka $(function(){}) for the exact reason why JQuery Mobile users are told not to use it. It triggers only once on DOM load. Further pages don't trigger it because they're being switched via Ajax.
Other than that, on regular dynamic content loading you take a look at the following example I put together for someone else earlier:
http://jsbin.com/ozejif/1/edit

jQuery UI Sortable: Revert changes if update callback makes an AJAX call that fails?

I am using the sortable widget to re-order a list of items. After an item is dragged to a new location, I kick off an AJAX form post to the server to save the new order. How can I undo the sort (e.g. return the drag item to its original position in the list) if I receive an error message from the server?
Basically, I only want the re-order to "stick" if the server confirms that the changes were saved.
Try the following:
$(this).sortable('cancel');
I just encountered this same issue, and for the sake of a complete answer, I wanted to share my solution to this problem:
$('.list').sortable({
items:'.list:not(.loading)',
start: function(event,ui) {
var element = $(ui.item[0]);
element.data('lastParent', element.parent());
},
update: function(event,ui) {
var element = $(ui.item[0]);
if (element.hasClass('loading')) return;
element.addClass('loading');
$.ajax({
url:'/ajax',
context:element,
complete:function(xhr,status) {
$(this).removeClass('loading');
if (xhr.status != 200) {
$($(this).data('lastParent')).append(this);
}
},
});
}
});
You'll need to modify it to suit your codebase, but this is a completely multithread safe solution that works very well for me.
I'm pretty sure that sortable doesn't have any undo-last-drop function -- but it's a great idea!
In the mean time, though, I think your best bet is to write some sort of start that stores the ordering, and then on failure call a revert function. I.e. something like this:
$("list-container").sortable({
start: function () {
/* stash current order of sorted elements in an array */
},
update: function () {
/* ajax call; on failure, re-order based on the stashed order */
}
});
Would love to know if others have a better answer, though.

Resources