Can not save data 2nd time knockout mvc - asp.net-mvc

I am new in knockout and asp.net mvc both.
I am trying to Insert update delete data in database with knockout. My knockout model is
function City(data) {
this.CityId = ko.observable(data.CityId);
this.CityName = ko.observable(data.CityName);
}
function CityViewModel() {
var self = this;
self.Citys = ko.observableArray([]);
self.CityId = ko.observable();
self.CityName = ko.observable();
self.selectedCity = ko.observable();
// self.City = ko.observable();
selectCity = function (item) {
self.selectedCity(item);
}
//load
loadCitys = function () {
$.getJSON("/Admin/GetCitys", {}, function (result) {
var mappedCitys = ko.utils.arrayMap(result.Data, function (item) {
return new City(item);
});
self.Citys([]);
self.Citys.push.apply(self.Citys, mappedCitys);
});
}
//edit
EditCity = function (item) {
//what need to do here
// is it possible to fill the hidden fild and the text box ??
}
//save
SaveCity = function (item) {
City = new City(item);
$.ajax({
type: "POST",
url: "/Admin/SaveCity",
data: ko.toJSON({ City: City }),
contentType: "application/json",
success: function (result) {
if (result.Edit) {
City.CityId = result.Success;
City.CityName = item.CityName;
self.Citys.push(City);
toastr.success('City Information Save Successfully', 'Success');
}
else if (result.Edit == false) {
toastr.success('City Information Update Successfully', 'Success');
}
else {
toastr.error('There is an error please try again later', 'Errror');
}
}
});
}
//delete
DeleteCity = function (City) {
$.ajax("/Admin/DeleteCity", {
data: ko.toJSON({ CityId: City.CityId }),
type: "POST", contentType: "application/json",
success: function (result) {
if (result.Success) {
self.Citys.remove(City);
toastr.success('City Remove Successfully', 'Success');
}
else {
alert("Error..");
}
}
});
}
}
(function () {
ko.applyBindings(new CityViewModel, document.getElementById("Citys"));
loadCitys();
});
And my Html codes are
<table class="table table-striped">
<thead>
<tr>
<th>City Id</th>
<th>City Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: $root.Citys">
<tr data-bind="click: selectCity">
<td><span data-bind="text:CityId"></span></td>
<td><span data-bind="text:CityName"></span></td>
<td><button data-bind="click: EditCity" class="btn btn-primary">Edit</button></td>
<td><button data-bind="click: DeleteCity" class="btn btn-danger">Delete</button></td>
</tr>
</tbody>
</table>
<fieldset>
<legend>Add new / Edit City</legend>
<label>City name</label>
<input type="hidden" data-bind="value: CityId" />
<input type="text" data-bind="value: CityName" placeholder="Type city nameā€¦">
<button type="submit" data-bind="click: SaveCity" class="btn">Submit</button>
</fieldset>
With this codes I can get data form database display them successfully in my view file,
I delete the data from database, and I also can Insert data to database but here is a problem I can save data only 1st time when I change the textbox value (without page refresh) and try to save city information then it say (in Firebug on my javascript code):
TypeError: City is not a constructor
City = new City(item);
My question is what have I done wrong in this codes, and I am trying to fill the textbox and the hidden field when edit button click, how can I do this?
Thanks in advance.

