Data-binding MVC-4 to a Knockout.js foreach - razor

We have a form in our web application that, in one place, asks the user to enter a list of values. We wrote this part of the form using Razor and knockout.js so that there is a textbox for each value of the list, similar to this tutorial. How can we data-bind these textboxes to our MVC model?
Here is our form:
#model OurProject.Models.Input.InputModel
#{
ViewBag.Title = "Input";
}
<h2>
Inputs</h2>
<div id="inputKOApp">
#using (Html.BeginForm())
{
<!-- snip - lots of part of our form that work correctly -->
<div class="row-fluid">
<div class="control-group">
<div class="span8 control-label">
#Html.LabelFor(model => model.POSTransactionCodes)
</div>
<div class="controls">
<!-- These are the textboxes we would like to bind to MVC -->
<ul class="pull-right" data-bind="foreach: POSTransactionCodes">
<li>
<input data-bind="value: code" />
Delete</li>
</ul>
<button class="pull-right" data-bind="click: addPOSTransactionCode">
Add another POS Transaction Code</button>
#Html.ValidationMessageFor(model => model.POSTransactionCodes, null, new { #class = "help-inline" })
</div>
</div>
</div>
<!-- snip - more cshtml that is irrelevant to the problem -->
</div>
<input type="submit" value="Submit" />
}
</div>
<script type="text/javascript" src='~/Scripts/jquery-1.8.2.min.js'></script>
<script type="text/javascript" src='~/Scripts/knockout-2.1.0.js'></script>
<script type="text/javascript" src='~/Scripts/OP/OP.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Form.js'></script>
<script type="text/javascript" src='~/Scripts/OP/Input/OP.Input.Input.Data.js'></script>
<script type="text/javascript">
var inputApp = $('#inputKOApp')[0];
OP.Input.Form.init(inputApp);
</script>
Here is our knockout.js script, OP.Input.Input.Form.js:
extend(OP, 'OP.Input.Form');
OP.Input.Form = function (jQuery) {
var TransactionCodeView = function () {
var self = this;
self.code = "";
};
//The ViewModel for the page
var ViewModel = function () {
var self = this;
//Fields
/* snip - lots of fields that work as expected */
self.POSTransactionCodes = ko.observableArray([]); //is a list of transaction codes
//Set up with initial data
/* I'm guessing that we won't need this function anymore since MVC will populate
* everything for us, but I'll leave it in until I'm far enough along to know
* I won't need to gut lots of stuff */
self.initialize = function () {
var c = function (data, status, response) {
/* snip - lots of fields that work as expected */
if (status === "success") {
if(data.POSTransactionCodes != null) ko.utils.arrayPushAll(self.POSTransactionCodes, data.POSTransactionCodes);
self.POSTransactionCodes.valueHasMutated();
} else {
}
};
OP.Input.Data.GetInput(c);
}
//When saving, submit data to server
self.save = function (model) {
var c = function (data, status, response) {
if (status === "success") {
} else {
}
};
OP.Input.Data.SaveInput(model, c);
}
//Modifying POSTransactionCodes array
self.removePOSTransactionCode = function (POScode) {
self.POSTransactionCodes.remove(POScode);
}
self.addPOSTransactionCode = function () {
self.POSTransactionCodes.push(new TransactionCodeView());
}
};
//Connect KO form to HTML
return {
init: function (elToBind) {
var model = new ViewModel();
ko.applyBindings(model, elToBind);
model.initialize();
}
};
} ($);
Here is our MVC model:
namespace OurProject.Models.Input
{
public class InputModel : IModel
{
//Snip - lots of properties that aren't interesting for this problem
[Required]
[DisplayName("POS Transaction Codes")]
public List<double> POSTransactionCodes { get; set; }
public InputModel()
{ }
/* I ommitted a few other methods that
* aren't relevant to this problem. */
}
}

I don't see how do you send the data back to the server but you need to name your inputs in way which allows model binding:
If you're binding to a list/collection your inputs should be name like:
<input type="text" name="CollectionPropertyName[index]" />
You can read about Model Binding To A List in this article
So you just need generate proper names for your inputs:
<input data-bind="value: code, attr: { name: 'POSTransactionCodes[' + $index() + ']' }" />
You should note that the above solution may only works if you're using the submit button and send the data form-urlencoded if you are sending the data as json you may need to tweak your serialization logic to make the model binder happy:
In this case your json should something like this:
{
//...
POSTransactionCodes: [ 1 , 3 ]
//..
}

