Problem Description
I wanted to ask about how to use a list Exbando Objects in knockout.js,am using Rob Conrey's Massive and all returned results are dynamic, that's fine with me it suits my needs but when it comes to sending results to knockout i just don't know what to do with it.
Goal
Access object properties like obj.Name, obj.Brand etc...
Sample Code
View:
<div data-bind="foreach: Products">
<p>Product name: <strong data-bind="text: Name"></strong></p>
</div>
Controller:
public JsonResult GetProducts()
{
Products products = new Products();
var Model = products.GetAllProducts();
return Json(Model, JsonRequestBehavior.AllowGet);
}
The result of calling GetProducts is:
[[{"Key":"Id","Value":1},{"Key":"Name","Value":"Badass Boots"},{"Key":"Brand","Value":"Nike"},{"Key":"Description","Value":"Super cool boots that can make you fly (Not Really!)."}, etc...]]
Script File:
function ProductListViewModel() {
// Data
var self = this;
self.Products = ko.observableArray([]);
$.getJSON("/Home/GetProducts", function (data) {
self.Products(data);
});
}
JavaScript error on running the application:
Uncaught ReferenceError: Unable to parse bindings. Bindings value: text: Name Message: Name is not defined
Screen Shot 1:
Screen Shot 2:
An ExpandoObject generally speaking is for all intents and purposes, a dictionary. When serialized as JSON here, it is treated as a dictionary and becomes a collection of key/value pairs (not all serializers behave this way, but the one you're using does). It is not an object you can access members by name, you'll have to convert it into a representation where you can (or serialize it as one in the first place).
Doing the conversion is not the worst thing in the world:
function Product(item) {
var self = this;
// assuming item is an array of key/value pairs
ko.utils.arrayForEach(item, function(pair) {
// doesn't necessarily have to be mapped as an observable
self[pair.Key] = ko.observable(pair.Value);
});
}
With this, you can then map the results to your array of products:
$.getJSON("/Home/GetProducts", function (data) {
self.Products(ko.utils.arrayMap(data, function(item) {
return new Product(item);
}));
});
It looks like the problem is because you are trying to set the value of your ko.observableArray to a json array. Not sure that this will work. Typically this is how I would do it:
function ProductListViewModel() {
// Data
var self = this;
self.Products = ko.observableArray([]);
$.getJSON("/Home/GetProducts", function (data) {
ko.utils.arrayForEach(data, function(item) {
self.Products.push({
Name : item.Name
});
});
});
}
Use knockout's arrayForEach function to iterate through your json array, and push each object into your observableArray.
As in your JSON I see the sequence of Key and Value, so you have to specify the filed name which knockout has to query for to get the relative value and put it on the screen.
So change <strong data-bind="text: Name"> to <strong data-bind="text: Key"> and this should work for you.
Related
I am trying to get my JSON string passed through the ViewBag into my knockout observable and use that to populate a multiselectlist. I'm not sure what I am doing wrong, but right now it is taking each individual character in the JSON string and sticking it in it's own line in the select list.
Here is my C# controller code:
Dictionary<string, string> salesReps = new Dictionary<string, string>();
foreach ( var rep in salespeople.Salesperson )
{
salesReps.Add( rep.Code, rep.Code + " - " + rep.fullName );
}
//ViewBag.salespeople = salespeople.Salesperson.Select( x => x.Code ).Distinct().ToList();
ViewBag.salespeople = JsonConvert.SerializeObject( salesReps );
And here is the Knockout code:
var cSource = function (data) {
var self = this;
.....
self.salesPeople = ko.observableArray();
};
$(document).ready(function () {
var modelData = #Html.Raw( Json.Encode( Model ) );
mySource = new cSource(modelData);
//var salesRepList = Html.Raw( Json.Encode( ViewBag.salespeople ) );
var salesRepList = #Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(ViewBag.salespeople));
console.log(salesRepList);
mySource.salesPeople = salesRepList;
And here is the HTML select code:
<select data-bind="
options: salesPeople,
selectedOptions: chosenReps,
optionsCaption: 'Choose rep(s)'
"></select>
And a small snippet of what my console.log spits out:
{"USERNAME1":"USERNAME1 - Full Name 1","USERNAME2":"USERNAME2 - Full Name 2","USERNAME3":"USERNAME3 - Full Name 3",..... }
UPDATE
I changed the c# code to create named variables in the JSON now. Here is my new console.log output:
{"username":"BOBC","fullname":"Bob Cottin"},{"username":"JIMT","fullname":"Jim Thorpe"},{"username":"LUKEP","fullname":"Luke Peppin"}, ....}
The options binding is expecting an array of objects or values, but according to your console log salesPeople is a single dictionary style object. You may need to convert that to an array so the binding knows how to parse it into select options.
If you only need the value property and can disregard the "Username1", "Username2", etc then you could use a loop like this to grab the value properties of the object:
for (var x in salesRepList) {
if (salesRepList.hasOwnProperty(x)) {
mySource.salesPeople.push(salesRepList[x]);
}
}
If you need the more complex types then you'll have to create an array of objects, or (per your last edit) have your json pass them with standard parameter names. Then you can use knockout's optionsText binding to tell the select which property on the object to use as the display text.
Per example:3 of the knockout docs
<select data-bind="options: availableCountries,
optionsText: 'countryName',
value: selectedCountry,
optionsCaption: 'Choose...'"></select>
Here's a jsFiddle with some of that adapted to your example: fiddle
I used the following tutorial:
http://msdn.microsoft.com/en-us/library/gg508808%28VS.98%29.aspx
And everything seemed fine, but in my case, string Username always comes back null. After tonnes of research, I found everyone discovered BIND Prefixes. That would be great in many circumstances, but not this one. I should note all properties and names line up, however in my for loop, the EditorFor creates a [i].Username field and this doesn't map to any model property.
QUESTION: I think I want to map [i].Username to Username where i is any number from 0-infinity, so when it GETS, the value is passed to the Action properly. How do I do this? If this is wrong, what do I do validate this for a specific row in a table?
#for (var i = 0; i < Model.Count; i++)
{
BLAH BLAH BLAH CODE FOR BUILDING TABLE ROWS
<td>
#Html.EditorFor(modelItem => Model[i].Username)
</td>
}
Since I could technically have HUNDREDS if not THOUSANDS of records, I would rather not had a binding PREFIX for all 1000. Am I fundamentally missing something here? I am new to ASP.NET MVC and am used to WebForms so I feel like sometimes I am mixing concepts and mashing up something that is entirely wrong.
EDIT:
I fixed it by doing the following, but not sure if this is the best idea. I set the parameter equal to the FieldName without [i] prefix, but still retrieve the element with the [i] prefix. Javascript isn't my Forte so please let me know if it is horrible.
adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
var value = {
url: options.params.url,
type: options.params.type || "GET",
data: {}
},
prefix = getModelPrefix(options.element.name);
$.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) {
var paramName = fieldName.substr(fieldName.lastIndexOf(".") + 1);
var actualFieldName = appendModelPrefix(fieldName, prefix)
value.data[paramName] = function () {
return $(options.form).find(":input").filter("[name='" + escapeAttributeValue(actualFieldName) + "']").val();
};
});
setValidationValues(options, "remote", value);
});
You have not posted your code for the model or controller, but assuming you have a RemoteAttribute applied to property Username, for example
public class MyModel
{
[Remote("IsValidUserName", "Person")]
public string Username { get; set; }
}
with a method in PersonController
public JsonResult IsValidUserName(string Username)
{
....
}
and the view
#model List<Person>
...
#for (var i = 0; i < Model.Count; i++)
{
#Html.EditorFor(m => m[i].Username)
}
This will generate html such as
<input name="[0].UserName" ... />
<input name="[1].UserName" ... />
Unfortunately the remote method in jquery-validate posts back the name and value of the element so that the ajax call looks like
$.ajax({
url: '/Person/IsValidUserName',
data: { [0].UserName: 'someone#somewhere.com' },
...
which will not bind.
I have reported this as an issue at Codeplex with a possible solution. In the meantime you can modify the remote method in jquery-validate.js file as follows
remote: function(value, element, param) {
....
var data = {};
// data[element.name] = value;
data[element.name.substr(element.name.lastIndexOf(".") + 1)] = value; // add this
This will strip the prefix so that the posted data is
data: { UserName: 'someone#somewhere.com' },
and will correctly bind to the method.
Assuming the code is formatted in the following way:
View:
#for(var i = 0; i<Model.Count; i++) {
<div class="row">
#Html.EditorFor(modelItem => Model[i].Username)
</div>
}
<style>
.valid{
background: lime;
}
</style>
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
Model:
public class MyModel {
[Remote("IsValidUserName", "Validation", HttpMethod = "POST")]
public string Username { get; set; }
}
It is possible to use the automatic modelbinding to bind to the remote validation. If you were to use a list or array for this, the binding would fail while a Dictionary can catch this error.
Be aware however that the Key in the dictionary will be consistent with the id in the view (e.g. [5].Username will map to {Key: 5, Value: MyModel{Username:...}}) and won't be a default 0, hence the use of a Linq query.
Controller:
[HttpPost]
public JsonResult IsValidUserName(Dictionary<int,MyModel> Users) {
return Json(Users.First().Value.Username.Contains("User"));
}
--I've deleted the original text to try to clean the post up a bit--
--------EDIT-------------------------------------------------------------------------
Here is some more info not sure if it helps anyone that's ran into this issue before.
Here is what my final object looks like in the JS script:
var DealerModel = {
Dealer: {
AccountId: 5678,
Name: "Austin",
City: "Who knows",
State: "TN"
},
Test: 111,
DealerCategories: [{Name: "Test1", Value:"Value1"},{Name:"Test2", Value:"Value2"}]
}
When I pass this into my controller via jquery it has the 111 value for Test, it shows that DealerCategories has 2 objects, but for both those objects as well as Dealer it shows NULL.
I've changed up the model/controller a few times and it seems no matter what if I pass in an object that has a sub json object it doesn't like that. I have a feeling this is something simple i'm missing.
the model binder knows how to bind arrays from JSON so you just have to give him something like this :
var DealerModel =
{
Dealer : 'my_dealer_value_here',
DealerContact = [] <- a list of Contrats here
}
and i think this should do it.
If your issue is with taking the leap from posting simple values like strings and numbers through jQuery.post, you can also provide json objects, like the one listed by Alex, above.
$.ajax({
type: "POST",
url: 'MYURL',
data: {
Dealer : {id:-1, accountId: '', name:'TheDealer'},
DealerContact: [{FirstName:'ContactName',...},{...},...]
}
});
And that bit of data should deserialize to your objects.
I'm trying to send selected checkbox values to another asp page to update database.
$("#Delete_selected_Comment_button").click(function () {
var dataToBeDeleted = new Array();
$('.checkboxes_to_delete:checked').each(function (key, value) {
dataToBeDeleted .push($(this).val());
});
$.ajax({
url: 'http://myurl.aspx',
type: 'GET',
data: dataToBeDeleted,
success: function () { alert('yipee'); },
error: function () { alert("Ooff could not delete data"); }
});
});
Problem:
I am not able to retrieve any value in myurl.asp page.
I am doing:
String datagotfromajax= request.QueryString[dataToBeDeleted];
Am doing this incorrectly? Where is my mistake ? Can anyone help please?
An easy way would be to change one line:
data: "ids="+dataToBeDeleted.join(","),
And, in the server do:
string[] ids = Request.QueryString["ids"].ToString().Split(",");
With this you'll hava a string array in the server with the corresponding checkboxes values.
HOpe this helps.
dataToBeDeleted will be converted to a query string (if its not already) when sent to the server, that means it consists key-value pairs, i.e. foo=bar&fred=fish. On the server you can query individual key-value pairs using the syntax
string bar = request.QueryString["foo"];
You are passing an array as the data parameter. This won't work. You need to pass an object. If the object contains an array, this will be serialized appropriately. This means your code should probably look like this:
data: {
toBeDeleted: dataToBeDeleted
},
This will then be serialized appropriately (see $.param for the serialization process) and you can retrieve the values with your back-end code.
I' still newby to this so I'll try to explain what I'm doing. Basically what I want is to load a dropdownlist depending on the value of a previous one, and I want it to load the data and appear when the other one is changed. This is the code I've written in my controller:
public ActionResult GetClassesSearch(bool ajax, string phylumID, string kingdom){
IList<TaxClass> lists = null;
int _phylumID = int.Parse(phylumID);
int _kingdom = int.Parse(kingdom);
lists = _taxon.getClassByPhylumSearch(_phylumID, _kingdom);
return Json(lists.count);
}
and this is how I call the method from the javascript function:
function loadClasses(_phylum) {
var phylum = _phylum.value;
$.getJSON("/Suspension/GetClassesSearch/",
{ ajax: true,
phylumID: phylum,
kingdom: kingdom
},
function(data) {
alert(data);
alert('no fallo')
document.getElementById("pClass").style.display = "block";
document.getElementById("sClass").options[0] = new Option("-select-", "0", true, true);
//for (i = 0; i < data.length; i++) {
// $('#sClass').addOption(data[i].classID, data[i].className);
//}
});
}
The HTML associated is:
<p id="pPhylum">
<%= Html.Label("Phylum: ") %>
<%= Html.DropDownList("sPhylum",
(SelectList)ViewData["PhyRecID"],
"--Select One--",
new { onchange = "loadClasses(this);" }
)%>
</p>
<p id="pClass">
<%= Html.Label("Class: ") %> <select id="sClass"></select>
</p>
The thing is that just like this it works, I pass the function the number of classes within a selected phylum, and it displays the pclass element, the problem gets when I try to populate the slist with data (which should contain the objects retrieved from the database), because when there is data returned by the database changing return Json(lists) instead of return Json(lists.count) I keep getting the same error:
A circular reference was detected while serializing an object of type 'SubSonic.Schema.DatabaseColumn'.
I've been going round and round debugging and making tests but I can't make it work, and it is suppossed to be a simple thing, but I'm missing something. I have commented the for loop because I'm not quite sure if that's the way you access the data, because I've not been able to make it work when it finds records. Can anyone help me?
Thanks in advance,
Victor
Finally I solved this problem. The problem was that JSON for some reason wasn't capable of displaying objects that are referenced to each other, that have problems with foreign keys and stuff. I created a new class just with the fields I needed from the database, and I populated it with a foreach statement. Then I passed that filled IList to the return Json and it worked...
Thank you all for your help,
Victor