Bind dynamically created element to complex mvc model in view - c#

I have a very huge form in my application with a lot of different inputs and a lot of lists in my model. So i will try to add/delete the lists without sending the complete model to the server.
I tried several ways now but i don´t find a clean way. You can imagine my model like:
public class EditSomething
{
public string name { get; set;}
public List<something> somethingList { get; set;}
// a lot other fields...
public EditSomething(EditSomethingFromDatabase editSomethingFromDatabase)
{
name = editSomethingFromDatabase.Name;
somethingList = new List<SomethingModel>();
foreach(var something in editSomethingFromDatabase.Something)
{
somethingList.Add(new SomethingModel(editSomethingFromDatabase.Something));
}
}
}
The other model looks similar but without lists.
In the view i have a table for the model:
<h2>Something</h2>
<div id="SomethingDiv">
<table id="SomethingTable">
<thead>
<tr>
<th>#Html.Label("SomethingName")</th>
<th>#Html.Label("SomethingID")</th>
<th></th>
</tr>
</thead>
<tbody id="SomethingTableBody">
#Html.EditorFor(x => x.somethingList)
</tbody>
</table>
<p>
<input type="button" name="addSomething" value="Add Something" id="AddSomething">
</p>
</div>
the jquery of the addSomething is:
$('#AddSomething').click(function () {
$.ajax({
url: '#Url.Action("AddSomething", "SomethingModels")',
data: { tableSize: $('#SomethingTable tr').length },
cache: false,
success: function (html) { $('#SomethingTable tr:last').after(html); }
});
The controller method AddSomething is:
public ActionResult AddSomething (int tableSize)
{
SomethingModel something= new SomethingModel(null, (-2) * (tableSize + 1));
return PartialView(""~/Views/EditorTemplates/EditSomethingModel.cshtml"", something);
}
And at least i have a editor template in EditorTemplates as for editorfor and partialview. This have the important informations i want to send to the server:
#model SomethingModel
<tr>#TextBoxFor(m=>m.SomethingName)<td>
#TextBoxFor(m=>m.SomethingID)
So the problem now is, that the submit of the first view only post the SomethingModel to the server who already existed while opening the view but the new SomethingModel from the AddMutation method aren´t in the post. Someone an idea to fix this?
Edit: Changed the path to the editor template so i only need one view for the EditorFor and PartialView.
Edit2: To solve the main problem i created a view as following and use it as partial view. Now the data is send to the server correctlly. Only the validation on client side is still not working:
#model SomethingModel
<tr>#TextBoxFor(m=>m.SomethingName, new{Name="somethingList["+ViewBag.ListId+"].SomethingName")<span class="field-validation-valid" data-valmsg-for="somethingList[#ViewBag.ListId].SomethingName" data-valmsg-replace="true"></span><td>
<tr>#TextBoxFor(m=>m.SomethingID, new{Name="somethingList["+ViewBag.ListId+"].SomethingID")<span class="field-validation-valid" data-valmsg-for="somethingList[#ViewBag.ListId].SomethingID" data-valmsg-replace="true"></span><td>
</tr>
In the AddSomething method i added the ViewBag.ListId with the id of the next element in the list.

It seems a reasonable enough approach, but You've not shown your EditorTemplate, so I'm going to assume its something like:
#model List<something>
#for(int i = 0; i < Model.Count; i++)
{
<tr>
<td>#Html.DisplayFor(m => m[i].Id) #Html.HiddenFor(m => m[i].Id)</td>
<td>#Html.EditorFor(m => m[i].Name)</td>
</tr>
}
Your ajax method should return the HTML of a row - and this is important... the form fields need to be named 1 above the last one in the table.
So when you view the rendered source of your table (before adding any new fields it might look like:
...
<tbody>
<tr>
<td>1 <input type="hidden" name="something[0].Id" value="1"/></td>
<td><input type="text" name="something[0].Name" value="somename" /></td>
</tr>
</tbody>
You need to ensure the html returned by the ajax method for your new row is:
<tr>
<td>2 <input type="hidden" name="something[1].Id" value="2"/></td>
<td><input type="text" name="something[1].Name" value="somenewname" /></td>
</tr>
ie. the number inside the brackets is the next index for the items in something. If there is a gap in the indexes (or they overlap) then the new items will not get parsed.
EDIT - to get client side validation to work for the new fields alter your jquery ajax success callback as follows:
$('#AddSomething').click(function () {
$.ajax({
url: '#Url.Action("AddSomething", "SomethingModels")',
data: { tableSize: $('#SomethingTable tr').length },
cache: false,
success: function (html) {
$('#SomethingTable tr:last').after(html);
$.validator.unobtrusive.parse('#SomethingTable');
}
});

Related

How can I bind a dynamic-length List<> in a Razor/.NET Core PageModel, without relying on JavaScript to re-index each input?

I have a form in which a user can supply an arbitrary-length list of <DateTime, int> pairs. It is represented like so:
List<ItemsPerDay> ItemsPerDayList = new List<ItemsPerDay>();
public class ItemsPerDay {
public DateTime Date { get; set; }
public int Amount { get; set; }
}
<tbody>
#{ var i = 0; }
#foreach (var _ in Model.ItemsPerDayList) {
<tr>
<td><input asp-for="ItemsPerDayList[i].Date" type="date" /></td>
<td><input asp-for="ItemsPerDayList[i].Amount" /></td>
<td><a class="remove">Remove</a></td>
</tr>
i++;
}
</tbody>
The issue:
The user is able to add/remove rows as they need. However, the property binding relies on the pairs being properly indexed. If, for example, you remove the first item, the list now begins at [1] and the property binding does not work; ItemsPerDayList is posted as null.
My current workaround:
I've had to use some JavaScript to make sure the indexes always remain correct. This works but isn't optimal.
function reIndexItemRows() {
$("table > tbody > tr").each(function(idx) {
$(this).find("input[type=date]").attr({
"data-val": true,
"data-val-required": "The Date field is required.",
id: `ItemsPerDayList_${idx}__Date`,
name: `ItemsPerDayList[${idx}].Date`
});
$(this).find("input[type=number]").attr({
"data-val": true,
"data-val-required": "The Amount field is required.",
id: `ItemsPerDayList_${idx}__Amount`,
name: `ItemsPerDayList[${idx}].Amount`
});
});
}
The question:
What is the appropriate way to represent this model on the front-end, such that I don't have to rely on JavaScript to groom the form each time a row is added or removed?
NOTE: I am not doing any updates, therefore the indexes are not necessary. Upon submission, any existing pairs are deleted, and the form-submitted pairs are inserted.
JavaScript is necessary for adjusting index. You can add events to adjust the index when submitting the form.
Add a event on Remove. Here is the form.
<form method="post" id="myform">
<table>
<tbody>
#{ var i = 0; }
#foreach (var _ in Model.ItemsPerDayList)
{
<tr>
<td><input asp-for="ItemsPerDayList[i].Date" type="date" /></td>
<td><input asp-for="ItemsPerDayList[i].Amount" /></td>
<td><a class="remove" onclick="remove(this)" >Remove</a></td>
</tr>
i++;
}
</tbody>
</table>
<input type="submit" name="name" value="submit" />
</form>
<button id="add" onclick="add()" class="btn-primary">add</button>
Before submitting the form, javascript iterates each row and modify the index.
#section Scripts{
<script>
$('#myform').submit(function () {
var i = 0;
$("tbody> tr ").each(function () {
$(this).find("td input[name$='Date']").attr("name", "ItemsPerDayList[" + i + "].Date");
$(this).find("td input[name$='Amount']").attr("name", "ItemsPerDayList[" + i + "].Amount");
i++
})
// ...
return true; // return false to cancel form action
});
function remove(e) {
$(e).parent().parent().remove()
}
function add() {
$('tbody').append('<tr><td> <input name="ItemsPerDayList[i].Date" type="date" /></td ><td><input name="ItemsPerDayList[i].Amount" /><td><a class="remove" onclick="remove(this)">Remove</a></td></tr>');
}
</script>
}
Then, I can get the all data from front-end.

Populate BindProperty Collections when jQuery Datatables Changes Row Orders

When populating a controller's BindProperty Model Collection in MVC, it seems I can only add to a return array when the index is in order.
Here is working code I've used in several applications:
Controller code:
The bind property that gets populated:
[BindProperty]
public EditRatesModel EditModel { get; set; }
The Model above:
public class EditRatesModel
{
public EditRateType EditRateType { get; set; }
public List<PayCode> PayCodes { get; set; } <--This works as array or List
...
[HttpPost]
public RedirectToActionResult EditFields()
{
EditModel.PayCodes <-- This object gets populated on form submit
The View Code:
<form asp-controller="Home" asp-action="EditFields" method="post" id="rateForm">
<table id="editTable" style="width: 100%; display: none" class="display" cursor="pointer">
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th>Abbreviation</th>
<th>Active</th>
</tr>
</thead>
<tbody style="font-size: .8em">
#for (int i = 0; i < #Model.PayCodes.Count(); i++)
{
<tr>
<td>
<select class="hideFormBox pointerTransform" name="EditModel.PayCodes[#i].PayCodeType" id="EditModel.PayCodes[#i].PayCodeType">
#if (#Model.PayCodes[i].PayCodeType.Equals("Credit"))
{
<option selected value="Credit">Credit</option>
<option value="Debit">Debit</option>
}
else
{
<option value="Credit">Credit</option>
<option selected value="Debit">Debit</option>
}
</select>
</td>
<td>
<textarea class="hideFormBox" name="EditModel.PayCodes[#i].PayCodeDesc" id="EditModel.PayCodes[#i].PayCodeDesc" rows="1" required>#Model.PayCodes[i].PayCodeDesc</textarea>
</td>
...
<input type="hidden" name="EditModel.PayCodes[#i].PayCodeKey" id="EditModel.PayCodes[#i].PayCodeKey" value="#Model.PayCodes[i].PayCodeKey" />
</tr>
}
</tbody>
</table>
<input type="hidden" name="EditModel.EditRateType" id="EditModel.EditRateType" value="#EditRateType.PayCode" />
</form>
#section ChartScript
{
<script type="text/javascript">
$(document).ready(function() {
$('#editTable').show();
$('#editTable').DataTable({
searching: false,
dom: 'Bfrtip',
buttons: [],
}
);
});
</script>
}
My issue comes about when a user sorts or filters in jQuery Datatables.
Example of the index issue:
If I had three objects from db with index 0, 1, 2 -
If the user sorts by reverse order: 2, 1, 0 = 1 item (index 0)
If the user goes to the second page: 10, 11, 12.. = 0 items (no 0)
If the user sorts filters something: 0, 10 = 1 item
I could turn off sorting, but this wouldn't solve the paging issue.
To fix paging, id have to either load mvc with ajax, or load a new view for say 10 at a time.
My question:
There has to be a way a better way to do this without using the #HTML editor or paging in the controller?
Is there perhaps a js or jquery solution that lets me populate the dumb hard coded objects out of order?
As a last result, I suppose I could AJAX in, AJAX out? I'm not sure how to do the latter given the editing approach above though.
My apologies for the long winded and probably unconventional question. My frontend experience is limited.

Check if a checkbox is checked in a list of items

I'm building an MVC app and right now my view generates a pack of items. The user needs to check a checkbox if he wants to send the data.
Here's my view and how it is builded:
<script type="text/javascript">
$(document).ready(function() {
//alert("The document is ready");
$("#selectAll").click(function() {
//alert("The case has been clicked");
var chkValue = $(this).is(":checked");
$(".divChckBox").prop("checked", chkValue);
});
});
</script>
<p>
#using (Html.BeginForm("SendObj", "Manager"))
{
<p>
Select / UnSelet All Items #Html.CheckBox("selectAll", true)
</p>
<table>
<tr>
<th>Card Name</th>
<th>Number In Stock</th>
(...)
</tr>
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>#Html.DisplayFor(x => x[i].m_OthObj.m_ObjName)</td>
<td>#Html.DisplayFor(x => x[i].m_NbInStock)#Html.HiddenFor(x => x[i].m_NbInStock)</td>
(...)
<td>
<input type="checkbox" name="itdoesnotmatter" class="divChckBox" checked="true"/>
</td>
</tr>
}
</table>
<input type="submit" value="Send"/>
}
</p>
So you understand why I cannot use "CheckboxFor". Now what I want to do is send only the items which checkbox status is "checked". I know how to do this via model binding (checkboxfor), but I'm clueless as to how to build this.
I need to return a list of items. So how could I do this? Thank you very much!
Your form will return the values based on name, so shoot whoever told you such a stupid name :)
Use
<input type="checkbox" name="InStock" class="divChckBox" checked="true" value="#Model[i].ID" />
Or something more representative. Note that it is CRITICAL that you supply a unique identifier as the value of your checkbox. The value is how you will identify what was checked!
In your controller, there's several ways you can capture it. I do it like this:
public ActionResult Create(List<int> InStock)
{
foreach(var inStockItem in InStock)
{
//do what you need to do
}
}
The important points:
List<int> InStock
This must match the NAME attribute on your checkbox. The actual values will be the Value of your checkboxes.
Here I just randomly selected Create for your Action, but you need to make it match whatever action you are in (Edit, Index, etc..)
Good Luck!
try using the attr method to change the property checked.
$(document).ready(function() {
$("#selectAll").click(function() {
var chkValue = $(this).is(":checked");
$(".divChckBox").attr("checked", chkValue);
});
});
View code:
<!-- note "x[i].m_id"; Use the entity's id property is here
...maybe this should be m_NbInStock? -->
<input type="checkbox" name="selectedItems" value="#x[i].m_id" class="divChckBox" checked="true"/>
Controller code:
public class Manager : Controller
{
/* ... */
[HttpPost]
public ActionResult SendObj(IList<Int32> selectedItems)
{
// Grab those items by their IDs found within `selectedItems` and perform
// any processing necessary
// ...
//return View();
}
/* ... */
}

How can I create a list of object based on checkboxfor created for the model

I have this view based on a list of a model where I create strongly-typed checkboxes for each items of the model based on a boolean.
Here's my view:
#using MyApp.Models
#model IList<MyApp.Models.ObjInfo>
#{
ViewBag.Title = "Obj Inventory";
}
<h2>Search Inventory</h2>
<p>
#using (Html.BeginForm())
{
(Many search filters which are non-relevant)
<p>
Send Items: #Html.ActionLink("Click Here", "SendItems")
</p>
}
<table>
<tr>
<th>
Obj Name
</th>
<th>
Number In Stock
</th>
(...)
<th>
Select Item
</th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(modelItem => item.m_IsSelected)
</td>
</tr>
}
</table>
The whole process works fine and I can actually generate a view with checkboxes for each item of my list of model.
Now my question is that I want to create a list which would regroup only the items in the list which are checked and send them to the controller. How could I do that? Can anyone help me or suggest me a way to work?
Thank you!
* EDIT *
Here is the HttpPost Method used to get the List of items as mentioned below:
//
// GET: /Inventory/SendItems
[HttpPost]
public ActionResult SendItems(IList<ObjInfo> listToSend)
{
m_ListObjToSend = new List<ObjInfo>();
foreach (var item in listToSend.Where(item => item.m_IsSelected))
{
m_ListObjToSend .Add(item);
}
return View(m_ListObjToSend );
}
However I have encountered many problems:
This method does NOT work if I put the [HttpPost] attribute (it will show as "Not Found");
The list I am supposed to receive is null;
Each hiddenfield linked with the checkbox has default value as false even if the checked value shows true;
I am using an actionlink because I do not want to use a button, there is already one that is doing another job.
I am open for any comments / help available, thank you!
If you use the CheckBoxFor helper to generate checkboxes you will notice that it generates an additional hidden field along with each checkbox. This means that all values will be sent to the controller and you will have to filter in your controller those that are checked.
Also I would recommend you using indexes to ensure proper model binding. You just need to use an IList<ObjInfo> or ObjInfo[] which is trivially easy achievable by calling .ToList() or .ToArray() extension methods on your view model before passing it to the view:
#using MyApp.Models
#model IList<ObjInfo>
...
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
#Html.DisplayFor(x => x[i].OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(x => x[i].m_IsSelected)
</td>
</tr>
}
...
And now your controller action could directly take the list of items:
[HttpPost]
public ActionResult SomeAction(IEnumerable<ObjInfo> model)
{
...
}
and if you wanted to find the selected values, you could simply get them through LINQ:
[HttpPost]
public ActionResult SomeAction(IEnumerable<ObjInfo> model)
{
var selectedItems = model.Where(x => x.m_IsSelected);
...
}
Remark: m_Name and m_IsSelected is a disastrously bad naming convention for a properties in C#.
UPDATE:
Another issue you have with your code is that your Html.BeginForm doesn't contain any input field. It has only a single ActionLink which obviously only does a GET request. If you want to submit the values you should wrap your entire table with the form and use a submit button and not some action links:
#using MyApp.Models
#model IList<ObjInfo>
#{
ViewBag.Title = "Obj Inventory";
}
<h2>Search Inventory</h2>
<p>
#using (Html.BeginForm("SendItems", null, FormMethod.Post))
{
(Many search filters which are non-relevant)
<table>
<tr>
<th>Obj Name</th>
<th>Number In Stock</th>
(...)
<th>Select Item</th>
</tr>
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td>
<!--
This will not be sent to your controller because it's only a label.
You will need a corresponding hidden field if you want to get that value back
-->
#Html.DisplayFor(x => x[i].OtherObj.m_Name)
</td>
(...)
<td>
#Html.CheckBoxFor(x => x[i].m_IsSelected)
</td>
</tr>
}
</table>
<p>
Send Items: <button type="submit">Click Here</button>
</p>
}
</p>
So really, 2 things you should learn:
The naming convention that the default model binder expects when binding to a list
How to use a javascript debugging tool (such as FireBug and/or Chrome Developper Toolbar) which will allow you to inspect all the values that are sent to your server and immediately recognized whether you respected the convention you learned in 1.

