Modify List in View and send back to Controller - c#

I have the following in my controller:
ViewBag.UserList = userlist.ToList();
return View ();
The userlist have the following 3 fields:
First Name, Last Name, Phone Number
How do I go about creating a list in the view where the user can modify the fields?
I am well aware of using the foreach to loop through the list but how do I program the ability to modify the records?
I also need to know how I can then send this list back to the controller so that I can insert the records into my database.

As you said, iterate the list in the view, Use the TextboxFor() method of each property you want to modify, add a submit button, in the controller action add List<T> list to catch the changes and there you go.

I'll assume this is a web mvc project since it's tagged asp.net-mvc.
the view will render html. the html will contain (at a minimum) a form, inputs for each editable value and a submit button. ultimately the markup will look like this
<form id="?" method="post" action="usercontroller/update">
<input name="users.firstname[0]" value="john"/>
<input name="users.lastname[0]" value="doe"/>
<input name="users.phonenumber[0]" value="555-123-4567"/>
<input name="users.firstname[1]" value="jane"/>
<input name="users.lastname[1]" value="smith"/>
<input name="users.phonenumber[1]" value="555-123-4567"/>
<submit value="update"/>
</form>
on the server you would have a controller
class UserController : BaseClassIfNecessary
{
public ActionResult() Update(User[] users)
{
foreach user update database
return RedirectAction("url");
}
}

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

Dynamically Add Fields to Razor Form

I have a Razor form with a list/table of items that I'd like to dynamically add items to. You can select the items from a dropdown, click "Add", and the item from the dropdown will be added to the list. I'd then like all of that to be sent via POST when I submit my form and my controller's HttpPost method can handle the input.
Is there a way to dynamically add fields and still be able to accept them as arguments in the HttpPost function?
The first answer is correct in that you can iterate over a form collection to get the values of the dynamically inserted fields within your form element. I just wanted to add that you can utilize some of the neat binding.
The code below accepts a dynamic list of textboxes that were posted against the action. Each text box in this example had the same name as dynamicField. MVC nicely binds these into an array of strings.
Full .NET Fiddle: https://dotnetfiddle.net/5ckOGu
Sample code (snippets for clarity) dynamically adding sample fields
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div id="fields"></div>
<button>Submit</button>
}
<div style="color:blue"><b>Data:</b> #ViewBag.Data</div>
<script type="text/javascript">
$(document).ready(function() {
var $fields = $('#fields');
$('#btnAddField').click(function(e) {
e.preventDefault();
$('<input type="text" name="dynamicField" /><br/>').appendTo($fields);
});
});
</script>
Sample code from the action accepting the dynamic fields in a post.
[HttpPost]
public ActionResult Index(string[] dynamicField)
{
ViewBag.Data = string.Join(",", dynamicField ?? new string[] {});
return View();
}
Screenshot of output
Every combobox/hiddenfield/textbox/... that is included inside the <form> element gets posted on submit. Doesn't really matter if you create them on-fly or have them ready by default. The biggest difference however is that with those created dynamically you can't really utilize that neat binding we're used to. You'll have to perform validation etc. manually as well.
Then you'll have a method like this:
public ActionResult HandleMyPost(FormCollection form)
{
// enumerate through the FormCollection, perform validation etc.
}
FormCollection on MSDN

Adding my own name tag to a form component in a partial view - MVC 5.0

I need to be able to show a dropdown list for some items in various different locations... I can safely assume that considering it is a dropdown, it will always be used in context with a form and thus a name tag will always be needed. However I wanted to know what's the best practice for this type of problem.
I have the following in a partial view and you can see I have explicitly set the name="Name" which at this moment in time is correctly going to map to the items Class property:
#model IEnumerable<TEST.Domain.Entities.James>
#{
Layout = "";
}
<select name="Name" id="JamesDropdownList">
#foreach (var item in Model)
{
<option value="#item.JamesID">#item.Name</option>
}
</select>
I may then use this partial view in such a context:
#using (Html.BeginForm("LoadInformation", "James"))
{
<div class="col-lg-9 remove-padding-left">
#{ Html.RenderAction("DropdownList", "James"); } // This is the partial view being used in a form
</div>
<div class="col-lg-3 remove-padding-right">
#Html.ContinueButton("Continue") // This is a custom button html helper I have created
</div>
}
So the question is, is this absolutely okay, to declare specifically that the name for this select field is "Name" or is there some special clever razorey way of doing this?
EDIT
In fact, don't I have to do #Html.Hidden("JamesID", item.JamesID) or something in the select? So that when I submit that form, it pushed the JamesID to the controller which looks like this:
[HttpPost]
public ViewResult LoadInformation(int jamesID)
{
// Do something with jamesID
return View("LoadInformation");
}
You can see i'm a bit confused...
Maybe a better question is, how do I have a re-usable dropdown list that I can use in a form that requires the JamesID from that dropdown list as one of the receiving controller's parameters?
One razory way would be to do this:
#Html.DropDownList("JamesDropdownList", new SelectList(Model,"JamesID","Name"))
This will assign JamesDropdownList to both the name and id attribute of the dropdown.

MVC: what code gets called when you click the "submit" button?

MVC newbie question; I'm learning by playing around rather than Reading The Manual... :)
I see when I create an "Edit" view that the auto-generated view includes a "submit" button:
<input type="submit" value="Save" />
But what code gets called behind the scenes to do this save? Specifically, the model underlying this view has its own fancy save logic in code that I would want to call. How do I get the view to invoke my code instead of whatever standard code is being called invisibly behind the scenes?
It's not the button that defines what happens, but the form itself. The button of type submit (one per form) just triggers the form submission, which is handled by the form itself.
A form has an action - e.g.:
<form name="input" action="users/save" method="post">
<!-- Form content goes here -->
<input type="submit" value="Submit" />
</form>
The action is an URL and what happens is that the browser collects the values of all the fields in the form (<input...>) and posts them to the specified url.
In ASP.NET MVC forms are usually defined using the Html helpers, so that building the URL for the form action is delegated to ASP.NET MVC. For the above for example:
<% using(Html.BeginForm("Save", "Users")) %>
<% { %>
<!-- Form content goes here -->
<input type="submit" value="Save" />
<% } %>
Which in this case will create a url /users/save and the form will post to that url. That in terms will trigger the ASP.NET routing which will handle the /users/save url and break it into chunks so that it knows that it has to invoke the "Save" action method on the "Users" controller class. It will then read all the incoming field name-value pairs and try to map them to the method parameter names if any.
It would call whatever public action method the form action is pointing to on your controller. You can then call save on the view model.
public virtual ActionResult Save(MyViewModel model) {
model.Save();
--- more code to do stuff here
}
Set your form action to MyController/Save
You can also use using (Html.BeginForm... in your code to point the form to a specific action method on a specific controller.
when you click submit button, request goes to the HTTp Module which directs it to corresponding controller action. when edit view is created from template the post address of the form is same as of the edit form i.e if you are visiting /home/edit you can see following html in form's opening tag
<form method="post" action="/home/edit">
you can have another action method that only accepts post requests like
[HttpPost]
public ActionResult Edit(int id, ViewModel model)
{
//put your logic here handling submitted values
}
HttpPost attribute tells that it will only handle post request as opposed to get requested used to render the form
it calls the Action method defined in the action part of the form element
eg:
<form action="/Account/LogOn" id="loginForm" method="post">
The LogOn action in the Account controller will be invoked in this form
The ViewPage has a BeginForm Method using (Html.BeginForm() at the top which would render the FormTag. This method has a overload which takes ActionName and controller Name. So you can specify the action in your controller which has to be called.

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).

Categories