MVC Get model data posted from dynamically created partial views to controller - c#

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.

Related

Can't pass two parameters to action ASP.NET MVC

I want to pass id and quantity to action, but I get this error: parameter dictionary contains a null entry for parameter id.
I've tried to do map routing, but I can't find how to do it properly.
My Action:
[HttpPost]
public ActionResult Index(int id, int quantity)
{
var user = unitOfWork.Users.FindByEmail(HttpContext.User.Identity.Name);
unitOfWork.Carts.AddProductToCartByEmail(user.Email, id, quantity);
unitOfWork.Complete();
return View();
}
From this page I'm trying to pass the parameters:
#using PagedList;
#using PagedList.Mvc;
#model IPagedList<MVC_Task.Models.AllProductsModel>
#{
ViewBag.Title = "Index";
}
<h2>Products</h2>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.First().Name)
</th>
<th>
#Html.DisplayNameFor(model => model.First().Price)
</th>
#if (User.Identity.IsAuthenticated)
{
<th>
Quantity
</th>
}
</tr>
#foreach (var item in Model)
{
using (Html.BeginForm("Index", "Product", FormMethod.Post))
{
#Html.AntiForgeryToken()
<tr>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Price)
</td>
#if (User.Identity.IsAuthenticated)
{
<td class="form-inline">
#Html.TextBoxFor(modelItem => item.Quantity, new { #type = "number", #class = "form-control" })
<input type="submit" value="Add" class="btn btn-default"
onclick="window.location.href = '#Url.Action("Index", new { item.Id, item.Quantity})';" /> //on this line I send the parameters
</td>
}
</tr>
}
}
</table>
<center>#Html.PagedListPager(Model, page => Url.Action("Index", new { page }))</center>
In post actions there is only one parameter can be taken from body, all other parameters you may pass them as route parametes or query string parameters, so your signature
public ActionResult Index(int id, int quantity)
may become:
public ActionResult Index(int id, [FromBody] int quantity)
you can call the action using the url /index?id=...
and try to call the action using ajax post or submit the form not a hyperlink
for more information see https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api#using-frombody
You're not really posting correctly, using the client side window.location.href - it will result in a HttpGet.
You should add a hidden input for the item id, here's the relevant snippet:
<td class="form-inline">
<!-- add the hidden input -->
#Html.HiddenFor(modelItem => item.Id)
<!-- keep this as is -->
#Html.TextBoxFor(modelItem => item.Quantity, new { #type = "number", #class = "form-control" })
<!-- remove the onclick redirect,
this will post to the action defined in BeginForm -->
<input type="submit" value="Add" class="btn btn-default"/>
</td>
Next, a model is often passed as a complex type. It has many benefits over simple types, because you can extend it with, e.g.; validation logic.
So, lets define a model:
public class PostModel
{
public int Id {get;set;}
public int Quantity {get;set;}
}
Now adjust your Index to accept the model:
[HttpPost]
public ActionResult Index(PostModel model)
{
//your logic
}
By default the binders will bind to the appropriate properties in the model. If your TextBox's name is Id, or Quantity, it will be able to do so. You can validate that in the rendered html.

Update list from Razor View using Entity Framework Core

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; }

MVC 5 BeginCollectionItem Model Always Null

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

Passing collection to the controller from view