There are a number of faults with your javascript, including:
The methods on your viewmodel, such as SaveCity, DeleteCity, EditCity are all being defined without the 'this/self' prefixes, therefore they are being added to the global namespace.
In your SaveCity method, your are assigning a new instance of the City class to a variable called 'City', therefore destroying the City class. It will work the first time, but any other attempts to create an instance of a City will yield an exception.
Anyway, this should be a working version of your script and HTML without the ajax stuff. You will need to adapt that yourself. I have also created a working JsFiddle here..
function City(data) {
this.CityId = ko.observable(data.CityId);
this.CityName = ko.observable(data.CityName);
}
function CityViewModel() {
var self = this;
self.Citys = ko.observableArray([]);
self.SelectedCity = ko.observable();
self.EditingCity = ko.observable(new City({CityId: null, CityName: ''}));
self.EditCity = function(city){
self.EditingCity(new City(ko.toJSON(city)));
};
//load
self.loadCitys = function () {
self.Citys().push(new City({CityId: '1245', CityName: 'Los Angeles'}));
self.Citys().push(new City({CityId: '45678', CityName: 'San Diego'}));
};
//save
self.SaveCity = function () {
var city = self.EditingCity();
if(city.CityId()){
var cityIndex;
for(var i = 0; i < self.Citys().length; i++) {
if(self.Citys()[i].CityId() === city.CityId()) {
cityIndex = i;
break;
}
}
self.Citys[cityIndex] = city;
}
else{
city.CityId(Math.floor((Math.random()*1000000)+1));
self.Citys.push(city);
}
self.EditingCity(new City({CityId: null, CityName: ''}));
}
//delete
self.DeleteCity = function (city) {
self.Citys.remove(city);
};
}
var viewModel = new CityViewModel();
viewModel.loadCitys();
ko.applyBindings(viewModel, document.getElementById("Citys"));
HTML
<table class="table table-striped">
<thead>
<tr>
<th>City Id</th>
<th>City Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: Citys">
<tr data-bind="click: $root.SelectedCity">
<td><span data-bind="text:CityId"></span></td>
<td><span data-bind="text:CityName"></span></td>
<td><button data-bind="click: $root.EditCity" class="btn btn-primary">Edit</button></td>
<td><button data-bind="click: $root.DeleteCity" class="btn btn-danger">Delete</button></td>
</tr>
</tbody>
</table>
<fieldset data-bind='with:EditingCity'>
<legend>Add new / Edit City</legend>
<label>City name</label>
<input type="hidden" data-bind="value: CityId" />
<input type="text" data-bind="value: CityName" placeholder="Type city name" />
<button type="submit" data-bind="click: $root.SaveCity" class="btn">Submit</button>
</fieldset>

Related

Update razor collection via knockout

