Posting model back to controller containing complex collection - javascript

I've posted a question similar to this, but I don't believe it clearly stated my question correctly. So here goes a better explanation!
I'm working on a C# MVC 5 application. I have a page that dynamically lists the model's collection property A. Each item in this collection uses a bit of bootstrap to show/hide it's own collection property B. So just to clarify that: This model has a collection of objects each of which has a collection of objects associated with it. These objects each have a checkbox associated with them bound to an isChecked boolean too that can be used to select/deselect items. Checking a box of an item from collection A, automatically selects all checkboxes of its collection B. So, as you can perhaps see, the sole purpose of having a checkbox on collection A items, is to act as a select all for its associated collection B items.
I'm trying to pass the entire model back to the controller on POST and then do stuff with it, e.g. grab all items that have their , but am running into a few problems.
E.g.
#foreach (var category in Model.Categories)
{
var headerId = category.Name;
var childCounter = 0;
#*some divs and headers here*#
#if (category.Items.Any())
{
#*lets give it the ability to check all of its associated items*#
#Html.CheckBoxFor(d => category.IsChecked, new {id = #headerId, onclick = String.Format("CheckUncheckChildren({0}, {1})", #headerId, category.MenuItems.Count)})
}
else
{
#Html.CheckBoxFor(d => category.IsChecked)
}
#*some other divs and headings*#
#if (category.MenuItems.Any())
{
#foreach (var item in dealItemCategory.MenuItems)
{
var childId = childCounter++ + "_" + headerId;
var serverId = item.Id;
#*some other divs and headings*#
#Html.CheckBoxFor(d => item.IsChecked, new {id = #childId})
}
}
}
Lower down on the page I have:
for (var i = 0; i < Model.Categories.Count(); i++)
{
var category = Model.Categories.ElementAt(i);
var headerId = category.Name;
#Html.HiddenFor(d => category, new {id = #headerId})
for (var j = 0; j < Model.Categories.ElementAt(i).Items.Count(); j++)
{
var item = Model.Categories.ElementAt(i).Items.ElementAt(j);
#Html.HiddenFor(d => item, new {id = j + "_" + #headerId})
}
}
In my mind, I'm retaining these properties on the page for this complex collection... (not that my understanding is 100%) I've forced ids for these hidden fields so that they correlate with the ids of the Checkboxes. Maybe not the best idea, but it was just the last thing I tried to get this to work...
Now I have two options here:
1) Believe in the above to mean that whenever a checkbox is selected/deselected, the object in the collection it is associated with, will get its isChecked boolean changed. Then, POST back the entire model and hope I'll be able to cycle through all checked items and do awesome stuff.
or
2) Create a string property in the model to hold a JSON representation of this complex collection of objects. When the page is POSTED, update this JSON string to the state of the complex collection that has been hopefully updated to have some of its items checked/unchecked. Then, POST back the entire model and server-side I would deserialize this single object into a logically equivalent complex collection and work from there.
I.e.
My model change would be adding a property:
public string JsonCategories{ get; set; }
And then in the view I'd have to initiate a post back:
#Html.HiddenFor(d => d.JsonCategories);
function accept() {
var categories= #Html.Raw(Json.Encode(Model.Categories));
var jsonCategories = JSON.stringify(categories);
var a = $('#JsonCategories').val();
$('#JsonCategories').val(jsonCategories );
$("form").submit();
};
Attempting solution A, I either get a null for the Categories property, or if I add #Html.HiddenFor(d => d.Categories); I get an empty object, count of 0.
Attempting solution B, I do get a lovely JSON representation for the complex Categories back in server land, but I see checkboxes I checked aren't changing the isChecked bools leading me to believe that, while the JSON object is able to be set to the Categories object on POST client-side, everything done by the user isn't being kept so ultimately the Categories collection of the model hasn't changed since it was passed to the View initially.
Sorry if this all seems complicated, it is and for a junior like me, I thought it best I ask around as I've never posted back lists and stuff to the controller before.
I'd be happy to provide more information if it will help!
Warmest regards,

