Posting dynamically added list items to controller - c#

I have a partialview that has a viewmodel containing a form with only email and name input. I post this data with jquery ajax to my controller to do some validation. My controller is a PartialViewResult method, see:
[HttpPost]
public PartialViewResult InviteUser(InviteEmailViewModel item)
{
return PartialView("_InvitedUsers", item);
}
My jquery adds the result of this method to the DOM, since ive uses hiddenfor i get inputs like:
<input data-val="true" data-val-required="Name is required." id="Name" name="Name" type="hidden" value="test">
<input data-val="true" data-val-regex="Invalid Email Address" data-val-regex-pattern="^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$" data-val-required="Email Address is required." id="Email" name="Email" type="hidden" value="test#test.test">
The problem with this is that my ViewModel contains a List but after posting the form with the added partialviews the List is Null. Ive tested with hardcoded input fields like:
<input data-val="true" data-val-required="Name is required." id="Name" name="InviteEmailViewModel[0].Name" type="hidden" value="test">
and that does seem to work. How do i make MVC generate hidden fields like in my hardcoded example?

To answer your Q:
To include hidden fields in the post, these need to 'survive' the create-in-controller-survive-the-post, if the field isn't in the form, then it won't be posted back to the controller, regardless if it's been created before in the controller.
Add the hidden field anywhere within the form so that it's posted back. So when it's created it'll have a container and will therefor be present on post.
IE:
#Html.HiddenFor(m => m.myHiddenField)
#Html.TextBoxFor(m => m.displayedField, new { #class = "NotHidden" })
What it could be:
If I'm understanding correctly you've got a ViewModel with a List, which is the model of the partial, which you want to add dynamically to your VM and bind on post.
To add items to a list dynamically, without them going null, you'll need to use BeginCollectionItem, which handles the hidden GUID issue for you.
I had similar issues and asked two questions which have solutions here and here, the latter is a problem with accessing a property two models down, but shows more complete code.
The issues that you've got are with the GUID. It doesn't go back through the post without help.
What you need is a prefix in the name/id so that the object can be properly assigned to its model when you post.
BeginCollectionItem will sort this out for you - which you can get 'stock' via typing Install-Package BeginCollectionItem in Package Manager Console in Visual Studio, check out this guide - here or you can download the file/whole thing and add it to your project manually by going to GitHub here
name="viewmodel[f7d8d024-5bb6-451d-87e3-fd3e3b8c1bba].listOfObjects[d5c08a43-f65e-46d1-b224-148225599edc].objectProperty" is what you need on the dynamically created model properties so that they all bind on post back.
Good luck. =)

Related

Add item into List from View Loaded Dynamically and pass it to Controller in asp.net core

