My model has a list property and in the view i need to be able to add an unlimited number of strings to it.
So far it's not working and my lousy idea to make it work is the following: Each time a string is added, there's a postback. The new string is in the ViewModel's "newString" property (not a list). The HttpPost method will then save "newString" to the database, refill the list "allStrings" with all strings stored in the database and return the view with all strings and an emtpy textbox to add another string.
This is not a good solution for me because:
There's a lot of postbacks if the user wants to add multiple strings
If the user adds some strings to his item (a supplier), all these strings are saved to the database. When he then decides he doesn't want to save the supplier all the stored strings are useless and need to be deleted from the database.
I have not implemented this because I know there's far better solutions and I just don't find them. This is what I have:
The ViewModel:
public class SupplierViewModel
{
public Supplier Supplier { get; set; }
public List<string> allStrings;
public string newString { get; set; }
}
The Controller:
[HttpPost]
public ActionResult Create(SupplierViewModel model)
{
model.allStrings.Add(model.newString);
if (ModelState.IsValid && model.newString == "")
db.Suppliers.Add(model.Supplier);
db.SaveChanges();
return RedirectToAction("Index");
}
model.newString = "";
return View(model);
}
The View:
<div class="editor-label">
#Html.LabelFor(model => model.allStrings)
</div>
#for (int i = 0; i < Model.allStrings.Count; i++)
{
<div class="editor-label">
#Html.EditorFor(model => model.allStrings[i])
</div>
}
<div class="editor-label">
#Html.EditorFor(model => model.newString)
</div>
Note that in this implemented version, none of the strings are saved to the database and the list is cleared after each postback. Only one string (the last one added) is displayed on the view.
Basically the question is: How can I have the user add as many strings as he wants with as few postbacks and database-interaction as possible?
Thanks in advance
You can dynamically add new elements with jquery that will post back to your collection. The html your generating for the textboxes will be similar to
<input type="text" name="allStrings[0]" .../>
<input type="text" name="allStrings[1]" .../>
The name attribute includes an indexer which allows the DefaultModelBinder to bind a collection.
Wrap you textboxes in a container, include a button to add a new item, an input that gets copies and added to the DOM.
<div id="strings">
#for (int i = 0; i < Model.allStrings.Count; i++)
{
<div class="editor-label">
#Html.TextBoxFor(m => m.allStrings[i])
</div>
}
</div>
<div id="newstring" style="display:none;">
<input type="text" name="allStrings[#]" />
</div>
<button type="button" id="addstring">Add</button>
Script
var container = $('#strings');
$('#addstring').click(function() {
var index = container.children('input').length;
var clone = $('#newstring').clone();
clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
container .append(clone.html());
});
Refer this fiddle for a working example
Note your model no longer required the public string newString { get; set; } property, and when you post back your collection will contain all the values of the textboxes.
Related
This question already has answers here:
How does MVC 4 List Model Binding work?
(5 answers)
Closed 7 years ago.
I have a list of items that will be associated to a user. It's a one-to-many relationship. I want the entire list of items passed into the view so that they can choose from ones that are not associated to them yet (and also see those that are already associated). I want to create checkboxes from these. I then want to send the selected ones back into the controller to be associated. How can I pass in the list of all of them, including those that aren't yet associated, and reliably pass them back in to be associated?
Here's what I tried first, but it's clear this won't work as I'm basing the inputs off the items passed in via the AllItems collection, which has no connection to the Items on the user itself.
<div id="item-list">
#foreach (var item in Model.AllItems)
{
<div class="ui field">
<div class="ui toggle checkbox">
<input type="checkbox" id="item-#item.ItemID" name="Items" value="#item.Active" />
<label for="item-#item.ItemID">#item.ItemName</label>
</div>
</div>
}
</div>
You cannot bind to a collection using a foreach loop. Nor should you be manually generating your html, which in this case would not work because unchecked checkboxes do not post back. Always use the strongly typed html helpers so you get correct 2-way model binding.
You have not indicated what you models are, but assuming you have a User and want to select Roles for that user, then create view models to represent what you want to display in the view
public class RoleVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
public class UserVM
{
public UserVM()
{
Roles = new List<RoleVM>();
}
public int ID { get; set; }
public string Name { get; set; }
public List<RoleVM> Roles { get; set; }
}
In the GET method
public ActionResult Edit(int ID)
{
UserVM model = new UserVM();
// Get you User based on the ID and map properties to the view model
// including populating the Roles and setting their IsSelect property
// based on existing roles
return View(model);
}
View
#model UserVM
#using(Html.BeginForm())
{
#Html.HiddenFor(m => m.ID)
#Html.DisplayFor(m => m.Name)
for(int i = 0; i < Model.Roles.Count; i++)
{
#Html.HiddenFor(m => m.Roles[i].ID)
#Html.CheckBoxFor(m => m.Roles[i].IsSelected)
#Html.LabelFor(m => m.Roles[i].IsSelected, Model.Roles[i].Name)
}
<input type"submit" />
}
Then in the post method, your model will be bound and you can check which roles have been selected
[HttpPost]
public ActionResult Edit(UserVM model)
{
// Loop through model.Roles and check the IsSelected property
}
It doesn't look like you're going to be deleting the checkboxes dynamically so that makes this problem a lot easier to solve. NOTE: The following solution won't work as expected if you allow clients or scripts to dynamically remove the checkboxes from the page because the indexes will no longer be sequential.
MVC model binding isn't foolproof so sometimes you have to help it along. The model binder knows it needs to bind to a property called Items because the input field's name is Items, but it doesn't know Items is a list. So assuming in your controller you have a list of items to model bind to called Items what you need to do is help MVC recognize that you're binding to a list. To do this specify the name of the list and an index.
<div id="item-list">
#for (var i = 0; i < Model.AllItems.Count; i++)
{
<div class="ui field">
<div class="ui toggle checkbox">
<input type="checkbox" id="item-#Model.AllItems[i].ItemID" name="Items[#i]" value="#Model.AllItems[i].Active" />
<label for="item-#Model.AllItems[i].ItemID">#Model.AllItems[i].ItemName</label>
</div>
</div>
}
</div>
The key line here is this:
<input type="checkbox" id="item-#Model.AllItems[i].ItemID" name="Items[#i]" value="#Model.AllItems[i].Active" />
Notice the Items[#i]? That's telling the model binder to look for a property named Items and bind this value to the index at i for Items.
I'm working in asp.net core inside a MVC application. I'm using the scaffolding feature that creates the views and controller based on a model. Below is the model that i'm using:
class ShoppingList
{
public int ShoppingListId { get; set; }
public string Name { get; set; }
public List<string> ListItems { get; set; }
}
The form that displays to the user via the view only displays the field for Name. I would like the form to be able to show a field for a list item, and then if the user wants to add another list item they can hit a button to add another field to do so. They at run time decide how many shopping list items they want to add.
Here is the razor cshtml form i'm using:
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
Is there an easy way to do this? I don't want to have to hard code a number.
If you want to allow the user to add a new form element on the client side you need to use javascript to update the DOM with the new element you want to add. To list the existing items you may use editor templates. Mixing these 2 will give you a dynamic form. The below is a basic implementation.
To use editor templates, we need to create an editor template for the property type. I would not do that for string type which is more like a generic one. I would create a custom class to represent the list item.
public class Item
{
public string Name { set; get; }
}
public class ShoppingList
{
public int ShoppingListId { get; set; }
public string Name { get; set; }
public List<Item> ListItems { get; set; }
public ShoppingList()
{
this.ListItems=new List<Item>();
}
}
Now, Create a directory called EditorTemplates under ~/Views/YourControllerName or ~/Views/Shared/ and create a view called Item.cshtml which will have the below code
#model YourNameSpaceHere.Item
<input type="text" asp-for="Name" class="items" />
Now in your GET controller, create an object of the ShoppingList and send to the view.
public IActionResult ShoppingList()
{
var vm = new ShoppingList() { };
return View(vm);
}
Now in the main view, All you have to do is call the EditorFor method
#model YourNamespace.ShoppingList
<form asp-action="ShoppingList" method="post">
<input asp-for="Name" class="form-control" />
<div class="form-group" id="item-list">
Add
#Html.EditorFor(f => f.ListItems)
</div>
<input type="submit" value="Create" class="btn btn-default" />
</form>
The markup has an anchor tag for adding new items. So when user clicks on it, we need to add a new input element with the name attribute value in the format ListItems[indexValue].Name
$(function () {
$("#add").click(function (e) {
e.preventDefault();
var i = $(".items").length;
var n = '<input type="text" class="items" name="ListItems[' + i + '].Name" />';
$("#item-list").append(n);
});
});
So when user clicks it adds a new input element with the correct name to the DOM and when you click the submit button model binding will work fine as we have the correct name attribute value for the inputs.
[HttpPost]
public IActionResult ShoppingList(ShoppingList model)
{
//check model.ListItems
// to do : return something
}
If you want to preload some existing items (for edit screen etc), All you have to do is load the ListItems property and the editor template will take care of rendering the input elements for each item with correct name attribute value.
public IActionResult ShoppingList()
{
var vm = new ShoppingList();
vm.ListItems = new List<Item>() { new Item { Name = "apple" } }
return View(vm);
}
First this is you must have a public accessor to your ShoppingList class.
So, public class ShoppingList.
Next is your view will need the following changes.
#model ShoppingList
<h1>#Model.Name</h1>
<h2>#Model.ShoppingListId</h2>
foreach(var item in Model.ListItems)
{
<h3>#item</h3>
}
So, the above code is roughly what you are looking for.
In Razor you can accessor the models variables by using the #model at the top of the view. But one thing you need to note is if your model is in a subfolder you'll need to dot into that.
Here's an example: #model BethanysPieShop.Models.ShoppingCart.
Here BethanysPieShop is my project name, Models is my folder the ShoppingCart class is in.
So I have a form that I am trying to submit and I can get either the list or the model to bind, but not both at the same time. I suspect it has to do with the model binder.
HTML
#using (Html.BeginForm("Index", "Home", FormMethod.Post)){
#Html.AntiForgeryToken()
<div class="TransferHeader">
<div>
#Html.LabelFor(model => model.tranRequestedBy)
#Html.EditorFor(model => model.tranRequestedBy, new { #Name = "h.tranRequestedBy" })
</div>
<div>
#Html.LabelFor(model => model.tranNotes)
#Html.EditorFor(model => model.tranNotes, new { #Name = "h.tranNotes" })
</div>
<input name="h.TransfersDetail.Index" id="detIndex" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="h.TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" id="detToolCode" type="hidden" value="1234">
</div>
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(TransfersHeader h)
{
return View();
}
Model Class:
public virtual ICollection<TransfersDetail> TransfersDetail { get; set; }
public string tranRequestedBy { get; set; }
public string tranNotes { get; set; }
The two bottom inputs were generated from an AJAX call to an add method, what happens is if they are not present the two HTML helper editors will come in the model, but if they do exist only the transfer detail list will appear.
Is there anything I could do to make sure all of the data comes into the model?
Its not clear how you are generating those inputs, but the name attributes are incorrect. You model does not contain a collection property named h, but it does contain one named TransfersDetail, so your inputs need to be
<input name="TransfersDetail.Index" type="hidden" value="c3a3f7dd-41bb-4b95-b2a6-ab5125868adb">
<input name="TransfersDetail[c3a3f7dd-41bb-4b95-b2a6-ab5125868adb].detToolCode" type="hidden" value="1234">
Its also not clear why your adding an id attribute (if you referencing collection items in jQuery, you would be better off using class names and relative selectors), but the id your using does not have an indexer suggesting that your going to be generating duplicate id attributes which is invalid html (and jQuery selectors would not work in any case)
There are razor checkbox controls in application which needs to be repeated for each of the collection. But for the collection number second onwards below code passes nothing as value for checkbox:
<div class="checkbox">
<label class="checkbox-inline">
#Html.CheckBoxFor(m => m.Collection[i].Item)Some Label
</label>
</div>
The viewmodel is:
public class Items{
public List<Collection> Collection{get; set;}
}
public class Collection{
public bool Item { get; set; }
}
Assuming your HttpPost action's parameter is an object of
[HttpPost]
public ActionResult Create(Items model)
{
//to do : Save and Redirect
}
You need to make sure that the checkboxes in your form will have the name matching to your ViewModel property hierarchy. So , for model binding to work, you need to have your checkboxes with names like this
<input name="Collection[1].Item" type="checkbox" >
So in your view, Make sure you manipulate the name like that
#model Items
#using (Html.BeginForm())
{
for (int index = 0; index < Model.Collection.Count; index++)
{
var collection = Model.Collection[index];
#Html.CheckBox("Collection["+index+"].Item",collection.Item)
}
<input type="submit"/>
}
Another (better) option is to use Editor Templates. With this approach, you do not need to manipulate the form field name. Here is a complete post which explains the step by step
Instead of Item write selected
<div class="checkbox">
<label class="checkbox-inline">
#Html.CheckBoxFor(m => m.Collection[i].selected)Some Label
</label>
</div>
Or try this
#Html.CheckBoxFor(m => m[i].Checked,new {Style ="vertical-align})
maybe this question is seen repeatedly around here but i was not able to find a answers.
my project is about reservations for hotels. I have a class Reservation witch has a Icollection of ChoosenRooms and a class that represents de User making the reservation, and other stuff like dates and other stuff.
The process is this:
In my first view I get the chosen rooms, dates, etc, then i pass that to my second view where i´m going to get the user info, and then i have a third view where i want to show all the gathered information so the user can finally click a button to save the data.
My problem is that i need to pass the reservation object class across all these views. In my testing i see that primitive types pass just fine BUT The iColletion of ChoosenRooms is lost when i post back from the view to the next controller action.
can someone post some example how to, Posting back from a view to a controller, complex types like ChoosenRooms inside another class Reservations, are not lost in the process?
Or maybe explain why this info is lost?
the code:
public class Reserva
{
public virtual int ID { get; set; }
[NotMapped]
public string[] q { get; set; }
[Display(Name = "Cliente")]
public virtual Utilizador utilizador { get; set; }
[Display(Name = "Quarto")]
public virtual ICollection<Quartos> ChoosenRooms{ get; set; }
[Display(Name = "Serviços Adicionais")]
public virtual ICollection<ReservasItens> itens { get; set; }
The view
#model SolarDeOura.Models.Reserva
#{
ViewBag.Title = "AddReservaUser";
var _reserva = TempData["reserva"] as Reserva;
}
<h2>AddReservaUser</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Reserva</legend>
<div class="editor-label">
#Html.LabelFor(model => model.dtEntrada)
</div>
<div class="editor-field">
#Html.DisplayFor(model => model.dtEntrada)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.dtSaida)
</div>
<div class="editor-field">
#Html.DisplayFor(model => model.dtSaida)
</div>
<div class="editor-label">
#Model.q.Count() Choosen Rooms
</div>
#foreach (var q in Model.ChoosenRooms)
{
<ul>
<li>
#Html.DisplayFor(modelitem => q.descricao)
</li>
</ul>
}
posting back from here is the Problem. In this view " #foreach (var q in Model.ChoosenRooms)" has data but posting back the data is lost.
The concept of model binder at this point was not very clear to me and some knowledge about this topic is all it takes to solve the question.
In resume:
The view gets a model which is a complex type: class [reserva] has a collection of [ChoosenRooms] which is also a complex type.
The line #Html.DisplayFor(modelitem => q.descricao) renders the necessary html elements to display the data, but not the necessary html to be posted back to the controller (input element or hidden field ) so the model binder fails.
Also the controller (post) action argument didn't had the property name that matches the field, in this case it needed to be a String[] type since its a collection of values.
I would also recommend reading about Display Templates and Editor Templates.