If you change your foreach loops to for loops you can make use of the loop index in the lambda expression. This should make the engine output the correct syntax for posting the values. For example:
#for (int i = 0; i < Model.Categories.Count(); i++)
{
...
#Html.CheckBoxFor(d => d.Categories[i].IsChecked)
...
}
This should correctly bind the values to the POST request. The resulting html will be similar to the following snippet (0 being the first item, 1 would be the second etc.):
<input name="Categories[0].IsChecked" ... ></input>
Alternatively you can create editor templates for your child collections, which will result in you writing out this snippet to replace where your for loop is currently.
#Html.EditorFor(m => m.Categories)
More on how to do this here.
This should help you go with option 1.

Related

How to display values of 2d array in QML?

So I created a property in the root element of my qml and filled it with JavaScript as a 2d array. I did it this way:
property var cars: {
var carList = new Array(root.numberOfCars)
for (var i=0;i<root.numberOfCars;i++) {
var carProperties = new Array(numberOfCarProperties);
carProperties[root.currentStation] = -1;
carProperties[root.score] = 100;
carProperties[root.numberOfErrors] = 0;
carProperties[root.hasProblem] = false;
carProperties[root.errorScore] = 0;
carProperties[root.finished] = false;
carList[i] = carProperties;
}
return carList;
}
The values of the array will be changed as the program runs via JavaScript.
I want to display the values of this 2d array in a table and update them when they're changed, in addition to some action buttons that affect the values of the array at it's own row.
I'm just a noob at QML and programming in general, so if this is a very basic question please tell me where can I learn more, since I've found few resources online to learn QML.
Plain arrays are not the ideal solution if you intend to visualize the data. QML views can use arrays as models, but this is inefficient if you want to reflect internal changes. You either have to force update of the entire view, recreating all view delegates instead of just updating the changed value, or you will have to implement your own mechanism to update changes.
The easiest thing to use would be a ListModel element, and rather than implementing properties as an array (bad idea anyway) you can implement them as list element properties:
ListModel {
id: carList
ListElement {
currentStation: -1
score: 100
numberOfErrors: 0
// ...
}
ListElement {
// ...
}
}
The model can be populated declarative as above, or imperatively:
carList.append({"currentStation": -1, "score": 100, ...})
You also have the usual index access, property access and so on, just scroll through the doc to get an idea of the interface.
The benefit of this is you will get efficient automatic updates in the view. So you just set up a view and implement a delegate that will serve to visualize and manipulate the data.

Comparing and interlacing variables for editing purposes with AngularJS

I have a list of names (staff in stafflist) from which I select some names and add them as an object to an array (paxlist). This operation is repeated, so several objects with different names are added into the array.
What I am attempting to do is to be able to edit each one of this objects to add or remove names.
For UX reasons, when I first select names from stafflist, they turn blue, and they reset to white when the object is added.
Basically, the effect/functionality I'm looking for is:
The object is added -> The main list resets
The edit button from one of the objects is clicked
The list of names of the object is compared with the main list, and the relevant names are highlighted (in blue) as existing/already selected names.
The user selects or deselects names.
The edition is completed, the resulting object saved and the main list reset.
I have a Plunkr depicting the addition functionality, but I don't see clear how could I compare and make the 2 variables (stafflist and pax in recordlist) work together as to edit the result.
I'm not specially looking for somebody to do it and solve this for me, but more to understand the logic behind a possible solution, as so far I can't think of anything...
Any comments will be highly appreciated!
I created a new Plunk with what I think you were trying to accomplish. Basically I just added a new state (editMode) which captured the pax being edited.
var editMode;
$scope.editRecord = function(record) {
editMode = record.pax;
$scope.stafflist.forEach(function (s) {
s.chosen = false;
});
record.pax.forEach(function(p) {
$scope.stafflist.forEach(function (s) {
if(p.name === s.name) {
s.chosen = true;
}
});
});
};
I then used this new state to figure out whether I was creating a new record or editing an existing one.
$scope.pushStaff = function (staff) {
staff.chosen = true;
var arr = editMode || $scope.paxlist;
arr.push(staff);
};
$scope.unpushStaff = function (staff) {
staff.chosen = false;
var arr = editMode || $scope.paxlist;
var index=arr.indexOf(staff);
arr.splice(index,1);
};
I'm sure there are cleaner approaches, but this is one way to do it.

