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)
Related
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.
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})
I'm not so experienced using MVC. I'm dealing with this situation. Everything works well until call the HttpPost method where has all its members null. I don't know why is not persisting all the data on it.
And everything works well, because I can see the data in my Html page, only when the user submit the information is when happens this.
[HttpGet]
public ActionResult DoTest()
{
Worksheet w = new Worksheet(..);
return View(w);
}
[HttpPost]
public ActionResult DoTest(Worksheet worksheet)
{
return PartialView("_Problems", worksheet);
}
This is class which I'm using.
public class Worksheet
{
public Worksheet() { }
public Worksheet(string title, List<Problem> problems)
{
this.Title = title;
this.Problems = problems;
}
public Worksheet(IEnumerable<Problem> problems, WorksheetMetadata metadata, ProblemRepositoryHistory history)
{
this.Metadata = metadata;
this.Problems = problems.ToList();
this.History = history;
}
public string Title { get; set; }
public List<Problem> Problems { get; set; } // Problem is an abstract class
public WorksheetMetadata Metadata { get; set; }
public ProblemRepositoryHistory History { get; set; }
}
And my razor view.... the razor view shows successfully my view. I realized something rare, please note in my 5 and 6 lines that I have HiddenFor method, well if I used that, when calls HTTPPOST persists the data, I don't know why.
#model Contoso.ExercisesLibrary.Core.Worksheet
<div id="problemList">
<h2>#Html.DisplayFor(model => model.Metadata.ExerciseName)</h2>
#Html.HiddenFor(model => model.Metadata.ExerciseName)
#Html.HiddenFor(model => model.Metadata.ObjectiveFullName)
#for (int i = 0; i < Model.Problems.Count; i++)
{
<div>
#Html.Partial(Contoso.ExercisesLibrary.ExerciseMap.GetProblemView(Model.Problems[i]), Model.Problems[i])
</div>
}
</div>
UPDATE
I'm using a static class to get the view name, but as I'm testing I'm just using this Partial view
#model Contoso.ExercisesLibrary.AbsoluteArithmetic.Problem1
<div>
<span style="padding:3px; font-size:18px;">#Model.Number1</span>
<span style="padding:5px; font-size:18px;">+</span>
<span style="padding:5px; font-size:18px;">#Model.Number2</span>
<span style="padding:5px; font-size:18px;">=</span>
<span style="font-size:18px">
#Html.EditorFor(model => model.Result, new { style = "width:60px; font-size:18px;" })
#Html.ValidationMessageFor(model => model.Result)
</span>
</div>
#section Scripts {
}
And here the user do the post
#model Contoso.ExercisesLibrary.Core.Worksheet
<form method="post">
#Html.Partial("_Problems", Model)
<input type="submit" value="Continue" />
</form>
The Model Binder will 'bind' or link input fields on your view to the model. It will not bind display fields (like label), that is why you need the HiddenFor it will add an <input type="hidden" which will then be bound to the Model when you Post.
You can use 'TempData'. It is used to pass data from current request to subsequent request means incase of redirection.
This link also helps you.
TempData
SO Tempdata
Make sure your form tag looks like the following, for instance the controller name, action method, the form method and an id for the form. I am referring to the #using statement. In my case the controller name is RunLogEntry, the action method is Create and the id is form.
Normal Post from View to Controller
#using (Html.BeginForm("Create", "RunLogEntry", FormMethod.Post, new { id = "form", enctype = "multipart/form-data" }))
{
<div id="main">
#Html.Partial("_RunLogEntryPartialView", Model)
</div>
}
If you want to post via Jquery, could do the following:
$.post("/RunLogEntry/LogFileConfirmation",
$("#form").serialize(),
function (data) {
//this is the success event
//do anything here you like
}, "html");
You must specify a form with correct attribute in your view to perform post action
<form action="Test/DoTest" method="post">
...
</form>
or
#using(Html.BeginForm("DoTest", "Test", FormMethod.Post)) {
...
}
The second is recommended.
Put your entire HTML code under:
#using(Html.BeginForm())
tag.
I have a data-first set-up so my models are generated by the entity framework from my database and there is no default [Required] annotations. I have a simple table with three fields. One ID and two VARCHAR / text based fields.
No matter what I try, I cannot get the CRUD forms to stop validation. I disabled in the Web.config, I add [ValidateInput(false)] to the Create() method in the controller, but has no effect. I set the #Html.ValidationSummary to false,
This is the basic view:
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
<fieldset>
<legend>CallType</legend>
<div class="editor-label">
#Html.LabelFor(model => model.CALLTYPE)
</div>
<div class="editor-field">
#Html.TextBox("calltype", "", new { style = "width: 50px;" })
#Html.ValidationMessageFor(model => model.CALLTYPE)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DESCRIPTION)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.DESCRIPTION)
#Html.ValidationMessageFor(model => model.DESCRIPTION)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Model (generated by Framework):
public partial class CALLTYPES2
{
public int ID { get; set; }
public string CALLTYPE { get; set; }
public string DESCRIPTION { get; set; }
}
Even if I insert just one character in each field, it still says: "The Value 'x' is invalid"
(I leave the validation messages on so I can see what is going on.)
What am I supposed to do? And how would I validate these fields later on - can I just add [Required] to Model generated code? What if I regenerate the Model from the database?
Does this have something to do with the model state in the controller?
[HttpPost]
public ActionResult Create(CALLTYPES2 calltype)
{
if (ModelState.IsValid)
{
db.CALLTYPES2.Add(calltype);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(calltype);
}
Not sure what I am missing and the tutorials I have read do not shed much light. Thanks for your response and apologies for my ignorance.
UPDATE
Found my error - The object name "calltype" in the Method Create() is the same as the name/id of the form field "calltype". I guess the binder tries to bind the string "calltype" to the object "calltype". Renamed it to:
public ActionResult Create(CALLTYPES2 ctype)
Now it works in both the Edit and Create Windows. "ctype" is not clashing with "calltype".
You forgot to include the ID field in your form. You could include it as a hidden field:
#Html.HiddenFor(model => model.ID)
Now the value of the ID property will be sent to the server when the form is submitted and the default model binder should not complain.
So I have this demo project almost completely working.
public class Project
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
public int ProjectID { get; set; }
public virtual Project Project { get; set; }
}
Controller
public ActionResult Edit(int id)
{
var project = db.Projects.Where(p=>p.ID==id).Single();
return View(project);
}
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
var dbProject = db.Projects.Where(p => p.ID == project.ID).Single();
UpdateModel(dbProject);
db.SaveChanges();
TempData["Success"] = "Modelo Valido";
}
return RedirectToAction("Index");
}
View//strongly typed for project
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Project</legend>
#Html.HiddenFor(model => model.ID)
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<h1>Tasks</h1>
#Html.EditorFor(m => m.Tasks)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
EditorTemplate
#model MvcApplication2.Models.Task
<span>Task</span>
<br />
#Html.LabelFor(m => m.Name)
#Html.EditorFor(m => m.Name)
#Html.HiddenFor(m => m.ID)
#Html.HiddenFor(m => m.ProjectID)
#Html.ValidationMessageFor(m => m.Name)
The view displays this
The problem is that when I submit the form the Tasks are populated with everything except the virtual Project property... so the error i get it is
The operation failed: The relationship could not be changed because
one or more of the foreign-key properties is non-nullable. When a
change is made to a relationship, the related foreign-key property is
set to a null value. If the foreign-key does not support null values,
a new relationship must be defined, the foreign-key property must be
assigned another non-null value, or the unrelated object must be
deleted.
Here is a pic of my debugging breakpoint result
Please Help.
UPDATE:
I have changed my controller action to this
[HttpPost]
public ActionResult Edit(Project project)
{
if (ModelState.IsValid)
{
db.Entry(project).State = EntityState.Modified;
db.SaveChanges();
TempData["Success"] = "Modelo Valido";
return RedirectToAction("Index");
}
return View(project);
}
it is still not working correctly.
Now changes made to the Name of the project are updated correctly in the database. but changes made to any Task Name are ignored completely.
i believe #Html.EditorFor(m => m.Tasks) is generating html like (approximately)
<label>Name</label>
<input type="text" name="Tasks[0].Name" id="auto-gen-id"/>
<input type="hidden" name="Tasks[0].ID" id = "auto-gen-id"/>
<input type="hidden" name="Tasks[0].ProjectID" id = "auto-gen-id"/>
<!--html for validation span-->
Above is the approximate html generated for first Task in Collection and similar html will be generated for each task in the collection. The only difference is that index will be incremented in name attributes of all inputs i.e Tasks[1].Name, Tasks[1].ProjectID etc. This portion will actually bind to the Collection<Task> Tasks property of Project but you can see that in detail portion you don't have any inputs like
<input type="whatever" name="Tasks[0].Project.ProjectID" .../>
<input type="whatever" name="Tasks[0].Project.Name" ..../>
Modelbinder needs input elements with proper naming conventions to bind values to all properties of action method parameters. For testing purpose you can inlude these two lines in your Editor template for Task
#Html.TextBoxFor(x=>x.Project.ID)
#Html.TextBoxFor(x=>x.Project.Name)
input proper values for them in the form and you will have Project property of Task populated with these values. But may not be what you desire i.e entering project information twice and this may not be needed (if u are using Linq to sql its sure not needed). When you call your ORM for attaching entities to db entities it will know which Project elements, current Task belongs to.
Side Note: When you have problems with modelbinding, always pay attention to generated html. Generated html will dictate which form values will map to which properties of the model as long as you are using default modelbinder. it becomes especially important if you are having master detail kind of scenario as in your example.
I have found a way to get this to work but Im not completely happy with the approach.
see this question on how to refactor my current code to see how I am currently (hopefully temporarily doing it)
Help improving (refactoring) my code. Automapper - EF - asp.net mvc-3