Thanks to nemesv's answer show me light on this one.
But I need to have the "normal" name and id attribute for jquery validation. So I come up with the idea of writing my own data binder.
ko.bindingHandlers.nameId = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor(),
allBindings = allBindingsAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
var bindName = $(element).data('bind-name');
bindName = bindName.replace('{0}', valueUnwrapped);
$(element).attr({name : bindName, id : bindName});
}
};
And use it in html
<input
data-bind="value: qty, nameId: $index"
data-bind-name="inventory[{0}].qty" />
jsfiddle

Related

How do I update the model for a WebGrid using Ajax?

I'm trying to create a WebGrid that allows me to update the data source with Ajax. The data source is assigned a List<> containing a model I created to hold the data. The WebGrid populates correctly and I have no problem sorting using the header of the WebGrid or paging.
The code for the View:
#model IEnumerable<TestDataModel.Models.TestData>
#{
ViewBag.Title = "Test Website";
var grid = new WebGrid(canPage: true, canSort: true, ajaxUpdateContainerId: "grid", ajaxUpdateCallback: "callBack");
grid.Bind(Model, rowCount: 10);
grid.Pager(WebGridPagerModes.All);
#grid.GetHtml(htmlAttributes: new { id = "grid" },
tableStyle: "webgrid-table",
headerStyle: "webgrid-header",
alternatingRowStyle: "webgrid-alternating-row",
rowStyle: "webgrid-row-style",
mode: WebGridPagerModes.All,
firstText: "<< First",
previousText: "< Prev",
nextText: "Next >",
lastText: "Last >>",
columns: grid.Columns("This is a place holder,
I have a lot of columns"
));
}
<div>
<br />
<p>
Page Number: <input type="text" name="pageNumber" id="pageNumber" />
</p>
<p>
<input type="button" name="pageNumberButton" id="pageNumberButton" value="Page" />
</p>
</div>
<script type="text/javascript">
$(document).ready(function () {
$("#pageNumberButton").click(function () {
var pageNumber = $("#pageNumber").val();
$.ajax({
url: '#Url.Action("GetData", "Test")?pageNumber=' + pageNumber,
success: function (data) {
$("#grid").html(data);
},
error: function (data) {
alert("Error");
}
})
});
});
</script>
The bottom script tag is the ajax call that I'm trying to use to update the WebGrid. All that the "GetData" does is create a list pulling data from an SQL server and returns Json(testData, JsonRequestBehavior.AllowGet).
Code for Controller:
public ActionResult GetData(string pageNumber)
{
List<TestData> testData = new List<TestData>();
if (string.IsNullOrEmpty(pageNumber))
{
testData = Data.Data.TestDataGet();
}
else
{
try
{
var pageInt = Convert.ToInt32(pageNumber);
List<ParameterSQL> parameter = new List<ParameterSQL>();
parameter.Add(new ParameterSQL { Name = "#pageOffset", Value = pageInt });
testData = Data.Data.TestDataGet(parameter);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
return Json(testData, JsonRequestBehavior.AllowGet);
}
The error that I'm experiencing is when I push the button the WebGrid disappears entirely. The reason that I need to be able to update the WebGrid data source is because my table is millions of lines long and I'm only grabbing them a few thousand at a time. Once the user reaches the end of the of the list I want them to be able to load more using a SQL query that I already have working. Any help would be great, thanks.

How to bind object list in a form submission to a spring #RequestBody with Thymeleaf

I am trying to bind a list of object in Spring controller that is annotated with #RequestBody. I am using Thymeleaf for template engine. I am using Kotlin programming language.
Here is the HTML code
<form class="attendanceBook" id="form_submit" role="form" method="post" action="/attendances">
<table class="table table-striped">
<th>Role Number</th>
<th style="width: 30%">Student Name</th>
<th>Present</th>
<div th:each="std, stat : ${studentList}" class="separator">
<tr>
<td th:text="${std.roleNumber}"></td>
<td th:text="${std.studentName}"></td>
<td><input type="checkbox" th:name="|i[__${stat.index}__]|" id="i" /></td>
<td><input type="text" th:name="|t[__${stat.index}__]|" th:value="*{studentList[__${stat.index}__].studentId}"/> </td>
</tr>
</div>
</table>
<input type="submit" id="submit_btn" value="Click"/>
</form>
When I submit the form that converted to the Json format with jQuery. Here is only that JavaScript part which convert form data to Json.
var genFormSubmitParams = function (context) {
var _this = $(context),
data = {};
_this.find('[name]').each(function (index, value) {
var _this = $(this),
name = _this.attr('name'),
value = _this.val();
data[name] = value;
});
var params = {};
params["data"] = JSON.stringify(data);
console.log(params["data"]);
return params;
};
Here is the Controller.
#PostMapping
fun patchAttendance(#RequestBody attendanceJson: AttendanceJsonWrapper): ResponseEntity<*> {
{
logger.info("attendanceJson list size {}", attendanceJson.attendanceJsons?.size)
logger.info("attendanceJson {}", attendanceJson)
return responseOK(attendanceJson)
}
This is the data class in which I need to bind one single row.
data class AttendanceJson (
var i: String = "",
var t: String = ""
)
And here is the wrapper class for the actual data class for list of object. I have written this in Java.
public class AttendanceJsonWrapper {
List<AttendanceJson> attendanceJsons;
public AttendanceJsonWrapper() {
}
public AttendanceJsonWrapper(List<AttendanceJson> attendanceJsons) {
this.attendanceJsons = attendanceJsons;
}
public List<AttendanceJson> getAttendanceJsons() {
return attendanceJsons;
}
public void setAttendanceJsons(List<AttendanceJson> attendanceJsons) {
this.attendanceJsons = attendanceJsons;
}
}
When I am posting the JavaScript console show this in log
{"i[0]":"on","t[0]":"90","i[1]":"on","t[1]":"106"}
But it is not binding in the data class AttendanceJson as object. The attendanceJsons list remain null but the ajax call has success.
How can I bind this object list to back end ? Thanks in advance.
try .serializeArray() from jquery
$( "form" ).submit(function( event ) {
console.log( $( this ).serializeArray() );
event.preventDefault();
});
https://api.jquery.com/serializeArray/

Typeahead each time user writes a certain word

I'm using the ui-bootstrap typeahead for when a user types to show all the variables available for him to write which are proprieties from an object which is loaded Ex: item.cost+item.quantity.
My question is I want the suggestions only to appear each time user types "item.", I've notice the typeahead only works for one word and at the beginning.
html
<div class="modal-body">
Add expression:
<textarea style="width: 568px;" ng-model="item.formula"
uib-typeahead="state for state in states "
typeahead-show-hint="true"
typeahead-on-select="item"
ng-change="eval(item.formula)">
</textarea>
<p><b>Result:</b> <br>
<div style="width: 100%">{{ans}}
</div>
</p>
</div>
controller
ctrl.controller('myController', ['$scope', function ($scope) {
$scope.imageShowModal = function (item) { //loads the object items
$scope.item = item;
$scope.states =Object.keys(item); //get the JSON keys from item object like cost,price,quantity,workflow...
};
$scope.eval = function (v) {
try {
$scope.ans = $scope.$eval(v);
} catch (e) {
}
};
You can use a custom filter in your uib-typeahead expression, ex: uib-typeahead="state for state in states | myCustomFilter:$viewValue"
A custom filter in your case might look like this:
angular.module('myApp')
.filter('myCustomFilter', function() {
return function(list, term) {
if (term.indexOf('item.') === -1)
return [];
var filteredList = [];
angular.forEach(list, function(value) {
if (value.indexOf(term) !== -1)
filteredList.push(value);
});
return filteredList;
}
});
See also: AngularJs UI typeahead match on leading characters

MultipartMemoryStreamProvider and reading user data from MultiPart/Form Data

I have a file and user data that is being posted from Multipart/form data to a post method in my apicontroller class.
I am able to read the file without any problems but unable to read user data.
I tried couple of things like using model binding, passing the individual fields as a method parameter in the post method but i get: No MediaTypeFormatter is available to read an object of type 'FormDataCollection' from content with media type 'multipart/form-data'.
var provider = await Request.Content.ReadAsMultipartAsync(new MultipartMemoryStreamProvider());
foreach (var item in provider.Contents)
{
var fieldName = item.Headers.ContentDisposition.Name.Trim('"');
if (item.Headers.ContentDisposition.FileName == null)
{
var data = await item.ReadAsStringAsync();
if (fieldname == "name")
{
Name = data;
}
else
{
fileContents = await item.ReadAsByteArrayAsync();
}
}
}
Thanks.
It seems to me the OP, was really close. This is some code that tries to clearly show how to get the form variables, as well as the file upload data.
First the ApiController:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http;
namespace WebApplication1.Controllers
{
public class FormAndFileDataController : ApiController
{
private class FormItem
{
public FormItem() { }
public string name { get; set; }
public byte[] data { get; set; }
public string fileName { get; set; }
public string mediaType { get; set; }
public string value { get { return Encoding.Default.GetString(data); } }
public bool isAFileUpload { get { return !String.IsNullOrEmpty(fileName); } }
}
/// <summary>
/// An ApiController to access an AJAX form post.
/// </summary>
/// <remarks>
///
/// </remarks>
/// <returns></returns>
public async Task<HttpResponseMessage> Post()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
var provider = new MultipartMemoryStreamProvider();
await Request.Content.ReadAsMultipartAsync(provider);
var formItems = new List<FormItem>();
// Scan the Multiple Parts
foreach (HttpContent contentPart in provider.Contents)
{
var formItem = new FormItem();
var contentDisposition = contentPart.Headers.ContentDisposition;
formItem.name = contentDisposition.Name.Trim('"');
formItem.data = await contentPart.ReadAsByteArrayAsync();
formItem.fileName = String.IsNullOrEmpty(contentDisposition.FileName) ? "" : contentDisposition.FileName.Trim('"');
formItem.mediaType = contentPart.Headers.ContentType == null ? "" : String.IsNullOrEmpty(contentPart.Headers.ContentType.MediaType) ? "" : contentPart.Headers.ContentType.MediaType;
formItems.Add(formItem);
}
// We now have a list of all the distinct items from the *form post*.
// We can now decide to do something with the items.
foreach (FormItem formItemToProcess in formItems)
{
if (formItemToProcess.isAFileUpload)
{
// This is a file. Do something with the file. Write it to disk, store in a database. Whatever you want to do.
// The name the client used to identify the *file* input element of the *form post* is stored in formItem.name.
// The *suggested* file name from the client is stored in formItemToProcess.fileName
// The media type (MimeType) of file (as far as the client knew) if available, is stored in formItemToProcess.mediaType
// The file data is stored in the byte[] formItemToProcess.data
}
else
{
// This is a form variable. Do something with the form variable. Update a DB table, whatever you want to do.
// The name the client used to identify the input element of the *form post* is stored in formItem.name.
// The value the client input element is stored in formItem.value.
}
}
return Request.CreateResponse(HttpStatusCode.OK);
}
}
}
and the MVC View to test it:
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
<script type="text/javascript">
var hiddenForm, hiddenFile;
function initialize() {
// Use a hidden file element so we can control the UI
// of the file selection interface. The built in browser
// UI is not localizable to different languages.
hiddenFile = document.createElement("input");
hiddenFile.setAttribute("type", "file");
hiddenFile.setAttribute("style", "display: none;");
// We don't need the form really, but it makes it easy to
// reset the selection.
hiddenForm = document.createElement("form");
hiddenForm.appendChild(hiddenFile);
hiddenFile.onchange = function () {
var elementToUpdate = document.getElementById("fileNameToUpload");
var filesToUpload = hiddenFile.files;
var fileToUpload = filesToUpload[0];
elementToUpdate.value = fileToUpload.name;
}
document.body.appendChild(hiddenForm);
}
function chooseFile() {
hiddenFile.click();
}
function clearFile() {
var elementToUpdate = document.getElementById("fileNameToUpload");
elementToUpdate.value = "";
hiddenForm.reset();
}
function testAJAXUpload() {
// We are going to use the FormData object and jQuery
// to do our post test.
var formToPost = new FormData();
var formVariableNameElement = document.getElementById("variableNameToUpload");
var formVariableValueElement = document.getElementById("variableValueToUpload");
var formVariableName = formVariableNameElement.value || "formVar1";
var formVariableValue = formVariableValueElement.value || "Form Value 1";
var filesToUpload = hiddenFile.files;
var fileToUpload = filesToUpload[0];
formToPost.append(formVariableName,formVariableValue)
formToPost.append("fileUpload", fileToUpload);
// Call the Server.
$.ajax({
url: '#Url.HttpRouteUrl("DefaultApi", new { controller = "FormAndFileData" })',
type: 'POST',
contentType: false,
processData: false,
data: formToPost,
error: function (jqXHR, textStatus, errorThrown) {
alert("Failed: [" + textStatus + "]");
},
success: function (data, textStatus, jqXHR) {
alert("Success.");
}
});
}
</script>
</head>
<body>
<input id="variableNameToUpload" type="text" placeholder="Form Variable: Name" />
<br />
<input id="variableValueToUpload" type="text" placeholder="Form Variable: Value" />
<br />
<input id="fileNameToUpload" type="text" placeholder="Select A File..." /><button onclick="chooseFile()">Select File</button><button onclick="clearFile()">Reset</button>
<br />
<button onclick="testAJAXUpload()">Test AJAX Upload</button>
<script type="text/javascript">
initialize();
</script>
</body>
</html>
I had considered adding this to your other post per your comment, but (as you also decided), it is a separate question.
public async Task<HttpResponseMessage> Post()
{
if (!Request.Content.IsMimeMultipartContent())
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
try
{
string root = HttpContext.Current.Server.MapPath("~/App_Data");
var provider = await Request.Content.ReadAsMultipartAsync(new MultipartFormDataStreamProvider(root));
// file data
foreach (MultipartFileData file in provider.FileData)
{
using (var ms = new MemoryStream())
{
var diskFile = new FileStream(file.LocalFileName, FileMode.Open);
await diskFile.CopyToAsync(ms);
var byteArray = ms.ToArray();
}
}
// form data
foreach (var key in provider.FormData.AllKeys)
{
var values = provider.FormData.GetValues(key);
if (values != null)
{
foreach (var value in values)
{
Console.WriteLine(value);
}
}
}
return Request.CreateResponse(HttpStatusCode.Created);
}
catch (Exception ex)
{
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
}
}

MVC4 WebApi Knockout JSON Invalid operand to 'in'

Hy, I'm stuck with this error message and I can not find an solution.
I get this message error in the Knockout JavaScript library v2.2.0:
Unhandled exception at line 1053, column 5 in
localhost:port/Scripts/knockout-2.2.0.debug.js 0x800a138f -
Microsoft JScript runtime error: Invalid operand to 'in': Object
expected If there is a handler for this exception, the program may be
safely continued.
It stops at this line of code in knockout-2.2.0.debug.js
if ((initialValues !== null) && (initialValues !== undefined) && !('length' in initialValues))
I use this WebApi:
public class ProductsController : ApiController
{
IEnumerable<Product> products = new List<Product>()
{
new Product { Id = 1, Name = "Tomato_Soup", Category = "Groceries", Price = 1 },
new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
};
public IEnumerable<Product> GetAllProducts(){
return products.AsEnumerable(); }
The scripts that I use are in a header section
#section Testscripts
{
<script src="~/Scripts/jquery-1.8.2.js"></script>
<script src="~/Scripts/knockout-2.2.0.debug.js"></script>
}
And the Knockout code in the footer default script section
#section scripts
{
<script type="text/javascript">
var apiUrl = '#Url.RouteUrl("DefaultApi", new { httproute = "", controller = "products" })';
function Product(data) {
this.Id = ko.observable(data.Id);
this.Name = ko.observable(data.Name);
this.Price = ko.observableArray(data.Price);
this.Category = ko.observable(data.Category);
}
function ProductViewModel() {
var self = this;
self.myproducts = ko.observableArray([]);
$.getJSON(apiUrl, function (allData) {
var mappedProducts = $.map(allData, function (item) { return new Product(item) });
self.myproducts(mappedProducts);
});
};
ko.applyBindings(new ProductViewModel);
}
and show the data in body:
<ul data-bind="foreach: myproducts">
<li>
<input data-bind="value: Id" />
<input data-bind="value: Name" />
<input data-bind="value: Category" />
<input data-bind="value: Price" />
</li>
</ul>
The bug is in your Product function.
You want to create an ko.observableArray from data.Price which is a decimal value and not an array of values, which results in this not so nice exception.
Change to ko.observable and it should work:
function Product(data) {
this.Id = ko.observable(data.Id);
this.Name = ko.observable(data.Name);
this.Price = ko.observable(data.Price);
this.Category = ko.observable(data.Category);
}