how can i update an item in a collection, without breaking angular databinding?

I have a collection of objects, I think this is called an associative array. Regardless, I would like to locate an item in that list and update it. As a side note this collection is the ng-model for a select. I know lodash has some of this type of functionality. I can find the item but I am not sure what is the best way to update the item so the data binding still works on my select.
Here is an example that doesn't work:
for (x = 0; x < $scope.RulesTemplates.length; x++)
{
if($scope.RulesTemplates[x].Name == $scope.TempRules.Name)
{
$scope.RulesTemplates[x] = $scope.TempRules;
}
}
Assuming a sample dataset, if you want to extend an existing object in an array that is bound to ng-options, you first need to make sure the angular has the array item being tracked by a specific unique property. Then you could just update the new item in the array at the respective place, angular will automatically perform the update in the bound select options (without re-rendering other option values). But that approach might not refresh already bound ng-model.
Another simple approach using traditional loop:-
<select ng-options="item.display for item in items" ng-model="selected"></select>
and your update logic can just be:-
for (x = 0, l = $scope.items.length; x < l; x++){
var item = $scope.items[x];
if(item.name == itemsUpdate.name) {
angular.extend(item,itemsUpdate); //Extend it
break;
}
}
Demo1
or since you are using lodash, probably less lines of code:-
var item = _.first($scope.items, {name:itemsUpdate.name}).pop(); //Get the item add necessary null checks
angular.extend(item, itemsUpdate);
Demo2
angular.extend will make sure underlying source object reference remains the same with updating the properties, you could also use lodash merge.
Try something like this:
angular.forEach($scope.RulesTemplates, function(value, key){
if(value.Name == $scope.TempRules.Name){
value = $scope.TempRules;
}
})
angular.forEach

Javascript array grid object property serialized by Kendo is null on server

