was trying to use Html.BeginCollectionItem to work in my application and am struggling with getting the data to post. I want to add items to a list and then post the entire list. I am using ajax and jquery to add and delete items to my list and that seems to be working. But when I post the model received in my controller is always null even though when I look at fiddler the Form Data contains all my information.
Does anyone see something simple in my code that I am doing wrong?
Main View:
#model Test.Models.TestList
#using (Html.BeginForm())
{
<div class="form-group">
<div class="row">
<label for="AssemblyText" class="col-sm-1 col-sm-offset-1 control-label">Assembly:</label>
<div class="col-sm-2">
<input type="text" id="assembly" />
</div>
<label for="QuantityText" class="col-sm-1 control-label">Quantity:</label>
<div class="col-sm-2">
<input type="text" id="Qty" />
</div>
<button type="button" id="AddAssembly">Add Button</button>
</div>
</div>
<table id="Assemblies" class="table table-striped">
<thead>
<tr>
<th>Assembly</th>
<th>Quantity</th>
<th>Action</th>
</tr>
</thead>
<tbody class="text-left">
#if (Model != null)
{
foreach (var assembly in Model.mylist)
{
#Html.Partial("AssemblyRow", assembly)
}
}
</tbody>
</table>
<div class="form-group">
<input type="submit" id="submitbtn" class="btn btn-success" value="Submit" />
</div>
}
Partial View (AssemblyRow)
#model Test.Models.Test
<tr class="editorRow">
#using (Html.BeginCollectionItem("Assembly"))
{
<td>
#Html.HiddenFor(m => m.assembly)
#Html.TextBoxFor(m => m.assembly)
</td>
<td>
#Html.HiddenFor(m => m.Qty)
#Html.TextBoxFor(m => m.Qty)
</td>
<td>
<span class="dltBtn">
Delete
</span>
</td>
}
My Models are simple and look like...
public class TestList
{
public List<Test> mylist { get; set; }
}
public class Test
{
public string assembly { get; set; }
public string Qty { get; set; }
}
My controller
[HttpPost]
public ActionResult PostMain(TestList model)
{
return View();
}
I can provide whatever other code you guys think is helpful but I tried to keep it simple with what I thought were the relevant pieces.
Thanks for any help!
Edit: Pic of fiddler
Your collection property is named mylist therefore you must pass that name to the BeginCollectionItem method
#using (Html.BeginCollectionItem("mylist"))
{
....
which will generate elements with name=mylist[xxxx].assembly" (where xxxx is a Guid) that are need to correctly bind to your model.
However, you have other issues with your code. The DefaultModelBinder binds the first name/value pair matching a model property and ignores any subsequent name/value pairs with the same name. Because you have a hidden input for each property before the textbox, only the initial values you sent to the view will be bound when you submit, not the edited values. You need to remove both hidden inputs s that the partial is
#model Test.Models.Test
<tr class="editorRow">
#using (Html.BeginCollectionItem("mylist"))
{
<td>#Html.TextBoxFor(m => m.assembly)</td>
<td>#Html.TextBoxFor(m => m.Qty)</td>
<td>
<span class="dltBtn">Delete</span>
</td>
}
</tr>
Side note: It is also unclear what the html in the initial <div class="form-group"> is for. You including 2 inputs and a button, but that will not bind to your model and will not correctly add items to your collection (your add button needs to use ajax to call a server method that returns another partial view and append it to the DOM)
I don't see a submit button, so can't really tell what you are submitting, but try changing ActionResult PostMain(TestList model) to:
ActionResult PostMain(List<Test> model)
You need to use Editor Template instead of partial views. Main controller context is not available in partial views.
There are so many posts on Stackoverflow discussing this. One of them is
ASP.NET MVC 3 - Partial vs Display Template vs Editor Template
Related
I'm having a challenge updating from Razor View table and not sure if my approach is right. Instead of displaying the data (to be edited) in DIV, I'm displaying in a TABLE. I can understand how the hidden property is used to update the row. In my case I am displaying multiple rows on the Edit page, but unable to update the database for only rows that have updates.
My view page is like this
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<table class="table">
<thead>
<tr>
<th> #Html.DisplayNameFor(model => model.Fields[0].Name) </th>
<th> #Html.DisplayNameFor(model => model.Fields[0].Value) </th>
<th></th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Fields)
{
<tr>
<td> <input type="hidden" asp-for=#item.ID /> </td>
<td> #Html.DisplayFor(modelItem => item.Name) </td>
<td contenteditable='true'> #Html.DisplayFor(modelItem => item.Value) </td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
And the code behind is this. The Fields is showing 0 in the OnPostAsync method.
[BindProperty]
public IList<FieldInfo> Fields { get; set; }
public async Task<IActionResult> OnGetAsync(string id)
{
Fields = await _context.FieldInfo.Where(m => m.GUID == id).ToListAsync();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
foreach (var p in Fields) // In this approach Fields shows as count=0
{
_context.Attach(p.Value).State = EntityState.Modified;
}
_context.Attach(Fields).State = EntityState.Modified; // In this approach Exception: The entity type 'List<FieldInfo>' was not found.
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
If you want to work with a List or collection, you must use an index to identify individual elements. https://www.learnrazorpages.com/razor-pages/model-binding#binding-complex-collections
In your case, I would use an explicit index:
#foreach (var item in Model.Fields)
{
<tr>
<td> <input type="hidden" asp-for="Fields[item.ID].ID" /> <input type="hidden" name="Fields.Index" value="Fields[item.ID].ID" /></td>
<td> #item.Name</td>
<td><textarea asp-for="Fields[item.ID].Value">#Fields[item.ID].Value">#Fields[item.ID].Value</textarea> </td>
</tr>
}
Note that the value assigned to a td designated as contenteditable is not posted as part of a form. You should use a suitable form control like an input or textarea instead.
I got it working after changing the type as below, from IList to List
[BindProperty]
public List<FieldInfo> Fields { get; set; }
I am trying to create a view in my application that performs basic CRUD commands in ASP.NET Core to teach myself some new skills. I am however stuck and would appreciate some assistance please.
I would like to have each "component" of the application sitting in a partial view for maintenance going forward. I initially had my Index view use a declaration of type IEnumerable (for the for each loop):
#model IEnumerable<Project.Web.Models.Sample.SampleModel>
Which worked perfect for returning the list and rendering the page but then when trying to have my Modal window partially loaded into the page and insert data using the "CreateSample" function on the controller it was not picking up the function and failed the insert (no form action found). If I then try to add:
#model Project.Web.Models.Sample.SampleModel
to the CreateModal view page it throws an error and wont even let me render the page, I presume because its being partial loaded the app is seen as having two SampleModel declarations. If I create this page completely separate and not partially loaded with the normal #model declaration it works.
I have the basic setup going so far and have included my code for each below.
Model - SampleModel
public class SampleModel
{
public int Id { get; set; }
public string SampleText { get; set; }
}
Controller - SampleController
public class SampleController : Controller
{
public const string ControllerName = "Sample";
//Open Database Connection
private _DBContext DBDatabase = new _DBContext ();
public ActionResult Index()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView(Model);
}
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var Model = DBDatabase.Sample.Select(s => new SampleModel
{
Id = s.Id,
SampleText = s.SampleText
}).ToList();
return PartialView("_CreateModal", Model);
}
Views - Index, View, Create
Index - Calls Partial Views for View and Create
#using Project.Web.Controllers
#model Project.Web.Models.Sample.SampleModel
<!--html stuff here -->
#await Html.PartialAsync("_CreateModal")
<!--more html stuff here -->
#await Html.PartialAsync("_ViewData")
View - Foreach to Loop Records
#model Project.Web.Models.Sample.SampleModel
<table style="width: 100%;" id="example">
<thead>
<tr>
<th>#</th>
<th>Sample Text</th>
<th class="text-center">Status</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
#foreach (var sample in Model)
{
<tr>
<th scope="row">#sample.Id</th>
<td>#sample.SampleText</td>
<td class="text-center">
<div class="badge badge-success">Active</div>
</td>
<td class="text-center">
<div role="group" class="btn-group-sm btn-group">
<button class="btn-shadow btn btn-primary">Edit</button>
<button class="btn-shadow btn btn-primary">Delete</button>
</div>
</td>
</tr>
}
</tbody>
</table>
Create - Insert New Record
#model Project.Web.Models.Sample.SampleModel
<form method="post" asp-action="/SampleModel/CreateSample">
<div class="form-group">
<label for="CreationTime">SampleText</label>
<div>
<input type="text" class="form-control" id="SampleText" name="SampleText" placeholder="SampleText">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Sign up</button>
</div>
</form>
As per Ammar's comment, you've just copy-pasted the Index Controller's data access. When building a form allowing the user to create a single new item, then the pattern is to typically pre-instantiate an empty model and pass it to the view:
[ActionName("_CreateModal")]
public ActionResult InsertNewRecord()
{
var model = new SampleModel(); // If Id is a GUID, then you could assign one here
return PartialView("_CreateModal", model);
}
in my create view I want to give the user the possibility to create a list of objects (of the same type). Therefore I created a table in the view including each inputfield in each row. The number of rows respective "creatable" objects is a fixed number.
Lets say there is a class Book including two properties title and author and the user should be able two create 10 or less books.
How can I do that?
I don't know how to pass a list of objects (that are binded) to the controller. I tried:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ICollection<Book> bookList)
{
if (ModelState.IsValid)
{
foreach(var item in bookList)
db.Books.Add(item);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(articlediscounts);
}
And in the view it is:
<fieldset>
<legend>Book</legend>
<table id="tableBooks" class="display" cellspacing="0" width="100%">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
#for (int i = 0; i < 10 ;i++ )
{
<tr>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
</td>
<td>
<div class="editor-field">
#Html.EditorFor(model => model.Author)
#Html.ValidationMessageFor(model => model.Author)
</div>
</td>
</tr>
}
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
As booklist is null, it doesn't work and I don't know how to put all created objects in this list.
If you have any suggestions I would be very thankful.
Scott Hanselman has some details on passing arrays to MVC control binding: http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Which is essentially: ensure your controls have the correct names: using an index for lists
Change your for loop to something like:
#for (int i = 0; i < 10 ; i++)
{
<tr>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Title" />
</div>
</td>
<td>
<div class="editor-field">
<input type="text" name="book[" + i + "].Author" />
</div>
</td>
</tr>
}
this will then bind to your post action automatically.
[HttpPost]
public ActionResult Create(IList<Book> bookList)
You can then show/hide these as required or use js/jquery to add them dynamically
Edit: As correctly observed by Stephen Muecke, the above answer only regards the binding from the form+fields to the HttpPost, which appears to be the emphasis of the question.
The post action in the original post is not compatible with the view. There's quite a bit of missing code in the OP that may or may not be relevant, but worth observing that if your view is for a single model, then your fail code on ModelState.IsValid needs to return a single model or your view needs to be for an IList (or similar), otherwise you won't get server-side validation (but you can still get client-side validation if you manually add it to the <input>s)
The fact you use #Html.EditorFor(model => model.Title) suggests that you have declared the model in the view as
#model yourAssembly.Book
Which allows to to post back only one Book so the POST method would need to be
public ActionResult Create(Book model)
Note that you current implementation create inputs that look like
<input id="Title" name="Title" ... />
The name attributes do not have indexers (they would need to be name="[0].Title", name="[1].Title" etc.) so cannot bind to a collection, and its also invalid html because of the duplicate id attributes.
If you want to create exactly 10 books, then you need initialize a collection in the GET method and pass the collection to the view
public ActionResult Create()
{
List<Book> model = new List<Book>();
for(int i = 0; i < 10;i++)
{
model.Add(new Book());
}
return View(model);
}
and in the view
#model yourAssembly.Book
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.TextBoxFor(m => m[i].Title)
#Html.ValidationMessageFor(m => m[i].Title)
.... // ditto for other properties of Book
}
<input type="submit" .. />
}
which will now bind to your collection when you POST to
public ActionResult Create(List<Book> bookList)
Note the collection should be List<Book> in case you need to return the view.
However this may force the user to create all 10 books, otherwise validation may fail (as suggested by your use of #Html.ValidationMessageFor()). A better approach is to dynamically add new Book items in the view using either the BeginCollectionItem helper method (refer example) or a client template as per this answer.
You'd need to send a JSON object that has the list of books in it. So the first thing is to create a Model class like this:
public class SavedBooks{
public List<Book> Books { get; set; }
}
Then the Book class would have to have these 2 props:
public class Book {
public string Title { get; set; }
public string Author { get; set; }
}
Next, change your controller to use this model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(SavedBooks model)
Then create a javascript method (using jQuery) to create a JSON object that matches the structure of the controllers SavedBooks class:
var json = { Books: [ { Title: $('#title_1').val(), Author: $('#Author_1').val() } ,
{ as many items as you want }
]
};
$.ajax(
{
url: "/Controller/Create",
type: "POST",
dataType: "json",
data: json
});
How do I post the dynamically created partial views model data to the controller?
My Models Class wrapper:
namespace Diabuddies.DAL
{
public class Exer_Main
{
public Exer_Workout Workout { get; set; }
public Exer_Routine Routine { get; set; }
public Exer_Set Set { get; set; }
}
}
My controller generates the following view (pieces):
#model Diabuddies.DAL.Exer_Main
<body>
#using(Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset style="width:800px">
<legend>Workout</legend>
<div style="float:left;text-align:right">
<table>
<tr style="justify-content:center">
<td>
Workout Name:
</td>
<td>
#Html.TextBoxFor(x => x.Workout.Name, new { style = "width:100%" })
</td>
</tr>
<tr>
<td>
Description:
</td>
<td>
#Html.TextAreaFor(x => x.Workout.Description)
</td>
</tr>
<tr>
<td>
Wrokout Notes:
</td>
<td>
#Html.TextAreaFor(x => x.Workout.Notes)
</td>
</tr>
</table>
<br />
<div id="AddItem">Add Routine</div>
<div id="RoutineRows">
</div>
<p>
<input type="submit" value="Create" />
</p>
</div>
</fieldset>
}
<script>
$(document).ready(function(){
$("#AddItem").click(function () {
//alert("Handler for .click() called.");
$.get( '#Url.Action("AddRoutineHTML", "Default", new { id = "ert" })', function(data) {
$('#RoutineRows').append(data);
});
});
});
</script>
Each time a user clicks on Add Row the following partial view is added:
#model Diabuddies.Models.Exer_Routine
<fieldset>
<legend>Routine</legend>
<table>
<tr style="justify-content:center">
<td>
Routine Name:
</td>
<td>
#Html.TextBoxFor(r => r.Name, new { style = "width:100%" })
</td>
</tr>
<tr>
<td>
Description:
</td>
<td>
#Html.TextAreaFor(x => x.Description)
</td>
</tr>
<tr>
<td>
Notes:
</td>
<td>
#Html.TextAreaFor(x => x.Notes)
</td>
</tr>
</table>
</fieldset>
Here is the problem: How do I post the dynamically created partial views to the controller? Right now I have:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult NewWorkout([Bind(Prefix = "Workout", Include = "Name, Description")]Exer_Workout eWO, List<Exer_Routine> eR)
{
//Exer_Workout stuff returns fine, I am lost on how to get the partial view data here.
Response.Write(eR.Count); //Program breaks here, nulled out. Obv list isn't answer
return View();
}
List is the right answer. It's just how the default model binding works with a list of complex objects. You will need array indexing in your input name properties like this:
<input type="text" name="Exer_Routine[0].Name" />
And for each partial that is loaded you will need to increase the index by 1. You will probably need to write custom HTML rather than use the helpers. I would suggest trying it out by hard coding a list and getting the model binding to work first. Then you can work out how to generate the dynamic HTML.
Hope this helps.
I'm writing a view that displays a list of managers. The managers have checkboxes next to their name to select them to be removed from the manager list. I am having problems binding the form submission back to my view model. Here's what the page looks like:
Here's the ViewModel for the page.
public class AddListManagersViewModel
{
public List<DeleteableManagerViewModel> CurrentManagers;
}
And here's the sub-ViewModel for each of the DeleteableManagers:
public class DeleteableManagerViewModel
{
public string ExtId { get; set; }
public string DisplayName { get; set; }
public bool ToBeDeleted { get; set; }
}
This is the code for the main View:
#model MyApp.UI.ViewModels.Admin.AddListManagersViewModel
<div class="row">
<div class="span7">
#using (Html.BeginForm("RemoveManagers","Admin"))
{
#Html.AntiForgeryToken()
<fieldset>
<legend>System Managers</legend>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(model => model.CurrentManagers)
</tbody>
</table>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Delete Selected</button>
</div>
}
</div>
</div>
And this is the EditorTemplate I've created for DeleteableManagerViewModel:
#model MyApp.UI.ViewModels.Admin.DeleteableManagerViewModel
<tr>
<td>#Html.DisplayFor(model => model.DisplayName)</td>
<td>
#Html.CheckBoxFor(model => model.ToBeDeleted)
#Html.HiddenFor(model => model.ExtId)
</td>
</tr>
But when I submit the form to the controller the model comes back null! this is what I want it to do:
[HttpPost]
public virtual RedirectToRouteResult RemoveManagers(AddListManagersViewModel model)
{
foreach (var man in model.CurrentManagers)
{
if (man.ToBeDeleted)
{
db.Delete(man.ExtId);
}
}
return RedirectToAction("AddListManagers");
}
I tried following along this post: CheckBoxList multiple selections: difficulty in model bind back but I must be missing something....
Thanks for your help!
Hmm. I think this is ultimately the problem; here's what you're posing:
CurrentManagers[0].ToBeDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].ExtId=X00405982144
Your model is an AddListManagersViewModel that has a collection of CurrentManagers. So, you're posting an array of DeleteableManagerViewModel, which isn't getting bound to the "wrapper" model. You can try changing the model parameter to
params DeleteableManagerViewModel[] model
I don't ever use the EditorFor extensions, though, so I'm just guessing...