I have this UI
And my goal is to update table after clicking Update button based on Id and Description parameters
I have following code
Html
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Products)
{
<tr>
<td>#item.Id</td>
<td>#item.Description</td>
</tr>
}
</tbody>
</table>
<label>Id</label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description</label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>
JavaScript
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = json.products;
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = null;
self.productDescription = null;
// Update function.
self.update = function () {
// Get products collection index.
index = self.products.findIndex(function (product) {
return product.Id == self.productId
});
// Throws -1, index was not found !
alert(index);
// Assign new value.
// self.products[index].description(self.productDescription);
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I need help with two things
Update knockout viewmodel (self.products collection based on Id and Description inputs in viewModel.Update function)
Update table (I don't know how to bind the rows, when using razor foreach)
EDIT:
When I change Razor loop with Knockout foreach
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
it does not work either :/ I tried some changes with observables
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observable(json.products);
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
self.update = function () {
self.products()[self.productId()-1].description = self.productDescription()
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I do not get an error, but the value is not changed in the html table.
Your knockout code looks mostly correct but your UI isn't updating because the values that you're changing aren't observable. If you want the changes to the items to reflect in the table you need to make each field that will update an observable instead of having a single observable property for the entire json object.
The following code will only update if the entire object reference changes. IE if you fill self.products with a new object. It will not update if you change a property on the existing object.
// Product array from server.
self.products = ko.observable(json.products);
You'll need to change your code to individually apply observable properties to your json object when it comes from the server.
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
And then you'll have to update the value using the function style getter/setter.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
</table>
<br/>
<label>Id: </label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description: </label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>

Foreach Checked and click event knockout

I would like to add the values of 'Cost' based on checkbox checked event on knock out.
<tbody data-bind="foreach: $root.searchedResults">
<!-- ko if: Id-->
<tr>
<td data-bind="text: Id"></td>
<td data-bind="text: Cost"></td>
<td><input type="checkbox" data-bind="checked: $root.searchedResults().length > 0, click: $root.Hospital_click"></td>
</tr>
<!-- /ko -->
</tbody>
And the sum of cost should be displayed in
<div data-bind="text: ko.toJSON(SumofItems)"></div>
The click event is not updating my sum based selection. Can someone help?
self.Hospital_click = function () {
//Suggest
}
My View model is:
var SearchModel = function () {
var self = this;
self.searchFromDate = ko.observable('01/01/2014');
self.searchToDate = ko.observable('01/01/2016');
self.searchedResults = ko.observableArray([]);
self.search = function () {
var model = {
BeginDate: self.searchFromDate(),
EndDate: self.searchToDate()
};
$.ajax({
url: '#Url.Action("SearchSpend", "Analysis", new { Area = "Portal" })',
type: 'POST',
contentType: 'application/json; charset=utf-8',
dataType: "json",
data: ko.toJSON(model),
success: function (response) {
self.searchedResults(response);
}
});
};
You could add a new observableArray with the id's of the selected results (selectedResults). The viewmodel could be:
function Result(id,cost) {
this.Id = id;
this.Cost = cost;
}
function viewModel() {
var self = this;
self.searchedResults = ko.observableArray([]);
self.selectedResults = ko.observableArray([])
self.totalCost = ko.computed(function() {
let total = 0;
for(let p = 0; p < self.searchedResults().length; ++p)
{
if (self.selectedResults.indexOf(self.searchedResults()[p].Id)>=0){
total += self.searchedResults()[p].Cost;
}
}
return total;
});
};
totalCost will return the sum of the Cost field for all selected results.
And the view could be something like this:
<table data-bind="foreach: searchedResults">
<tr>
<td data-bind="text: Id"></td>
<td data-bind="text: Cost"></td>
<td><input type="checkbox" value="" data-bind="checkedValue: Id, checked: $parent.selectedResults"></td>
</tr>
</table>
<div data-bind="text: totalCost"></div>
totalCost is a computed value that returns de sum of the Cost for only the selected results.
Here is a fiddle.
In the fiddle, the data comes from the array listOfResults. In your case, this comes from the success function (Ajax request); in this function you also needs to clean the selectedResults.

Knockout Nested List Post Null

I'm trying integrate ko on one of my razor views. I've researched quite a bit yesterday but haven't been able to find a similar solution for my problem.
I have a record model:
public class Record
{
public Int Request Id {get;set;}
public string RecordName {get;set;}
public Person Person {get;set;}
public IList<Person> People { get; set; }
}
And a Person Model:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IList<Alias> Aliases { get; set; }
}
My View:
#model Record
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#using (Html.BeginForm("PostRequest", "Request", FormMethod.Post))
{
#Html.LabelFor(m => m.Person.FirstName)
#Html.TextBoxFor(m => m.Person.FirstName)
<h5>People</h5>
<table>
<tbody data-bind="foreach: People">
<tr>
<td>#Html.LabelFor(m => m.Person.FirstName)</td>
<td><input type="text" data-bind="value: FirstName, attr: {name: 'People[' + $index() + '].FirstName'}" /></td>
<td>#Html.LabelFor(m => m.Person.LastName)</td>
<td><input type="text" data-bind="value: LastName, attr: {name: 'People[' + $index() + '].LastName'}" /></td>
</tr>
<tr>
<td><button id="removePerson" data-bind="click: $root.removePerson">Remove Person</button></td>
</tr>
<tr>
<td>
<button id="addAlias" data-bind="click: addAlias">Add Alias</button>
</td>
</tr>
<!-- ko foreach: Aliases -->
<tr>
<td>#Html.LabelFor(m => m.Alias.FirstNameAlias)</td>
<td><input type="text" data-bind="value: FirstNameAlias, attr: {name: 'Aliases[' + $index() + '].FirstNameAlias'}" /></td>
<td>#Html.LabelFor(m => m.Alias.LastNameAlias)</td>
<td><input type="text" data-bind="value: LastNameAlias, attr: {name: 'Aliases[' + $index() + '].LastNameAlias'}" /></td>
<td><button id="removeAlias" data-bind="click: $root.removeAlias">Remove Alias</button></td>
</tr>
<!-- /ko -->
</tbody>
</table>
<button id="addPerson" data-bind="click: addPerson">Add Person</button>
<button>Submit</button>
<input id="clickMe" type="button" value="clickme" onclick="submit();" />
}
And Script:
#section scripts
{
<script type="text/javascript">
$(function () {
var personItem = function () {
var self = this;
self.LastName = ko.observable();
self.FirstName = ko.observable();
self.Aliases = ko.observableArray();
};
var model = ko.mapping.fromJS(#Html.Raw(Model.ToJson()));
alert('The length of the array is ' + model.People().length);
alert('The first element is ' + model.People()[0].Aliases().length);
alert(ko.toJSON(model));
model.addPerson = function () {
model.People.push(new personItem());
};
model.removePerson = function (person) {
model.People.remove(person);
};
ko.applyBindings(model);
})
function submit() {
$.ajax({
url: '/Request/PostRequest',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: ko.toJSON(model),
success: function (status) {
alert(status);
}
});
};
</script>
}
The Controller action that GETs this view pre initializes some data for the record model, so the binding appears to be working properly, however when I post data, the data for Alias List comes back Null.
Controller:
public ActionResult CreateRequest(Record viewModel)
{
var list = new Record
{
People = new List<Person> {
new Person {FirstName = "My First Person", Aliases = new List<Alias> { new Alias{FirstNameAlias = "firstnamealias",LastNameAlias = "lastnamealias1"}},},
new Person {FirstName = "My Second Person", Aliases = new List<Alias> { new Alias{FirstNameAlias = "firstnamealias2", LastNameAlias ="lastnamealias2"}},}
},
Person = new Person()
{
FirstName = "me"
}
};
return View(list);
}
[HttpPost]
public JsonResult PostRequest(Record viewModel)
{
return Json(String.Format("'Success':'false','Error':'"));
}
I feel that I am missing something with ko.mapping. But so close, as the data populates from the GET controller action, but fails to POST.
SOLUTION
I updated my KO script, and the way I was rendering the ko data in the view. This has solved my problem.
Updated Script
<script>
var initialData = #Html.Raw(Serialize(Model.People));
var BackgroundModel = function(people) {
var self = this;
self.people = ko.mapping.fromJS(people);
self.addPerson = function() {
self.people.push({
FirstName: "",
LastName: "",
Aliases: ko.observableArray()
});
};
self.removePerson = function(person) {
self.people.remove(person);
};
self.addAlias = function(person) {
person.Aliases.push({
//todo
FirstNameAlias: "",
LastNameAlias: ""
});
};
self.removeAlias = function(Alias) {
$.each(self.people(), function() { this.Aliases.remove(Alias) })
};
self.save = function() {
self.lastSavedJson(JSON.stringify(ko.toJS(self.people), null, 2));
var subModel = JSON.stringify(ko.toJS(self));
$.ajax({
url: '/Request/PostRequest',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: subModel,
success: function (status) {
alert(status);
}
});
};
self.lastSavedJson = ko.observable("")
};
ko.applyBindings(new BackgroundModel(initialData));
</script>
Updated View
<div data-bind="foreach: people">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title"> <a data-bind="attr:{href:'#collapseOne'+$index()}, text:FirstName() +' '+ LastName()" #*href="#collapse_One"*# class="accordion-toggle" data-toggle="collapse" data-parent="#accordion"></a> </h4>
</div>
<div data-bind="attr:{id:'collapseOne'+$index()}" class="panel-collapse collapse in" #*data-bind="attr:{id:_collapseId, class:_collapsedIn}"*#>
<div class="panel-body">
<table>
<tbody>
<tr>
<td>#Html.DisplayNameFor(x => x.Person.FirstName)</td>
<td>#Html.DisplayNameFor(x => x.Person.LastName)</td>
</tr>
<tr>
<td><input data-bind="value: FirstName, name: FirstName" /></td>
<td><input data-bind="value: LastName, name: LastName" /></td>
</tr>
<tr>
<td><a href='#' data-bind='click: $root.removePerson'>Remove Person</a></td>
</tr>
</tbody>
</table>
<!-- ko foreach: Aliases -->
<table>
<tbody>
<tr>
<td>#Html.DisplayNameFor(x => x.Alias.FirstNameAlias)</td>
<td>#Html.DisplayNameFor(x => x.Alias.LastNameAlias)</td>
</tr>
<tr>
<td><input data-bind="value: FirstNameAlias, name: FirstNameAlias" /></td>
<td><input data-bind="value: LastNameAlias, name: LastNameAlias" /></td>
</tr>
<tr>
<td><a href='#' data-bind='click: $root.removeAlias'>Remove Alias</a></td>
</tr>
</tbody>
</table>
<!-- /ko -->
<a href='#' data-bind='click: $root.addAlias'>Add Alias</a>
</div>
</div>
</div>
</div>
The variable model in your submit funtion is going to be undefined it's not the model you're expecting it to be.
Move the submit method to in to your model...
$(function () {
var personItem = function () {
var self = this;
self.LastName = ko.observable();
self.FirstName = ko.observable();
self.Aliases = ko.observableArray();
};
var model = ko.mapping.fromJS(#Html.Raw(Model.ToJson()));
alert('The length of the array is ' + model.People().length);
alert('The first element is ' + model.People()[0].Aliases().length);
alert(ko.toJSON(model));
model.addPerson = function () {
model.People().push(new personItem());
};
model.removePerson = function (person) {
model.People().remove(person);
};
model.submit = function() {
$.ajax({
url: '/Request/PostRequest',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: ko.toJSON(model),
success: function (status) {
alert(status);
}
});
};
ko.applyBindings(model);
});
and update the binding on your button to
<input id="clickMe" type="button" value="clickme" data-bind="click: submit" />
Also
Controller.Json expects an object to serialize rather than a string
[HttpPost]
public JsonResult PostRequest(Record viewModel)
{
return Json(String.Format("'Success':'false','Error':'"));
}
Should be:
[HttpPost]
public JsonResult PostRequest(Record viewModel)
{
return Json(new { success = false, error: String.Empty });
}
Update
Also noticed another issue with updating the model.People array
model.addPerson = function () {
model.People.push(new personItem());
};
model.removePerson = function (person) {
model.People.remove(person);
};
In these functions model.People should be model.People(), i.e.
model.addPerson = function () {
model.People().push(new personItem());
};
model.removePerson = function (person) {
model.People().remove(person);
};
Notice the () after model.People - included in the code block above.

Connecting MVC Model View with associated Knockout.js ViewModel

Background: I'm pretty new to MVC & Knockout.js but I am trying to get up to speed on these technologies. I am using MVC 5 with EF6 and Knockout.JS 3.2.
I have a Detail view that pulls a "VoteAnswer" object using MVC based on the ID passed in the URL:
For example I can go to the url MyDomain/VoteAnswers/Details/1 and it will pull the information from my database correctly (It pulls a VoteAnswer with the ID of 1) and display in my Details view. However I am trying to hook-up my Knockout.js "VoteAnswer" ViewModel to function the same way and am having trouble.
Here is my Details View: (Note the #Html.DisplayFor(model => model.VoteAnswerId) etc works and displays the data from my Database.
#model AM_SPA_TestSite.Models.VoteAnswer
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Details</title>
<script src="~/KnockoutViewModels/VoteAnswers.js"></script>
</head>
<body>
<div>
<h4>VoteAnswer</h4>
<hr />
<table>
<tr>
<td>Id</td>
<td data-bind="text: id"></td>
</tr>
<tr>
<td>Display Text</td>
<td data-bind="text: isActive"></td>
</tr>
<tr>
<td>IsActive</td>
<td data-bind="text: displayText"></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td><input type="text" data-bind="value: id" /></td>
</tr>
<tr>
<td>Display Text</td>
<td><input type="text" data-bind="value: displayText" /></td>
</tr>
<tr>
<td>IsActive</td>
<td><input type="text" data-bind="value: isActive" /></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td>#Html.DisplayFor(model => model.VoteAnswerId)</td>
</tr>
<tr>
<td>Display Text</td>
<td>#Html.DisplayFor(model => model.DisplayText)</td>
</tr>
<tr>
<td>IsActive</td>
<td>#Html.DisplayFor(model => model.IsActive)</td>
</tr>
</table>
</div>
Here is my Knockout.Js ViewModel
// VoteAnswer ViewModel
var VoteAnswerVM = {
id: ko.observable(),
displayText: ko.observable(),
isActive: ko.observable(),
SaveVoteAnswer: function () {
$.ajax({
url: '/VoteAnswers/Create',
type: 'post',
dataType: 'json',
data: ko.toJSON(this),
contentType: 'application/json',
success: function (result) {
},
error: function (err) {
if (err.responseText == "Creation Failed")
{ window.location.href = '/VoteAnswers/Index/'; }
else {
alert("Status:" + err.responseText);
window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function () {
window.location.href = '/VoteAnswers/Index/';
}
});
}
};
//Go
$(document).ready(function () {
//initialize and create new VoteAnswerVM by URL value here?
ko.applyBindings(VoteAnswerVM);
});
I know what I am missing is initializing the ViewModel with the ID of 1, but I was thinking the MVC model already has the data and the knockout.js SHOULD map to that data without manually initializing by sending a request to the database again. What am I missing? thanks.
EDIT: Added solution below. I'm not sure I am settled on this approach but here it is. Updated the controller to ONLY return a view and not query the DB. (otherwise I would have two database calls for the same data.
// GET: VoteAnswers/Details/5
public ViewResult Details(int? id)
{
return View();
}
Added an API Controller that does query the DB.
// GET: api/VoteAnswers/5
[ResponseType(typeof(VoteAnswer))]
public async Task<IHttpActionResult> GetVoteAnswer(int id)
{
VoteAnswer voteAnswer = await db.VoteAnswers.FindAsync(id);
if (voteAnswer == null)
{
return NotFound();
}
return Ok(voteAnswer);
}
In my View (.cshtml file) I reference my knockout.js ModelView, View is Below:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Details</title>
<script src="~/KnockoutViewModels/VoteAnswers.js"></script>
</head>
<body>
<div>
<h4>VoteAnswer</h4>
<hr />
<table>
<tr>
<td>Id</td>
<td data-bind="text: VoteAnswerId"></td>
</tr>
<tr>
<td>Display Text</td>
<td data-bind="text: IsActive"></td>
</tr>
<tr>
<td>IsActive</td>
<td data-bind="text: DisplayText"></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td><input type="text" data-bind="value: VoteAnswerId" /></td>
</tr>
<tr>
<td>Display Text</td>
<td><input type="text" data-bind="value: DisplayText" /></td>
</tr>
<tr>
<td>IsActive</td>
<td><input type="text" data-bind="value: IsActive" /></td>
</tr>
</table>
</div>
<div id="error"></div>
</body>
</html>
Updated My ViewModel script to access the Database based on the URL ID.
// VoteAnswer ViewModel
var VoteAnswer = function () {
var self = this;
self.VoteAnswerId = ko.observable();
self.DisplayText = ko.observable();
self.IsActive = ko.observable();
self.SaveVoteAnswer = function () {
$.ajax({
url: '/VoteAnswers/Create',
type: 'post',
dataType: 'json',
data: ko.toJSON(this),
contentType: 'application/json',
success: function (result) {
},
error: function (err) {
if (err.responseText == "Creation Failed")
{ window.location.href = '/VoteAnswers/Index/'; }
else {
alert("Status:" + err.responseText);
window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function () {
window.location.href = '/VoteAnswers/Index/';
}
});
}
self.load = function (id) {
if (id != 0) {
$.ajax({
url: '/api/VoteAnswers/' + id,
type: 'get',
data: ko.toJSON(this),
contentType: 'application/json',
success: function(data) {
self.VoteAnswerId = ko.observable(data.voteAnswerId);
self.DisplayText = ko.observable(data.displayText);
self.IsActive = ko.observable(data.isActive);
ko.applyBindings(self);
},
error: function(err) {
if (err.responseText == "Creation Failed") {
window.location.href = '/VoteAnswers/Index/';
} else {
$("#error").text("Status:" + err.responseText);
//window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function() {
//window.location.href = '/VoteAnswers/Index/';
}
});
} else {
window.location.href = '/VoteAnswers/Index/';
}
}
};
function GetURLParameter() {
var sPageUrl = window.location.href;
var indexOfLastSlash = sPageUrl.lastIndexOf("/");
if (indexOfLastSlash > 0 && sPageUrl.length - 1 != indexOfLastSlash)
return sPageUrl.substring(indexOfLastSlash + 1);
else
return 0;
}
//Go
$(document).ready(function () {
//initialize and create new VoteAnswerVM by URL value here?
var viewModel = new VoteAnswer();
viewModel.load(GetURLParameter());
});
I hope I understood you correctly, if my answer is all wrong for your question let me know where I went wrong.
First thing to realize is, if you bind a KO observable to an input field, knockout will not look at the initial value of that input field and store it in the observable. It will do the opposite: it will look at the current value in the observable and store it in the input field's value. In your case, the observable are initialized without a value, which in JavaScript means the value undefined. So if you bind your observables to the fields you've filled with Razor/MVC viewmodel, you'll immediatly overwrite those values with the empty values stored in your observables.
There IS a way to fill your Knockout model with your data through Razor, but it involves inline JavaScript and is a bad practise for a number of reasons (I will elaborate on this on request).
The best way to do this is to separate your views from your data: Don't inject the MVC viewmodel into the view, but make a separate endpoint that returns JSON and return the data there (this endpoint will receive the ID parameter instead of the view). The JSON endpoint is called from JavaScript and can be used to fill your model with the correct values.
Upsides: separation of concerns, possibility to enable view caching for a more responsive frontend, no need to use razor syntax, or even worse, combine it with inline JS. All your binding of data to the UI will happen through Knockout. I learned this myself because we also started out using razor, but on the long run this solution wasn't feasible for a big project. We never regretted the switch to always getting the data from separate JSON endpoints.
If you are unsure on how to do this I can write some pseudocode to illustrate the idea.
These may be the possible solutions
1.) data_bind is wrongly used
<td><input type="text" data-bind="value: isActive" /></td> // which is wrong
<td><input type="text" data_bind="value: isActive" /></td> //data_bind is wrongly used
2.)
If still problem exists you may try this syntax
#Html.DisplayFor(model => model.IsActive, new { data_bind = "value:IsActive" });
If you find still something missing please provide some detail info .

Child JSON object undefined when its not

I have an array of JSON objects in the page source that all work except a child object (category).
Here is the code in cshtml:
<script type="text/javascript">
var initialData = #Html.Raw(Json.Encode(ViewBag.OfferItems));
</script>
Here is the resulting page source:
<script type="text/javascript">
var initialData = [{"Id":1,"Name":"Item1","ProductVariantLinks":[{"category":{"Id":2,"Name":"Basic Pizza","Products":null},"product":{"Id":1,"Name":"Margherita","Description":null,"ProductVariants":null},"productVariant":{"Id":1,"Name":"10 inch"}},{"category":{"Id":2,"Name":"Basic Pizza","Products":null},"product":{"Id":2,"Name":"Zeno","Description":null,"ProductVariants":null},"productVariant":{"Id":4,"Name":"8 inch"}}]},{"Id":2,"Name":"Item2","ProductVariantLinks":[]}];
</script>
As far as I can tell category is there and contains properties, but it appears as undefined in IE's debugger.
Is there something I'm missing?
P.S. the JSON is valid.
Update
I'm using knockoutjs and category is in inialdata until it does ko.applybindings. I'm not sure why it would do this, code below:
/// <reference path="jquery-1.5.1.js" />
/// <reference path="knockout-2.0.0.js" />
var ProductVariantLink = function() {
var self = this;
self.category = ko.observable();
self.product = ko.observable();
self.productVariant = ko.observable();
// Whenever the category changes, reset the product selection
self.category.subscribe(function() {
self.product(undefined);
self.productVariant(undefined);
});
};
var OfferItem = function() {
var self = this;
self.Name = new String();
self.ProductVariants = new Array();
};
var SpecialOfferItemModel = function (specialOfferItems) {
var self = this;
self.specialOfferItems = ko.observableArray(ko.utils.arrayMap(specialOfferItems, function (specialOfferItem) {
return { Id: specialOfferItem.Id, Name: specialOfferItem.Name, ProductVariants: ko.observableArray(specialOfferItem.ProductVariantLinks) };
}));
self.addSpecialOfferItem = function () {
self.specialOfferItems.push({
Id: "",
Name: "",
ProductVariants: ko.observableArray()
});
};
self.removeSpecialOfferItem = function (specialOfferItem) {
self.specialOfferItems.remove(specialOfferItem);
};
self.addProductVariant = function (specialOfferItem) {
specialOfferItem.ProductVariants.push(new ProductVariantLink());
};
self.removeProductVariant = function (ProductVariant) {
$.each(self.specialOfferItems(), function () { this.ProductVariants.remove(ProductVariant) })
};
self.save = function () {
var OfferItems = new Array();
$.each(self.specialOfferItems(),
function () {
var item = this;
var offer = new OfferItem();
offer.Name = item.Name;
$.each(item.ProductVariants(),
function () {
offer.ProductVariants.push(this.ProductVariant);
});
OfferItems.push(offer);
});
self.lastSavedJson(JSON.stringify(ko.toJS(self.specialOfferItems()), null, 2));
return false;
};
self.lastSavedJson = ko.observable("");
};
var model = new SpecialOfferItemModel(initialData);
ko.applyBindings(model);
$(function () {
$('#myForm').submit(function () {
model.save();
});
});
<table class="specialOfferItemsEditor">
<tr>
<th>
</th>
<th>
Name
</th>
<th>
ProductVariants
</th>
</tr>
<tbody data-bind="foreach: specialOfferItems">
<tr>
<td>
<div>
Delete</div>
</td>
<td>
<input data-bind="value: Name" />
</td>
<td>
<table>
<tr>
<th>
Category
</th>
<th>
Product
</th>
<th>
ProductVariant
</th>
</tr>
<tbody data-bind="foreach: ProductVariants">
<tr>
<td>
<select data-bind='options: ProductCategories, optionsText: "Name", optionsCaption: "Select...", value: category, uniqueName: true'>
</select>
</td>
<td data-bind="with: category">
<select data-bind='options: Products, optionsText: "Name", optionsCaption: "Select...", value: $parent.product, uniqueName: true' >
</select>
</td>
<td data-bind="with: product">
<select data-bind='options: ProductVariants, optionsText: "Name", optionsCaption: "Select...", value: $parent.ProductVariant, uniqueName: true'
>
</select>
</td>
<td>
<a href='#' data-bind='click: $root.removeProductVariant'>Delete</a>
</td>
</tr>
</tbody>
</table>
<a href='#' data-bind='click: $root.addProductVariant'>Add Product Variant</a>
</td>
</tr>
</tbody>
</table>
The JSON isn't coming in as you're expecting. I assigned the JSON string you provided above and my IE debugger was able to find 'category' without any issues.
Screenshot: https://i.stack.imgur.com/cI60U.jpg
Try to console.log (or alert) JSON.stringify(initialData);

Resources