I have a Kendo grid that I have an editable detail template. In the detail template, I have a multiselect.
When I save, I am manually updating the dataItem to grab the most up to date options in the detail template's multiselect. There is no problem with grabbing the data from the multiselect. Everything actually looks pretty good until I trigger the grid to save.
I add the multiselect items to the grid's dataItem with this code:
function SaveRefItemSubCategories() { //Called on Save button click
var grid = $("#RefItemSubCategoryGrid").data("kendoGrid");
for (var i = 0; i < grid.dataSource._data.length; i++) {
var dataItem = grid.dataSource._data[i];
dataItem.RefItemCategoryServiceIDs = [];
var ms = $("#MultiSelect_" + dataItem.uid).data("kendoMultiSelect");
var values = ms.value();
for(var x = 0; x < values.length; x++) {
dataItem.RefItemCategoryServiceIDs.push(values[i]);
}
}
grid.dataSource.sync();
}
At that point, when I look at what is posted to the server, the grid's dataSource looks like this for the serialized multiselect items:
models[0].RefItemCategoryServiceIDs[] 15
&models%5B0%5D.RefItemCategoryServiceIDs%5B%5D=15
It should look like this:
models[0].RefItemCategoryServiceIDs[0] 15
&models%5B0%5D.RefItemCategoryServiceIDs%5B 0%5D=15
Sorry for extra space in there, I couldn't get it to bold otherwise
The lack of the array's index in the serialized data results in that property being null when it hits the server. this property's type is List
I have tried a couple different approaches, but the end up creating new properties on the dataItem that stick around and cause problems after I save for the first time.
Some examples I've used instead of the push() method are:
This one does send the data back, but it ignores the existing list and creates a new dataSource property for each array index. This would work if I was only saving once, but if I save the grid multiple times, then the properties added stick around and mess with data integrity. I would entertain this option if a way to 'reset' the dataSource was possible after saving (without reloading the grid).
dataItem["RefItemCategoryServiceIDs[" + i + "]"] = values[i];
This one had the same effect as the .push(), which is the property on my object on the server being null
dataItem.RefItemCategoryServiceIDs[i] = values[i];
Please let me know if I can provide any more information and thanks in advance!
I'm not confident that this is the best way to handle it, but I have puzzled out a way that works.
I changed adding to the data item's properties from this:
for(var x = 0; x < values.length; x++) { //values is array from multiselect
dataItem.RefItemCategoryServiceIDs.push(values[i]);
}
to this:
for (var i = 0; i < values.length; i++) { //values is array from multiselect
dataItem["RefItemCategoryServiceIDs[" + i + "]"] = values[i];
}
Just this change works for every scenario EXCEPT this:
Save row with 2 options chosen from multiselect, resulting in 2 properties being added to the dataItem. We'll say the ID's of those items are 1 and 21. Posted data looks like this:
models[0].RefItemCategoryServiceIDs[0] = 1
models[0].RefItemCategoryServiceIDs[1] = 21
After saving, if we open that multiselect again and delete the first option (the one with ID 1), we get the following data posted:
models[0].RefItemCategoryServiceIDs[0] = 21
models[0].RefItemCategoryServiceIDs[1] = 21
Which is obviously not right (should just be 1 entry with the ID set to 21). This is happening because the RefItemCategoryServiceIDs isn't really an array, just properties whose names look like array entries so that they serialize into a list on the server (as I understand it). To mitigate this issue, I preceded the code that maps the current multiselect values to delete the existing properties associate with this array:
function CleanDataItemLists(dataItem) {
if (dataItem.RefItemCategoryCarrierIDs != undefined && dataItem.RefItemCategoryCarrierIDs.length > 0) {
var i = 0;
//Continues to remove properties by index until none are left
while (dataItem.RefItemCategoryCarrierIDs[i] != undefined) {
delete dataItem["RefItemCategoryCarrierIDs[" + i + "]"];
i++;
}
//Cleans up empty array property
delete dataItem.RefItemCategoryCarrierIDs;
}
return dataItem;
}
After this function is called, you can execute the for loop code referenced above and the data serializes with an index present (See initial post for example of missing index in serialized data), allowing the server to map it to the list object. Like I said, I'm not convinced that this is the best way to do this, but it does work for me. Hopefully, this can help someone else or lead to an even better answer.

Knockout: Dynamically controlling number of objects in an array?

So, I'm grabbing a number from a model, and trying to use that number (the length of an observable array) to dynamically control the number of objects in a "sub-model." In this case, when I add or remove "meta headings" in the parent model, the child model will automatically have the right number of corresponding "meta fields."
So this grabs the length of the array I'm basing things on:
self.metaCount = ko.computed(function() {
return parent.metaHeadings().length;
});
This is the array I want to dynamically push objects into:
self.metaColumns = ko.observableArray([]);
And this is how I'm trying to dynamically push items into the array:
self.columnUpdate = ko.computed(function() {
for (i=0; i<self.metaCount(); i++) {
self.metaColumns.push({heading: ko.observable()});
}
});
Now, I'm doing all of this in the model. The reason is that I have several instances of models and sub-models, and it makes more sense to have each one handle its own updating when a change takes place.
Am I going about this all wrong?
I would say it depends on your requirements. Firstly does your current code work correctly? If not what are the problems?
Is the metaColumns array required to to editable independently? If the answer is no then why maintain them as a separate property when you could simply do:
self.metaColumns = ko.computed(function() {
var result = [];
for (i=0; i<self.metaCount(); i++) {
self.metaColumns.push({heading: ko.observable()});
}
return result;
});
I notice that you currently aren't clearing the metaColumns array when the columnUpdate is recomputed so it will keep adding to the array.
Hope this helps.

Categories

Resources