I have a knockout observableArray bound to a table below.
<table id="Users">
<thead>
<tr>
<td>User Name</td><td>Primary Email</td><td>Product Role</td><td>Active</td><td>EditUser?</td>
</tr>
</thead>
<tbody data-bind="foreach: CustomerUsers">
<tr>
<td data-bind="text: UserName"></td>
<td data-bind="text: PrimaryEmail().EmailAddress"></td>
<td><select></select></td>
<td data-bind="text: StaticActiveText"></td>
<td>Edit User</td>
</tr>
</tbody>
</table>
And the following view model:
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray(GetCustomerUsers());
}
My problem is that when the view model initially loads GetCustomerUsers it correctly gets the values it needs, and inserts them into the observable array. However, the elements are not displayed in the table.
The strange thing is that if I call the following function:
this.AddUserToCustomer = function () {
if (vm.NewUser) {
vm.CustomerUsers.push(vm.ActiveUser());
}
vm.CloseUserModalDialog();
}
The user is added to the array, and correctly displayed in the table. Even more confusing is that the latest value of the array in the push shows the initial values that are not displayers are in the array.
Does anyone have any idea what could be causing this behavior?
The code for GetCustomerUsers is:
function GetCustomerUsers() {
var users = [];
$.ajax({
type: 'Get',
url: ControllerBase + 'Actions/GetAllUsersForCustomer',
async: false,
success: function (data) { users = $.map(data, function(item) { return new ObservableUser(item); }); }
});
return users;
}
And ActiveUser is also an ObservableUser.
What you should do is to create the ko variable in the constructor and then update it with the ajax values when the data arrives.
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray();
GetCustomerUsers(this.CustomerUsers);
}
function GetCustomerUsers(getData) {
$.ajax({
type: 'Get',
url: ControllerBase + 'Actions/GetAllUsersForCustomer',
async: false,
success: function (data) { getData($.map(data, function(item) { return new ObservableUser(item); })); }
});
}
Ok I actually found a work around for this. Instead of populating the data as part of the constructor I add it after the document is ready
function CustomerAdminVm() {
var vm = this
this.CustomerUsers = ko.observableArray();
$(document).read(function(){vm.CustomerUsers(GetCustomerUsers());});
}
I'm going to throw a theory out as to what is actually happening, feel free to tell me I am wrong, but the solution does work.
What I believe to be happening is the data is being populated in the viewmodel constructor. But because the call is a synchronous call the constructor is not completing before the data is being populated. then the apply bindings function is being called, but because the data is prepoulated before the apply bindings function comes back the data already in the object is ignored because it has no observable change. During a normal async operation the call the constructor completes, then the apply bindings function is applied, and then the data comes back and the observable is changed causing an event flag to update the view.
Related
I have some complication with service removing. I have function that removes service on the server but I have to reload page to update table. I found way how to remove row by click-binding but there is the issue beacuse I can only remove row or get ID for delete service from server NOT both. :/
This is example of code that removes service on the server but doesn't remove table row.
HTML:
<table id="serviceView" class="fixed_header" border: 1>
<thead>
<tr>
<th>Name</th>
<th>Adress</th>
<th>Notification</th>
</tr>
</thead>
<tbody data-bind="foreach: services">
<tr>
<td data-bind="text: name"></td>
<td data-bind="text: address"></td>
<td data-bind="text: serviceId"></td>
<td ><button data-bind="click: $parent.DeleteService.bind(this, serviceId)">Remove</button></td>
</tr>
</tbody>
</table>
JS:
self.services = ko.observableArray([]);
self.lastCheck = ko.observable();
$.getJSON("http://localhost:55972/api/status", function (data) {
self.services(data.services);
self.lastCheck = data.lastCheck;
}); //////This is loading data to the table from server
self.DeleteService = function (serviceId) {
$.ajax({
type: "GET",
url: "http://localhost:55972/api/services/remove/" + serviceId,
}).done(function () {
self.services.remove(serviceId)
})
};
This is example of code that removes table row
When I use click-binding like this:
<button data-bind="click: $parent.DeleteService">Remove</button>
And change delete function to this:
self.DeleteService = function (serviceId) {
self.services.remove(serviceId)
$.ajax({
type: "GET",
url: "http://localhost:55972/api/services/remove/" + serviceId,
}).done(function () {
// here I want to remove row but i doesnt goes here without service ID.
})
};
It removes row but instead serviceId I got [object, object] in the URL.
Can you help me with it ? I got idea to use jquery to just update the table but it's seems unnecessarily complicated for me when I can use knockout.
I know the solution is not that hard but I'am just unable to solve it..... -_-
I'am sorry for taking time with this bullshit but this is my first real project and I'am so desperate at this point beacuse I have lot of things to do and I'am stucked on this.
In your Js code, you can try this:
self.services = ko.observableArray([]);
self.lastCheck = ko.observable();
$.getJSON("http://localhost:55972/api/status", function (data) {
self.services(data.services);
self.lastCheck = data.lastCheck;
}); //////This is loading data to the table from server
var serviceIdRemoved;
self.DeleteService = function (serviceId) {
serviceIdRemoved = serviceId; // now you can do whatever you need more with this value
$.ajax({
type: "GET",
url: "http://localhost:55972/api/services/remove/" + serviceId,
}).done(function () {
self.services.remove(serviceId)
})
};
With this way of work you can user the content of the variable and donĀ“t loose it. Also if you get [Object, Object], you can:
console.log(serviceId) // to see the content in the console.
JSON.stringify(data) //to see the content in html
This source could help you to understand it better.
The [object, object] you are seeing is actually the data and event objects which are secretly added to the JS function parameters by Knockout. If you want to add your own parameter to the click binding then you should do it like this:
<button data-bind="click: function(data, event) { $parent.DeleteService(serviceId, data, event) }">Remove</button>
You can then define your JS function as follows:
self.DeleteService = function (serviceId, data, event) {
[code here...]
}
You can read up on the exact details of it in the excellent Knockout documentation here:
http://knockoutjs.com/documentation/click-binding.html
It's about half-way down under the heading that reads Note 2: Accessing the event object, or passing more parameters
I'm having issues refreshing data that has been POSTed using Ajax. The POST is successfully being executed, but the data on the VIEW does not get refreshed with the new data. When I debug, the values from the Ajax POST are successfully being passed to my Search controller. When the controller returns the view model return View(model);, my VIEW is not refreshing the new data. How do I get the new data to show in my VIEW?
Ajax/jQuery
$(document).ready(function () {
$("#typeId, #scorecardId, #dateId").on('change', function () {
$.ajax({
url: "/StaffChange/Search",
type: "POST",
dataType: "json",
data: {
typeSelected: $('#typeId').val(),
scorecardSelected: $('#scorecardId').val(),
effectiveDateSelected: $('#dateId').val()
}
})
});
});
View
<table class="table table-condensed table-hover table-responsive table-striped">
<tr>
<th>
<a href="#Html.GetUrlAndRouteObject(Model.Sort, "change_date")">
Change Date
#Html.AddSortArrow(Model.Sort, "change_date")
</a>
</th>
<th>
#Html.EditorFor(model => model.effectiveDate, new { htmlAttributes = new { #class = "datepicker", #placeholder = "Effective Date", #id = "dateId" } })
</th>
<th>
#Html.DropDownListFor(model => model.type, new SelectList(Model.type), "-Type-", new { #id = "typeId" })
</th>
<th>
#Html.DropDownListFor(model => model.type, new SelectList(Model.scorecard), "-Scorecard-", new { #id = "scorecardId" })
</th>
</tr>
#foreach (var item in Model.get_staff_changelog_results)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.Change_Date)
</td>
<td>
#Html.DisplayFor(modelItem => item.Effective_Date)
</td>
<td>
#Html.DisplayFor(modelItem => item.Type)
</td>
<td>
#Html.DisplayFor(modelItem => item.Scorecard)
</td>
</tr>
}
Controller
public ActionResult Search(string sort, string typeSelected, string scorecardSelected, DateTime? effectiveDateSelected)
{
//TO DO: Implement MVC way of permissions...
if (System.Web.HttpContext.Current.Session["ConnectelligenceAdmin"].ToString() != "true")
{
return View("AccessDenied");
}
//Get sort order for Change Date using BLL
var staffChangeSort = (String.IsNullOrEmpty(sort)) ? SortOptions.change_date_desc : (SortOptions)Enum.Parse(typeof(SortOptions), sort);
//Execute sort order for Change Date using BLL
var sortResults = _changeLogSort.SortStaffChangeLog(staffChangeSort,typeSelected, scorecardSelected, effectiveDateSelected);
//Get list of dropdown results which is used for filtering
var dropdownResults = _staffChangeFilter.StaffChangeFilter();
var model = new Hierarchy_AdjustmentViewModel { get_staff_changelog_results = sortResults.get_staff_changelog_results, Sort = staffChangeSort, type = dropdownResults.type, scorecard = dropdownResults.scorecard};
return View(model);
}
You need to use the response you receive from your ajax call. Currently your Search method is returning a view result. What you can do is, if the method is invoked from an xhr call ,you may return a partial view result which has only the markup for table rows (with the new set of data) and in your ajax call's done event, update the DOM (replace the existing table rows with this new markup).
So first create a partial view called _List.cshtml and have the code to render the table rows there
#model Hierarchy_AdjustmentViewModel
#if(Model.get_staff_changelog_results!=null)
{
foreach (var item in Model.get_staff_changelog_results)
{
<tr>
<td> #item.Change_Date </td>
<td> #item.Effective_Date </td>
<td> #item.Type </td>
<td> #item.Scorecard </td>
</tr>
}
}
You can use the same partial view in your main view as well to reduce duplicate code
Now update your action method to return this partial view when the request was made from ajax code
public ActionResult Search(string sort, string typeSelected)
{
// Your existing code to get data goes here
var model = new Hierarchy_AdjustmentViewModel();
mode.get_staff_changelog_results = sortResults.get_staff_changelog_result;
if (Request.IsAjaxRequest())
{
return PartialView("_List", model);
}
return View(model);
}
Now all you have to do is, use this response to update the table. Since you are returning view result (HTML markup), you do not need to specify dataType as json.
$("#typeId, #scorecardId, #dateId").on('change', function () {
var $tbl = $(this).closest("table");
$tbl.find("tbody").find("tr:gt(0)").remove();
$.ajax({
url: "#Url.Action("Search","StaffChange")",
type: "POST",
data: {
typeSelected: $('#typeId').val(),
scorecardSelected: $('#scorecardId').val(),
effectiveDateSelected: $('#dateId').val()
}
}).done(function(res) {
$tbl.find("tbody").append(res);
}).fail(function(x, a, e) {
alert(e);
});
});
Another option is returning the data as a JSON array, and the done handler has to parse it (loop through it) and create markup for each row and append to the table (after clearing existing rows).
Because you're not responding to the AJAX call in any way. Add a .done() callback handler to the AJAX call:
$.ajax({
/*...*/
}).done(function (response) {
// update the page in here
});
At that point the question becomes... What sort of updates do you plan to do? It looks like you're returning a view from the controller:
return View(model);
So the data you're getting back in the AJAX response is a bunch of raw HTML. If it's a subset of a full page (that is, no additional <head>, <body>, etc. tags) then you could replace an existing container element with the contents of the response:
$('#someContainer').html(response);
However, this tends to be a bit sloppy and can cause other problems. (For example, replacing DOM elements which have handlers attached to them or plugins initialized on them, requiring you to re-think how you approach some things.) Instead, for AJAX calls it's common to return JSON data:
return Json(model);
This returns just the data instead of all the HTML surrounding this. This is useful for a couple of reasons:
It's easier to manipulate if you only want to do small and specific things.
It uses less network bandwidth.
It's just plain silly to return a bunch of HTML that the page already has.
With that data you can then update the specific elements on the page. For example, maybe you want to update the value of an input:
$('#someInput').val(response.SomeProperty);
Or the text of a display element:
$('#someElement').text(response.AnotherProperty);
How you handle the response and what you need to do to your page to "update it" is up to you. The point is that the system doesn't automatically do this for you. You need to handle the AJAX response and write your logic accordingly.
I am in the process of learning about knockout/json/mvc et al and have tried to put together an example project, but for some reason I am unable to get the data to bind correctly.
In the code snippet below, I get some JSON data from a web server and then try to map it to a function and then eventually to my knockout observableArray. What I then do is use this observableArray to bind to a HTML table. However the HTML table is not displaying any data. I put a label on to the HTML page and this does print out but with a toString() value of :
[Object object]
five times, which matches the amount of properties in the JSON data.
Can anyone see anything obvious I am missing?
JSON received from web server:
{ "Id": 1, "Name": "Inst123", "Price": 10, "DateTime": "2014-01-16T17:22:43.6383507+00:00", "Description": "Descriptions" };
.
ViewModel
$(document).ready(function () {
var gtViewModel = new GvTradeViewModel();
ko.applyBindings(gtViewModel);
console.log("ViewModel created");
});
var GvTradeViewModel = function () {
var self = this;
self.gvTrades = ko.observableArray([]);
var apiUrl = "http://localhost:57858/api/Trade/1";
console.log("Starting JSON data retrieval from: " + apiUrl);
$.getJSON(apiUrl)
// Handle success event.
.done(function (jsonData) {
if (jsonData.isEmptyObject)
console.log("NoData recieved");
else {
console.log("JSON data: " + jsonData);
var mappedTrades = $.map(jsonData, function (gvTradeData) {
return new GvTrade(gvTradeData);
});
self.gvTrades(mappedTrades);
console.log(self.gvTrades);
}
})
// Handle error/fail event.
.fail(function (jqxhr, textStatus, error) {
var err = textStatus + ", " + error;
console.log("Request Failed: " + err);
});
};
function GvTrade(data) {
this.TradeId = ko.observable(data.TradeId);
this.InstrumentName = ko.observable(data.InstrumentName);
this.DateTime = ko.observable(data.DateTime);
this.Price = ko.observable(data.Price);
this.Description = ko.observable(data.Description);
}
HTML
<table>
<thead>
<tr>
<th>TradeId</th>
<th>InstrumentName</th>
<th>Price</th>
<th>DateTime</th>
<th>Description</th>
</tr>
</thead>
<tbody data-bind="foreach: $data.gvTrades">
<tr>
<td data-bind="text: InstrumentName"></td>
<td data-bind="text: Price"></td>
<td data-bind="text: DateTime"></td>
<td data-bind="text: Description"></td>
</tr>
</tbody>
The JSON coming from your server represents a single object and not an array.
So when you are calling $.map then it does not correctly maps your data as an array, so you will end up some unusable objects.
To fix this you need to make sure that your jsonData containing an array before the map operation, you can do this with calling jQuery.makeArray on it (or you can have an if which is based on your data type decide whether you need to map or not):
var mappedTrades = $.map($.makeArray(jsonData), function (gvTradeData) {
return new GvTrade(gvTradeData);
});
Demo JSFiddle.
I am using this plugin http://www.joshbuckley.co.uk/2011/07/knockout-js-datatable-bindings/
to handle datatables / ko bindings.
Here's JS code:
function ProductViewModel() {
// Init.
var self = this;
self.products = ko.observableArray();
self.singleProduct = ko.observable();
var mappedProducts;
// At first load i'm loading data.
$.getJSON("/admin/test", function(allData) {
mappedProducts = $.map(allData, function(item) {
var p = new Product(item);
// I'm adding a new property to my model, to handle row level actions.
// I'm not sure this is a good practice.
p.edit = "<button data-bind='click: $root.edit'><i class='icon-pencil'></i></button>";
return p;
});
self.products(mappedProducts);
});
// Here i'm using the basic switch pattern, as from KO tutorials.
self.edit = function(product) {
console.log(product); // <--- Prints the whole self.products() array
self.singleProduct(product);
self.products(null);
}
self.list = function() {
self.products(mappedProducts);
self.singleProduct(null);
}
}
// My model.
function Product(item) {
this.name = ko.observable(item.name);
this.dealer = ko.observable(item.dealer);
this.cost = ko.observable(item.cost);
this.price = ko.observable(item.price);
this.picture = ko.observable();
}
Here's my markup:
<table id="products-table" class="table table-striped table-bordered table-hover"
data-bind="dataTable: {data: $parent.products, options: {aoColumns: [
{ bSortable: false, mDataProp: null, sDefaultContent: '' },
{mData: 'name'},
{mData: 'dealer'},
{mData: 'cost'},
{mData: 'price'},
{ bSortable: false, mData: 'edit' }
]}}">
<thead>
<tr>
<th>Pic</th>
<th>Name</th>
<th>Dealer</th>
<th>Cost</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
I am able to virtually switch between list and edit views, bindings seems to be properly handled.
Problem: when in the edit handler, i would expect to receive a single model as an argument; i am receiving the whole collection though, so i am not able to identify the model to edit.
One more thing: I am not sure at all this is a good practice of binding events on the rows, so any suggestion would be appreciated!
Well i think i got it myself and reason is quite clear after peeking the plugin source code.
From plugin source:
(function($){
ko.bindingHandlers.dataTable = {
init: function(element, valueAccessor){
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding is an object with an options field,
// initialise the dataTable with those options.
if(binding.options){
$(element).dataTable(binding.options);
}
},
update: function(element, valueAccessor){
var binding = ko.utils.unwrapObservable(valueAccessor());
// If the binding isn't an object, turn it into one.
if(!binding.data){
binding = { data: valueAccessor() }
}
// Clear table
$(element).dataTable().fnClearTable();
// Rebuild table from data source specified in binding
$(element).dataTable().fnAddData(binding.data());
}
};
})(jQuery);
Basically, for each update operation table is cleaned up and built again with the observable array, which should provide binding features.
What KO is trying to do, in each native click: binding, is to pass in the contextual data, which is the whole array, to the proper handler.
I'm not sure if this is a knockout issue, a JSON issue or some other issue so I'll just explain it all.
I have a Web grid displaying a number of users. I can edit these members using a AJAX pop-up window. As part of this window I can assign roles to a particular user. As seen in the following image:
These roles are chosen from dropdown menus and if I want to apply multiple roles to a user I can add extra dropdowns. I do this using knockout's foreach binding.
Now after I hit save the webgrid columns update automatically in ALL browsers. However if I edit a user and give them extra roles, then hit save and then try and edit them again their newly added roles are not showing up in IE. Chrome and Firefox work perfectly however.
When I click on the 'save' button on this popup window the data is sent to the server as JSON using the following code:
$.ajax({
type: 'POST',
dataType: 'json',
url: '/Person/Save',
dataType: 'html',
data: JSON.stringify(jsonObj),
contentType: 'application/json; charset=utf-8',
success: function(data) {
dlg.dialog("close");
$('#myUserGrid').html(data);
},
error: function(data) {
console.debug(data);
}
});
The relevant html and knockout code from the popup window view is as follows:
<tr>
<td class="roleList" colspan="2">
<table data-bind="foreach: RoleDdList">
<tr>
<td class="label">
* Role:
</td>
<td>
<select data-bind="options: $root.Roles, value: $data.role, optionsValue:'Id', optionsText:'Name'"> <!--,optionsCaption: 'No Role' -->
</select>
</td>
</tr>
<tr>
<td></td>
<td>Delete Role</td>
</tr>
</table>
</td>
<tr></tr>
There is a lot of code in the ViewModel but the relevant pieces are
var Role = function(roleId, roleName) {
this.Id = roleId;
this.Name = roleName;
};
// Class to represent a row in the Roles DropDown List
function RoleDropDown(initialRole) {
var self = this;
self.role = ko.observable(initialRole);
}
$(function () {
function myViewModel() {
var self = this;
//code relating to other fields
// Role Code
self.Roles = new ko.observableArray();
#{
foreach (var pair in Model.RolesList)
{
#:self.Roles.push(new Role(#pair.Key,"#pair.Value"));
}
}
// List of Role DropDowns
self.RoleDdList = ko.observableArray([
new RoleDropDown(self.Roles()[0])
]);
// Add a Role (code for link on form)
self.addRole = function() {
self.RoleDdList.push(new RoleDropDown(self.Roles()[0]));
};
// Delete a Role
self.deleteRole = function(num) {
self.RoleDdList.remove(num);
};
//
#{
if(#Model.Roles!=null)
{
#:self.RoleDdList.pop();
foreach (var selectedRole in Model.RoleNumbers)
{
#:self.RoleDdList.push(new RoleDropDown(#selectedRole));
}
}
}
}
vm = new myViewModel();
ko.applyBindings(vm);
$.validator.unobtrusive.parse('#formUser');
});
Any ideas for what the root of this problem is?