I have a model bind to view. I would like to add a checkbox which allows user to change select and submit the selected items for another process. User also can change the value of NumberOfCopies if needed.
I am passing the ManufacturingJobEditModel to the controller. I can see all the items in the PrintErrors collection in the controller. However, I have 2 problems here
ManufacturingJob always NULL in ManufacturingJobEditModel in the controller
Only IsSelected and NumberOfCopies have values. The rest of the properties show NULL values.
Is that anything that I am missing here?
Model
public class ManufacturingJobProductEditModel
{
public ManufacturingJob ManufacturingJob{ get; set;}
public IList<PrintError> PrintErrors { get; set; }
}
public class PrintError
{
public bool IsSelected { get; set; }
public int ProductId { get; set; }
public string ISBN { get; set; }
public string ProductName { get; set; }
public int Sequence { get; set; }
public int NumberofCopies { get; set; }
}
MainView
<table>
<tr>
<td class="display-label valign-top">Products</td>
<td class="display-field white-space-reset"
colspan="3">
<table class="formDisplayTable">
<colgroup>
<col class="width05" />
<col class="width10" />
<col class="width10" />
<col class="width35" />
<col class="width05" />
<col class="width20" />
</colgroup>
<thead>
<tr>
<th></th>
<th>ISBN</th>
<th>Product ID</th>
<th>ProductName</th>
<th>Sequence Number</th>
<th>No of Copies</th>
</tr>
</thead>
<tbody>#foreach (var product in Model.ManufacturingJob.ManufacturingJobProducts.OrderBy(c => c.Sequence))
{
Html.RenderPartial("_PrintErrorDetails", product);
}</tbody>
</table>
</td>
</tr>
</table>
_PrintErrorDetails.cshtml
#model Bolinda.Matrix.Data.Domain.ManufacturingJobProduct
#{Html.RegisterFormContextForValidation();}
<tr class="valign-top">
#using (Html.BeginCollectionItem("PrintErrors"))
{
<td>
<div class="editor-field">#Html.CheckBox("IsSelected")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ISBN")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ManufacturingProduct.Product.ProductId")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("ManufacturingProduct.Product.Name")</div>
</td>
<td>
<div class="table-display-field">#Html.Display("Sequence")</div>
</td>
<td>
<div class="table-editor-field">#Html.Editor("NumberOfCopies")</div>
</td>
}
</tr>
Controller
[HttpPost]
public ActionResult PrintError(ManufacturingJobProductEditModel editModel)
{
var id = editModel.ManufacturingJob.ManufacturingJobId;
ManufacturingJob manufacturingJob = _unitOfWork.ManufacturingJob
.GetWhere(j => j.ManufacturingJobId == id, null, "ManufacturingJobProducts")
.FirstOrDefault();
if (manufacturingJob == null)
{
return new HttpNotFoundResult(String.Format("Manufacturing Job with id {0} was not found.", id));
}
// _service.RequeueErrorCorrection(manufacturingJob, printErrorCorrection, autoCdErrorCorrection, manualCdErrorCorrectionSequenceNumbers);
return RedirectToAction("Details", new { id = manufacturingJob.ManufacturingJobId });
}
ManufacturingJob always NULL in ManufacturingJobEditModel in the controller
The view you have shown does not generate any form controls for any properties so no values are posted back and bound to your model. From the code in your POST method, you appear to only need the ManufacturingJobId property so you need to include
#Html.HiddenFor(m => m.ManufacturingJob.ManufacturingJobId)
Only IsSelected and NumberOfCopies have values. The rest of the properties show NULL values
Again, you have not included form controls for any properties other than the IsSelected and NumberOfCopies of each PrintError object in the collection. If you want the other properties to be bound, use
<td>
<div class="table-display-field">#Html.Display("ISBN")</div>
#Html.HiddenFor(m => m.ISBN)
</td>
or
<td>
<div class="table-display-field">#Html.TextboxFor(m => m.ISBN, new { #readonly = "readonly" })</div>
</td>
Side note: Since you are not dynamically adding or deleting PrintError items in the view, there is no need to use the extra overhead of BeginCollectionItem(). Either use a for loop or a custom EditorTemplate for type of PrintError and in the main view use #Html.EditorFor(m => m.PrintErrors) (refer this answer for an example of using an EditorTemplate). I would also recommend that you populate your models PrintError collection on the server before you pass it to the view (including the .Order() clause) rather that trying to 'fake' it as you are doing.
This is because you are not rendering html input controls for the rest of the model properties other than "IsSelected" and "NumberOfCopies".
"#Html. Display" just render data without any html input control. You can check using page view source.
To render these control you can use below html helper methods. #Html. TextBox, #Html. DropDown, #Html. TextArea and others.
To submit all properties that you required for further processing, you must need to render html input control corresponding to that property. Only then you can able to submit those properties.
Please let me know if problem still persist.

Unable to Bind MVC Model on POST

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].ToB‌​eDeleted=true&CurrentManagers[0].ToBeDeleted=false&CurrentManagers[0].Ext‌​Id=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...

Categories

Resources