I am working on Mobile Store Management System's Order page. I want to allow users to select a company through a select list, and then select multiple models of that company from another select list which is loaded dynamically through AJAX.
The code for the cascading models is working, but I am unable to send the selected models to the server because it is adding them in the DOM through JavaScript.
The following is the code for the cascading selection:
<div class="form-group row">
<label class="control-label col-6">Company Name</label>
<div class="col-12">
<select id="CompanyId" class="custom-select mr-sm-2"
asp-items="#(new SelectList(
#ViewBag.Companies,"Phoneid","Com_name"))">
<option value="">Please Select</option>
</select>
</div>
<span class="text-danger"></span>
</div>
<div class="form-group row">
<label class="control-label col-6"></label>
<div class="col-12">
<select id="modelId" multiple class="custom-select mr-sm-2"
asp-items="#(new SelectList(string.Empty,"modelId","model_name","--Select--"))">
<option value="">Please Select</option>
</select>
</div>
<span class="text-danger"></span>
</div>
<div>
<input type="button" id="saveBtn" value="Save" />
</div>
Cascading Code:
$("#CompanyId").change(async function()
{
await $.getJSON("/Order/GetAllModels",{ PhoneId: $("#CompanyId").val()},
function(data)
{
$("#modelId").empty();
$.each(data, function (index, row) {
$("#modelId").append("<option value='" + row.modelId + "'>" +
row.model_name + '</option>')
});
});
}
Once the Save button is clicked, I am displaying the product for the currently selected models using a partial view:
$('#saveBtn').click(function () {
$.ajax({
url: '/Order/GetProduct?Phoneid=' + $("#CompanyId").val() + "&modelId=" + $('#modelId').val(),
type: 'Post',
success: function (data) {
$('#products').append(data);
},
})
})
Problem 1
When the user selects the first company and their two models, and then clicks the Save button, the partial view loads with indexes i=0,i=1. Then, the user selects another company and selects their models. Again, the partial view renders with same indexes. How can I make the indexes unique? This partial view is rendered when the user clicks the Save button, which renders only the current company's selected models.
#model List<Mobile_Store_MS.ViewModel.Orders.Products>
<table class="table">
<tbody>
#for (int i = 0; i < Model.Count; i++)
{
<tr class="card d-flex">
<td>
<input asp-for="#Model[i].isSelected" />
</td>
<td>
<input hidden asp-for="#Model[i].Phoneid" /> <input hidden asp-for="#Model[i].modelId" />
#Html.DisplayFor(modelItem => Model[i].com_Name) #Html.DisplayFor(modelItem => Model[i].model_name)
</td>
<td>
<input asp-for="#Model[i].Quantity" />
</td>
<td>
<input class="disabled" readonly asp-for="#Model[i].price" />
</td>
</tr>
}
</tbody>
</table>
Problem 2
How can I send all of the items rendered through the partial view to the server? I just want to send these selected products along with the quantity and price for each model to the server. This means binding these items in the product list of the OrderViewModel.
You can find my OrderViewModel and Products model in the following diagram:
Can you tell me how to bind Razor items into a list to post to the controller? I would be very grateful if you give me some suggestions.
Related
Link of my Previous Question
Sample of my order page
TL;DR: Instead of relying on the asp-for tag helper, you can set your own name attribute. This gives you the flexibility to start the index at whatever number you want. Ideally, you will pass the number of existing products to GetProduct() and start indexing off of that. In addition, you also need to prefix your name attribute with Products, thus ensuring those form elements are properly bound to your OrderViewModel.Products collection on post back.
<input name="Products[#(startIndex+i)].Quantity" value="#Model[i].Quantity" />
You can then filter the OrderViewModel.Products collection on the server-side using LINQ to limit the results to selected products:
var selectedProducts = c.Products.Where(p => p.IsSelected);
For a more in-depth explanation of how this approach works, as well as some of the variations in the implementation, read my full answer below.
Detailed Answer
There's a lot going on here, so this is going to be a lengthy answer. I'm going to start by providing some critical background on how ASP.NET Core MVC connects the dots between your view model, your view, and your binding model, as that will better understand how to adapt this behavior to your needs. Then I'm going to provide a strategy for solving each of your problems.
Note: I'm not going to write all of the code, as that would result in me reinventing a lot of code you've already written—and would make for an even longer answer. Instead, I'm going to provide a checklist of steps needed to apply the solution to your existing code.
Background
It's important to note that while ASP.NET Core MVC attempts to standardize and simplify the workflow from view model to view to binding model through conventions (such as the asp-for tag helper) these are each independent of one another.
So when you call asp-for on a collection using e.g.,
<input asp-for="#Model[i].Quantity" />
It then outputs the something like the following HTML:
<input id="0__Quantity" name="[0].Quantity" value="1" />
And then, when you submit that, the server looks at your form data, and uses a set of conventions to map that data back to your binding model. So this might map to:
public async Task<IActionResult> ProductsAsync(List<Product> products) { … }
When you call asp-for on a collection, it will always start the index at 0. And when it bind the form data to a binding model, it will always start at [0] and count up.
But there's no reason you need to use asp-for if you need to change this behavior. You can instead set the id and/or name attributes yourself if you need flexibility over how they are generated.
Note: When you do this, you'll want to make sure you're still sticking to one of the conventions that ASP.NET Core MVC is already familiar with to ensure data binding occurs. Though, if you really want to, you can instead create your own binding conventions.
Problem 1: Setting the index
Given the above background, if you want to customize the starting index returned from your call to GetProducts() for your second model, you‘ll want to do something like the following:
Before calling GetProduct(), determine how many products you already have by e.g. counting the number of elements with the card class assigned (i.e., $(".card").length).
Note: If the card class is not exclusively used for products, you can instead assign a unique class like product to each tr element in your _DisplayOrder view and count that.
Include this count in your call to GetProduct() as e.g., a &startingIndex= parameter:
$('#saveBtn').click(function () {
$.ajax({
url: '/Order/GetProduct?Phoneid=' + $("#CompanyId").val() + "&modelId=" + $('#modelId').val() + "&startingIndex=" + $("#card").length,
type: 'Post',
success: function (data) {
$('#products').append(data);
},
})
})
[HttpPost]
public IActionResult GetProduct(int Phoneid, string[] modelId, int startingIndex = 0) { … }
Relay this startingIndex to your "partial" view via a view model; e.g.,
public class ProductListViewModel {
public int StartingIndex { get; set; }
public List<Product> Products { get; set; }
}
Use that value to offset the index written to the output:
<input id="#(Model.StartingIndex+i)__Quantity" name="[#(Model.StartingIndex+i)].Quantity" value="#Model.Products[i].Quantity" />
That's not as tidy as asp-for since you're needing to wire up a lot of similar information, but it offers you the flexibility to ensure that your name values are unique on the page, no matter how many times you call GetProduct().
Notes
If you don't want to create a new view model, you could relay the startingIndex via your ViewData dictionary instead. I prefer having a view model that includes all of the data I need, though.
When using the asp-for tag helper, it automatically generates the id attribute, but if you're not ever referencing it via e.g. JavaScript you can omit it.
Browsers will only submit values to the server for form elements that have a name attribute. So if you have input elements that are needed on the client-side but aren't needed in the binding model, for some reason, you can omit the name attribute.
There are other conventions besides {Index}__{Property} that you can follow. But unless you really want to get into the weeds of model binding, you're best off sticking to one of the existing collection conventions.
Be careful of your indexing!
In the Model Binding conventions for collections, you'll notice a warning:
Data formats that use subscript numbers (... [0] ... [1] ...) must ensure that they are numbered sequentially starting at zero. If there are any gaps in subscript numbering, all items after the gap are ignored. For example, if the subscripts are 0 and 2 instead of 0 and 1, the second item is ignored.
As such, when assigning these, you need to make sure that they're sequential without any gaps. If you're using the count (.length) of existing e.g. $(".card") or $(".product") elements on your page to seed the startingIndex value, however, then that shouldn't be a problem.
Problem 2: Sending these values to the server
As mentioned above, any form element with a name attribute will have its data submitted to the server. So it doesn't really matter if you're using asp-for, writing out your form manually using HTML, or constructing it dynamically using JavaScript. If there's a form element with a name attribute, and it's within the form being submitted, it will get included in the payload.
Debugging your form data
You're likely already familiar with this, but if not: If you use your browser's developer console, you'll be able to see this information as part of the page metadata when you submit your form. For instance, in Google Chrome:
Go to Developer Tools (Ctrl+Shift+I)
Go to the Network tab
Submit your form
Click on the name of your page (normally the first entry)
Go to the Headers tab
Scroll down to the Form Data section (or Query String Parameters for a GET request)
You should see something like:
[0].isSelected true
[0].Phoneid 4
[0].modelId 10
[0].Quantity 5
[0].price 10.50
[1].isSelected true
[…]…
If you're seeing these in Chrome, but not seeing these data reflected in your ASP.NET Core MVC controller action, then there's a disconnect between the naming conventions of these fields and your binding model—and, thus, ASP.NET Core MVC doesn't know how to map the two.
Binding Problems
There are two likely issues here, both of which might be interfering with your data binding.
Duplicate Indexes
Since you are currently submitting duplicate indexes, that could be causing collisions with the data. E.g., if there are two values for [0].Quantity, then it will retrieve those as an array—and may fail to bind either value to e.g. the int Quantity property on your Products binding model. I haven't tested this, though, so I'm not entirely sure how ASP.NET Core MVC deals with this situation.
Collection Name
When you bind to a List<Products> with the asp-for tag helper, I believe it will use the [i].Property naming convention. That's a problem because your OrderViewModel is not a collection. Instead, these needs to be associated with the Products property on your binding model. That can be done by prefixing the name with Products. This will be done automatically if you use asp-for to bind to a Products property on your view model—as proposed on the ProductListViewModel above. But since you need to dynamically generate the name's based on the IndexOffset anyway, you can just hard-code these as part of your template:
<input id="Products_#(Model.StartingIndex+i)__Quantity" name="Products[#(Model.StartingIndex+i)].Quantity" value="#Model.Products[i].Quantity" />
Selected Products
There's still a problem, though! This is going to include all of your products—even if they weren't otherwise selected by the user. There are a number of strategies for dealing with this, from dynamically filtering them on the client, to creating a custom model binder that first validates the Products_[i]__isSelected attribute. The easiest, though, is to simply allow all of them to be bound to your binding model, and then filter them prior to any processing using e.g. LINQ:
var selectedProducts = c.Products.Where(p => p.IsSelected).ToList();
…
repo.SetProducts(selectedProducts);
For the 1st question, you can try different things. When you do the ajax call, you get a list of models. For each of these models, add the selected company ID as a property. So you don't have to worry about the index being something unique.
As for the 2nd question, should be a relatively easy thing to do. However, more information is needed.
1. When the save button is hit, are you doing a full postback? or it is also an AJAX call?
2. Why do you not want to opt for a AJAX call to do the update as well? So you can based upon the response, redirect the user to a results page, etc.
If you can create a small sample in a new project, and upload to github, and post the information here. I should be able to take a look and understand better. I will definitely be able to help.
Also try reading this thread, it might help
how to persist partial view model data during postback in asp.net mvc

MVC Form Post with multiple parameters

So I have an action method like this:
public ViewResult CareerSearch()
{
CareerSearchModel model = GetCareerSearchModel();
return View("Search", model);
}
In the view, I manually create a list of checkboxes from one of the model properties. The output of that ends up looking something like this:
<input id="location51438342" type="checkbox" checked="True" value="2" name="locations">
<label for="location51438342">Austin</label>
<input id="location14609737" type="checkbox" checked="True" value="9" name="locations">
<label for="location14609737">Dallas</label>
<input id="location25198218" type="checkbox" checked="True" value="11" name="locations">
<label for="location25198218">Houston</label>
So, on the action method that handles the form POST, I want to get a reference to the model AND the checkbox integer array. However, when I step through the following action method, "model" is null:
[HttpPost]
public ViewResult CareerSearch(CareerSearchModel model, int[] locations)
{
//omitted for brevity
}
What am I missing here? How do I get the reference to my model AND the array of checkbox values?
you'd have to be posting back all the fields of the model itself too aka have them in hiddenfor labels inside of the form. you can use firebug to see what is posted back. if it's not being posted back you'd have to recreate it in your action after the post.
The model is not saved anywhere on the server and is not automatically posted back in the POST data (use Fiddler to confirm). You must instantiate the model the same way you do in the GET action and use the other parameters to modify it accordingly.
See NerdDinner on how to map POST form data into your model.

How do I save multiple Models edited in form in ASP.NET MVC?

I need to make a view which will support bulk inserting of objects. I am using the repository pattern with Entity Framework 4.
Since there will be multiple individual models in the View, how do I handle binding the view's model to what get's sent to the Repository?
How do I retreive the input values from the form into the controller?
I have managed to do the view but I am unable to retrieve the data from controller. I used the folowing statement but after the post it heppens to be nul
public ActionResult AddPaymentForRoot_post(PaymentViewModelObject payments) {
return null;
}
I haven't used EF yet, but I have a similar requirement on our site. I accomplished it by specifying the View as inheriting System.Web.Mvc.ViewPage<IEnumerable<MyModelObject>> then use a for loop to build multiple input sections, utilizing the naming conventions required to allow the model binder to build the enumerable.
I prefer to create a view model with a property that is an enumerable (see update 2 below). In this example we could say it would be the MyViewModel object with an IEnumerable<MyModelObject> property called MyModelObjects. In this case, the view would play out as follows...
Specifically, make sure to include for each input section a hidden field named MyModelObjects.Index, then have your property inputs named MyModelObjects[0].MyProperty, where the value of the Index hidden field corresponds to the array index in the property names.
What you would end up with would look like this, for example:
<div>
<input type="hidden" name="MyModelObjects.Index" value="0" />
Name: <input type="text" name="MyModelObjects[0].Name" /><br />
Value: <input type="text" name="MyModelObjects[0].Value" />
</div>
<div>
<input type="hidden" name="MyModelObjects.Index" value="1" />
Name: <input type="text" name="MyModelObjects[1].Name" /><br />
Value: <input type="text" name="MyModelObjects[1].Value" />
</div>
<div>Add another item</div>
In some cases I also have javascript (triggered by the anchor link in the markup above) that will clone a template input section, modify the names to populate the index value, and add it to the DOM. In this way users can dynamically add records to the view.
Depending on how you are handling your models, you may also pass in a ViewModel that has a property that is IEnumerable. The typing for the view would change, but fundamentally the design of the view wouldn't.
UPDATE
Using the example above, here's a quick snippet of the jQuery code I use to add items. I'm assuming this is an insert, but note the comments if you wanted to support this type of interface for editing as well.
First, I set up a template. While this could be built totally in jQuery, I just find it easier to do the markup and manipulate the names.
<div id="templateDiv" style="display: none;">
<input type="hidden" name="MyModelObjects.Index" value="0T" />
Name: <input type="text" name="MyModelObjects[0T].Name" /><br />
Value: <input type="text" name="MyModelObjects[0T].Value" />
</div>
You can also specify IDs in there...probably a good practice, but in the interest of brevity I'll leave it out for the example.
With that, you can set up the jQuery function as follows:
var currentIndex = 1 // if you were using this for editing, this would be dynamically set to the number of items you have
var doAddItem =
function() {
currentIndex++;
var newItem = $("#templateDiv").clone();
newItem.attr("id", "item" + currentIndex); // reset the ID here
newItem.find("input[name=MyModelObjects.Index]").val(currentIndex);
newItem.find("input[name$=.Name]").attr("name", "MyModelObjects[" + currentIndex + "].Name");
newItem.find("input[name$=.Value]").attr("name", "MyModelObjects[" + currentIndex + "].Value");
newItem.insertBefore($(this).closest("div")); // insert it before the div containing the add link...adjust appropriate to your layout
return false;
}
$(document).ready(function() {
$("#addItem").click(doAddItem);
// In a real-world app you'd probably want to have a delete option, too
});
UPDATE 2
There's a bit of an error up there - if your view is typed to an IEnumerable<object> then your field names will end up just being Index and [0].Name / [0].Value. In my actual app I use a view model that has a property that is itself an enumerable, so I actually type my pages as <MyViewModelObject>. In the example above, that view model object would have a property called MyModelObject, which would be an enumerable (and more realistically called MyModelObjects plural).

MVC 2 View showing wrong model info

I'm using MVC 2 for a project and I'm having a problem with a view. In the controller I have the code:
return View(calendarDay);
If I debug this line and inspect calendarDay it tells me the calendarDay.Id property is equal to 2.
In the view I have some code like this:
<%: Html.HiddenFor(model => model.Id) %>
However, when the view is shown after binding it to a calendarDay with Id property = 2 I get this on the generated HTML:
<input id="Id" name="Id" type="hidden" value="1">
The value is 1, so when I do the TryUpdateModel(calendarDay) it gets the Id property to 1 instead of 2 and when I go to the repository to get the object to delete it, it crashes because it finds the wrong one.
Anyone knows what I might be doing wrong?
I suspect that you are trying to modify the POSTed value (which is 1) in your controller action to 2. This is not possible because that's how all HTML helpers work and it is by design: they will first look at the POSTed value when binding and after that in the model. So the HiddenFor helper ignores the Id of your model and uses the one that's posted.
As a workaround you could:
<input type="hidden" name="Id" value="<%: Model.Id %>" />
As suggested by #jfar in the comments section another workaround is to clear the model state in the post action before returning the view:
MoselState.Clear();
Seems like the problem is that the view is using the id from the controller and not the one from the model. I just changed the parameter name and works fine now.

ASP.NET MVC: Hidden field value does not get rendered using HtmlHelper.Hidden

Something pretty weird is happening with my app:
I have the following property in my ViewModel:
public int? StakeholderId { get; set; }
It gets rendered in a partial view as follows:
<%= Html.Hidden("StakeholderId", Model.StakeholderId) %>
The form is submitted, and the relevant controller action generates an id and updates the model, before returning the same view with the updated model
The problem I'm experiencing is that the hidden field does not have anything in its "value" attribute rendered the second time even though StakeholderId now has a value.
If I just output the value on its own, it shows up on the page, so I've got it to render the value by doing this:
<input type="hidden" id="StakeholderId" name="stakeholderId" value="<%: Model.StakeholderId %>" />
But it's pretty strange that the helper doesn't pick up the updated value?
(I'm using jQuery to submit forms and render the action results into divs, but I've checked and the html I get back is already wrong before jQuery does anything with it, so I don't think that has much to do with anything)
UPDATE
I've since discovered that I can also clear the relevant ModelState key before my controller action returns the partial view.
The helper will first look for POSTed values and use them. As you are posting the form it will pick up the old value of the ID. Your workaround is correct.
ADDENDUM: Multiple HTML Forms, eg, in a Grid
As an addendeum to this issue, one thing to be VERY careful of is with multiple forms on the same page, eg, in a grid, say one generated using Ajax.BeginForm.
You might be tempted to write something along the lines of:
#foreach (var username in Model.TutorUserNames)
{
<tr>
<td>
#Html.ActionLink(username, MVC.Admin.TutorEditor.Details(username))
</td>
<td>
#using (Ajax.BeginForm("DeleteTutor", "Members",
new AjaxOptions
{
UpdateTargetId = "AdminBlock",
OnBegin = "isValidPleaseWait",
LoadingElementId = "PleaseWait"
},
new { name = "DeleteTutorForm", id = "DeleteTutorForm" }))
{
<input type="submit" value="Delete" />
#Html.Hidden("TutorName", username)
}
</td>
</tr>
}
The lethal line in here is:
#Html.Hidden("TutorName", username)
... and intend to use TutorName as your action's parameter. EG:
public virtual ActionResult DeleteTutor(string TutorName){...}
If you do this, the nasty surprise you are in for is that Html.Hidden("TutorName", username) will, as Darin Dimitrov explains, render the last POSTed value. Ie, regardless of your loop, ALL the items will be rendered with the TutorName of the last deleted Tutor!
The word around, in Razor syntax is to replace the #Html.Hidden call with an explicit input tag:
<input type="hidden" id="TutorName" name="TutorName" value='#username' />
This works as expected.
Ie:
NEVER, EVER USE Html.Hidden TO PASS A PARAMETER BACK TO YOUR ACTIONS WHEN YOU ARE USING MULTIPLE FORMS IN A GRID!!!
Final Caveat:
When constructing your hidden input tag, you need to include both name and id, set to the same value, otherwise, at the time of writing (Feb 2011) it won't work properly. Certainly not in Google Chrome. All you get is a null parameter returned if you only have an id and no name attribute.

Categories