Adding/Removing items from a list as a property of a model in a view

I'm using the guide found here http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style and am using MVC2.
I've got it working with a controller method as such:
[HttpPost]
public ActionResult CreateStockRequest(StockRequestModel viewModel, List<StockRequestModel.StockRequestItem> items)
{
viewModel.Items = items;
// Validate the request and submit it
return View(viewModel);
}
As you can see, even though my Model contains an Items method, i've had to add an items parameter, as the property on the model wasn't getting populated.
I've tried changing items to Items in the BeginCollectionItem method and have tried various other values but I can't get it to work without adding the seperate items parameter in the controller method.
tl;dr: How can I add/remove/edit items in a list property of a model, from a view?
View
<table>
<thead>
<tr>
<td><%= Html.LabelFor(m => m.Items[0].Item )%></td>
<td><%= Html.LabelFor(m => m.Items[0].Quantity )%></td>
</tr>
</thead>
<tbody id="editorRows">
<% foreach (var item in Model.Items)
{
Html.RenderPartial("StockRequestItemEditor", item);
}%>
</tbody>
<tfoot>
<tr><td colspan="2"> </td></tr>
<tr>
<td colspan="2"><%= Html.ActionLink("Add Item...", "BlankEditorRow", null, new { id = "addItem" })%></td>
<script type="text/javascript">
$("#addItem").click(function() {
$.ajax({
url: this.href,
cache: false,
success: function(html) { $("#editorRows").append(html); }
});
return false;
});
</script>
</tr>
</tfoot>
</table>
Partial View
<tr>
<% using(Html.BeginCollectionItem("Items")) { %>
<td>
<%= Html.ComboBoxFor(m => m.Item,
null,
Url.Action("Products", "Data", new { area = (string)null }),
Model.Item,
2)%>
</td>
<td><%= Html.TextBoxFor(m => m.Quantity)%></td>
<% } %>
</tr>
It's a long shot but maybe this is the problem:
Html.RenderPartial("StockRequestItemEditor", item);
I noticed when inspecting the viewModel in the POST action that it would have the correct number of items in the collection, but they would all be null. That suggests to me that this is a prefixing issue with the model binder. So maybe something like this will work:
var dictPrefix = new ViewDataDictionary();
dictPrefix.TemplateInfo.HtmlFieldPrefix = "SomePrefix";
Html.RenderPartial("StockRequestItemEditor", item, dictPrefix);
I don't think RenderPartial() passes along the prefix without using this overload (might be wrong though). I'm not completely sure how the bind prefixing works, so I don't actually know what the name would be, but it seems like it's relevant here. The collection definitely has the correct number of items for me, but none of them are correctly bound.
Hopefully this will be enough to push someone else to giving you the right answer.
Why don't you use a view model?
public class StockRequestModel
{
public List<StockRequestItem> Items { get; set; }
... some other properties
}
and then have your controller action take this view model as parameter:
[HttpPost]
public ActionResult CreateStockRequest(StockRequestModel viewModel)
{
// TODO: do something with viewModel.Items ...
return View(viewModel);
}
and inside your view:
<div class="editorRow">
<% using(Html.BeginCollectionItem("Items")) { %>
...
<% } %>
</div>

Categories

Resources