asp net mvc post gives empty viewmodel - c#

So I have a list of viewmodels that i iterate through and one of the properties in the viewmodel object is a dictionary. In my customercontroller, in the details action I get all the viewmodels that correspond to the id from the asp-route and in the view i have a form where I present all the dictionary values so that you can modify them if you like. Afterwards you can submit the form. This is where I se that the list of viewmodels are "0". Why is this?
this is my model:
public class CustomerViewModel
{
public CustomerViewModel()
{
EmployeeAndHours = new Dictionary<string, int>();
}
public string projectName { get; set; }
public Dictionary<string, int> EmployeeAndHours { get; set; }
}
this is the get action:
// GET: Customers/Details/5
public IActionResult Details(int? id)
{
if (id == null)
{
return NotFound();
}
var customers = _customerHelper.GetCustomerDetails(id);
if (customers == null)
{
return NotFound();
}
return View(customers);
}
this is the post action:
[HttpPost]
public IActionResult EditCustomerDetailViewModel(List<customerViewModel> customers)
{
//TODO
return View("Details");
}
this is my view:
#model List<myNamespace.Models.ViewModels.CustomerViewModel>
<div class="container-fluid">
<form asp-controller="Customers" asp-action="EditCustomerDetailViewModel" method="post">
#foreach (var customer in Model)
{
<div class="media">
<div class="media-body">
<div class="text-wrapper">
<h5 class="mt-0">#customer.projectName</h5>
</div>
<div class="slider-wrapper">
#foreach (var employee in customer.EmployeeAndHours) // This is the dictionary
{
<input name="#("EmployeeAndHours[" + employee.Key + "]")" type="range" min="0" max="1000" step="1" value="#employee.Value" data-orientation="horizontal">
<hr />
}
</div>
</div>
</div>
}
<div class="form-group" style="text-align:center">
<input id="customer-detail-form-button" type="submit" value="Save changes" class="btn btn-success" />
</div>
</form>
</div>

You cannot use a foreach loop to generate form controls for collection items and get correct 2-way model binding. You need to use a for loop or and EditorTemplate for typeof CustomerViewModel as explained in Post an HTML Table to ADO.NET DataTable.
In addition, you should avoid binding to a Dictionary because you cannot use the strong typed HtmlHelper method or TagHelpers to give 2-way model binding.
In order oo bind to your current model, your name attribute would need to be in the format name="[#].EmployeeAndHours[Key]" where # is the zero-based collection indexer.
Instead, modify your view models to
public class CustomerViewModel
{
public string ProjectName { get; set; }
public List<EmployeeHoursViewMode> EmployeeHours { get; set; }
}
public class EmployeeHoursViewModel
{
public string Employee { get; set; }
public int Hours{ get; set; }
}
And the view then becomes
#model List<CustomerViewModel>
<form asp-controller="Customers" .... >
#for(int i = 0; i < Model.Count i++)
{
....
<h5>#Model[i].ProjectName</h5>
#Html.HiddenFor(m => m[i].ProjectName)
// or <input type="hidden" asp-for-"Model[i].ProjectName />
<div>
#for(int j = 0; j < Model[i].EmployeeHours.Count; j++)
{
#Html.HiddenFor(m => m[i].EmployeeHours[j].Employee)
#Html.LabelFor(m => m[i].EmployeeHours[j].Hours, Model[i].EmployeeHours[j].Employee)
// or <label asp-for="Model[i].EmployeeHours[j].Hours">#Model[i].EmployeeHours[j].Employee</label>
#Html.TextBoxFor(m => m[i].EmployeeHours[j].Hours, new { type = "range", ... })
// or <input asp-for-"Model[i].EmployeeHours[j].ProjectName type="range" ... />
}
</div>
}
<input type="submit" value="Save changes" class="btn btn-success" />
}
Note the above code assumes you want to post back the value of ProjectName (hence the hidden input), and that you want to display the employee name adjacent each 'Hours' input.

You are not referencing the model name from your POST action, try this:
#{int index = 0;}
#foreach (var customer in Model)
{
...
#foreach (var employee in customer.EmployeeAndHours)
{
<input name="customer[#(index)].EmployeeAndHours[#(employee.Key)]" type="range" min="0" max="1000" step="1" value="#employee.Value" data-orientation="horizontal">
<hr />
}
...
index++;
}

Related

Error using TryUpdateModelAsync in a form with navigation properties

Suppose I have a form that contains the following model structure:
TestRecord.cs
class TestRecord {
public string Id { get; set; }
public string RecordName { get; set; }
...other props
public ICollection<RtdbFiles> RtdbFiles { get; set; }
}
Where the corresponding RtdbFile contains the following props:
RtdbFile.cs
class RtdbFile {
public int Id { get; set; }
public string Filename { get; set; }
...other props
}
When I POST this model to my controller to update, I receive the following error:
The instance of entity type 'RtdbFile' cannot be tracked because another instance with the key value '{Id: 2021}' is already being tracked
So it appears that two of the same RtdbFile are being attached to the context. Here's how my controller method is formatted:
[HttpPost("UpdateMilestones")]
public async Task<IActionResult> UpdateMilestones(string testRecordId)
{
db.ChangeTracker.LazyLoadingEnabled = false;
var record = db.TestRecords
.Include(tr => tr.RtdbFiles)
.FirstOrDefault(tr => tr.TestRecordId == testRecordId);
if (await TryUpdateModelAsync(record))
{
await db.SaveChangesAsync();
}
return RedirectToAction("Milestones", new { id = testRecordId });
}
Is TryUpdateModelAsync() not made to handle situations with a One-to-Many relationship? When is the duplicate RtdbFile being added to the context? I've disabled lazy loading and eagerly load the RtdbFiles. This is similar to what is done in the Contoso University example by Microsoft but the difference is their eagerly loaded property is a One-to-One relationship.
How can I fix this? Thanks!
EDIT to show Razor Pages:
UpdateMilestones.cshtml
#model rtdb.Models.TestRecord
#addTagHelper *, rtdb
<input type="hidden" asp-for="#Model.TestRecordId" />
<div class="form-section-text">Milestones & Tracking</div>
<!--unrelated inputs removed -->
<div class="form-group">
<vc:file record="#Model" type="#RtdbFile.AttachmentType.TPR" approvers="true"></vc:file>
</div>
The RtdbFiles are abstracted out a bit in to view components:
File View Component
#model rtdb.Models.ViewModels.FileViewModel
#addTagHelper *, rtdb
#using HtmlHelpers.BeginCollectionItemCore
<div class="form-group attachments">
<div class="link-header">#(Model.AttachmentType.ToString("G"))</div>
<div class="row">
<div class="col-sm-12">
#if (Model.TestRecord.RtdbFiles.Count > 0)
{
foreach (var file in Model.TestRecord.RtdbFiles.Where(f => f.IsDeleted != true && f.Type == Model.AttachmentType && f.TestRecordId == Model.TestRecord.TestRecordId).ToList())
{
<div class="attachment">
#using (Html.BeginCollectionItem("RtdbFiles"))
{
<div class="form-group">
<div class="form-row">
<input asp-for="#file.Id" hidden />
<input asp-for="#file.Type" hidden />
<div class="col-sm-6">
#if (#file.Id < 1)
{
<input class="FileInput" asp-for="#file.UploadedFile" type="file" />
}
else
{
<div><span data-file-id="#file.Id"><a href='#Url.Action("Download", "RtdbFiles", new { id = file.Id })'>#file.Filename (#file.Type.ToString("G"))</a></span></div>
}
</div>
<div class="col-sm-6">
<div>
<label asp-for="#file.FileApproverPersonnel" class="col-form-label col-form-label-sm">Approvers:</label>
<input asp-for="#file.FileApproverPersonnel" class="form-control file-approver-personnel ldap-tags" />
</div>
</div>
</div>
</div>
}
</div>
}
}
<div id="#(Model.AttachmentType.ToString("G"))s"></div>
<button type="button" class="add-file btn btn-primary" data-test-type="Other" data-attachment-type="TPR" data-container="#(Model.AttachmentType.ToString("G"))s">Add #(Model.AttachmentType.ToString("G"))</button>
<small style="display: block; margin-top: 6px">File size limit: 100MB</small>
</div>
</div>
</div>
What is obvious is TryUpdateModelAsync or maybe ChangeTracker has some issues with string ForeignKeys. First of all I highly recommend you to change PrimaryKey to int because EF shows some odd behaviour in such cases.
But if you insist on it, I tried some ways and finally reach this way:
Preventing object from tracking with AsNoTracking and use context.Update after updating record based on controller model
Based on your latest models, It's my sample that works well:
Models:
public class TestRecord
{
public string Id { get; set; }
public string RecordName { get; set; }
public virtual IList<RtdbFile> RtdbFiles { get; set; }
}
public class RtdbFile
{
public int Id { get; set; }
public string TestRecordId { get; set; }
public string Filename { get; set; }
}
Razor Page:
Note: This part has the most important effect on your result. specially RtdbFiles[{i}].Id and RtdbFiles[{i}].Filename
Your View have to send items and values with exactly same name to server object to take effect correctly:
#model Jordan.TestRecord
#using (Html.BeginForm("UpdateMilestones", "Home", FormMethod.Post))
{
#Html.HiddenFor(p => p.Id);
#for (int i = 0; i < Model.RtdbFiles.Count; i++)
{
#Html.Hidden($"RtdbFiles[{i}].Id", Model.RtdbFiles[i].Id);
#Html.TextBox($"RtdbFiles[{i}].Filename", Model.RtdbFiles[i].Filename);
}
<button type="submit">Save</button>
}
Controller:
namespace Jordan
{
[Route("")]
public class HomeController : Controller
{
private readonly AppDbContext context;
public HomeController(AppDbContext context)
{
this.context = context;
context.Database.EnsureCreated();
}
[HttpGet]
public IActionResult Index()
{
var sampleRecord = context.TestRecords
.Include(r => r.RtdbFiles)
.FirstOrDefault();
return View(sampleRecord);
}
[HttpPost]
[Route("UpdateMilestones")]
public async Task<IActionResult> UpdateMilestones(int Id)
{
context.ChangeTracker.LazyLoadingEnabled = false;
var record = context.TestRecords
.Include(tr => tr.RtdbFiles)
.AsNoTracking()
.FirstOrDefault(tr => tr.Id == Id);
if (await TryUpdateModelAsync(record))
{
context.Update(record);
await context.SaveChangesAsync();
}
return RedirectToAction("Index");
}
}
}
I got it, but there seems to be no case of TryUpdateModelAsync on
one-to-many online. (And I tried without success).
Therefore, I suggest that you can use our common method of updating the one-to-many data model.
In the view, you need to bind each field of TestRecord to the corresponding control and pass the latest data of TestRecord to UpdateMilestones action.
Please refer to the following code:
View (which show one record of TestRecord and it related to multiple RtdbFiles datas):
#model WebApplication_core_mvc.Controllers.TestRecord
#{
ViewData["Title"] = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
var i = 0;
}
<h1>Index</h1>
<form asp-action="UpdateMilestones" method="post">
<input id="Text1" type="text" asp-for="Id" hidden />
<label>#Model.Id</label>
<input id="Text2" type="text" asp-for="RecordName" />
<br />
<h4>Rtb:</h4>
<table>
<tr>
<th>Id</th>
<th>FileName</th>
</tr>
#foreach (var item in Model.RtdbFiles)
{
<tr>
<td> <input id="Text1" type="text" value="#item.Id" name="RtdbFiles[#i].Id" hidden/>#item.Id</td>
<td> <input id="Text1" type="text" value="#item.Filename" name="RtdbFiles[#i].Filename" /></td>
</tr>
i++;
}
</table>
<input id="Submit1" type="submit" value="submit" />
</form>
Update
Controller:
[HttpPost("UpdateMilestones")]
public async Task<IActionResult> UpdateMilestones(TestRecord testRecord)
{
db.Entry(testRecord).State = EntityState.Modified;
db.Entry(testRecord).Property(x => x.Id).IsModified = false;
foreach (var item in testRecord.RtdbFiles)
{
db.Entry(item).State = EntityState.Modified;
db.Entry(item).Property(x => x.Id).IsModified = false;
}
await db.SaveChangesAsync();
return RedirectToAction("Milestones", new { id = testRecord.Id });
}
Here is the test result:

.NET Core Radio button group is not getting passed to the controller

Model:
public class TaxCertificateMailing
{
public IList<Report> SelectReports { get; set; }
public class Report
{
public string Text { get; set; }
public bool Selected { get; set; }
}
}
View:
#model LandNav.Areas.Reports.Models.TaxCertificateMailing
#{
ViewData["Title"] = "Tax Certificate Mailing List";
}
#using (Html.BeginForm("TaxCertificateMailing", "Reports", FormMethod.Post, new { id = "reportForm", #class = "report-form col-9" }))
{
<!--Start of the form body-->
<div class="row">
<div class="col-12">
<label><b>Select the report to run:</b></label><br />
#for (var x = 0; x < Model.SelectReports.Count; x++)
{
<input type="radio" asp-for="SelectReports" name="#reports" value="#Model.SelectReports[x].Selected" />
<input type="hidden" asp-for="SelectReports[x].Text"/>
<b>#Model.SelectReports[x].Text</b>
}
</div>
</div>
...
Controller:
[HttpPost]
public ActionResult TaxCertificateMailing(
//IFormCollection form
TaxCertificateMailing TCM
)
{
return View();
}
When the form is posted the SelectReports IList has a count of 0. What is the best way to handle posting a radio button group using .net core?
The name attribute of the input field should be #Model.SelectReports[x].Selected.
Use the code below for the for loop;
#for (var x = 0; x < Model.SelectReports.Count; x++)
{
<input type="radio" asp-for="SelectReports" name="#Model.SelectReports[x].Selected" value="#Model.SelectReports[x].Selected" />
<input type="hidden" asp-for="SelectReports[x].Text"/>
<b>#Model.SelectReports[x].Text</b>
}
Assuming only one value is selected at a time (which is how radio buttons are intended to be used), you have some issues with your models. I'd suggest this:
public class TaxCertificateMailing
{
public TaxCertificateMailing()
{
Reports = new HashSet<Report>();
}
public int SelectedReportID { get; set; }
public ICollection<Report> Reports { get; set; }
}
public class Report
{
public string Text { get; set; }
public int ID { get; set; }
}
This assumes you're pulling these from some sort of database - change the identifier to whatever makes sense, updating SelectedReportID's type to match.
Then, your view would look something like this:
<form asp-action="TaxCertificateMailing" asp-controller="Reports" method="post" id="reportForm" class="report-form col-9">
<fieldset>
<legend>Select the report to run:</legend>
#foreach (var report in Model.Reports)
{
<div class="form-group form-check">
<input type="radio" asp-for="SelectedReportID" id="report-#(report.ID)" value="#report.ID" class="form-check-input" />
<label for="report-#(report.ID)" class="form-check-label">#report.Text</label>
</div>
}
</fieldset>
</form>

Can’t update ICollection property

The problem is when I try to update Master and Details Tables at the same time.
When call Post Edit Task the Details objects don´t appear.
The Edit View displays all Details rows correctly, but while debugging the Edit POST, Casas is empty
MODELS
public partial class Modelo : IValidatableObject {
public Modelo()
{
Casas = new HashSet<Casa>();
}
public int Modeloid { get; set; }
public string Modelo1 { get; set; }
public virtual ICollection<Casa> Casas { get; set; }//Don’t work to update
}
public partial class Casa // DETAIL TABLE
{
public int Casaid { get; set; }
public int Modeloid { get; set; } // FK to Modelo
public string Casa1 { get; set; }
public virtual Modelo Modelo { get; set; }
}
CONTROLLER
public class ModelosController : Controller
. . . . . . . . .
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Modelo modelo)
{
if (id != modelo.Modeloid)
{
return NotFound();
}
if (ModelState.IsValid)
{
// Here modelo.Modelo1 has current modified value
// but modelo.Casas.Count == 0
_context.Update(modelo);
await _context.SaveChangesAsync();
}
}
// GET: Modelos/Edit
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var modelo = await _context.Modelo
.AsNoTracking()
.Include(m => m.Fotomodelos)
.Include(m => m.Casas)
.SingleOrDefaultAsync(m => m.Modeloid == id);
if (modelo == null)
{
return NotFound();
}
return View(modelo);
}
View EDIT.CSHTML
#using System.IO
#model Disponibilidad.Models.Modelo
<form asp-action="Edit">
<div class="form-horizontal">
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Modeloid" />
<div class="form-group">
<label asp-for="Modelo1" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Modelo1" class="form-control" />
<span asp-validation-for="Modelo1" class="text-danger"></span>
</div>
</div>
#{
for (int i = 0; i < Model.Casas.Count; i++)
{
<input type="hidden" asp-for="#Model.Casas.ElementAt(i).Modeloid"
value="#Model.Modeloid" />
<input type="hidden" asp-for="#Model.Casas.ElementAt(i).Casaid" />
<div class="form-group">
<label asp-for="#Model.Casas.ElementAt(i).Casa1"
class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="#Model.Casas.ElementAt(i).Casa1"
class="form-control" /> <!-- DISPLAY OK Detail rows -->
<span asp-validation-for="#Model.Casas.ElementAt(i).Casa1"
class="text-danger"></span>
</div>
</div>
}
}
<div class="btn-group">
<button type="submit" class="btn btn-danger">Save</button>
</div>
</div>
</form>
When you use a for cycle instead of foreach in Razor, the name of the properties doesn't get rendered correctly when using the default asp-for TagHelpers.
You can correct your example changing your razor form inputs as follow:
From:
<input type="hidden" asp-for="#Model.Casas.ElementAt(i).Casaid" />
To:
<input type="hidden" name="modelo.Casas[#i].Casaid" value="#Model.Casas.ElementAt(i).Casaid" />

Html.BeginForm not returning values to controller

Why is this returning null to the controller? The checkbox items are displaying but when I select items to save, the items in the controller action are null.
In the view
#model List<SearchOptionsViewModel>
#using (Html.BeginForm("Save","Query", FormMethod.Post))
{
#Html.AntiForgeryToken()
<li id="li_15">
<div>
<p class="padding" />
#foreach (var item in Model)
{
<div>
#Html.CheckBoxFor(x => item.Selected)
#Html.HiddenFor(x => item.Value)
#Html.DisplayFor(x => item.Text)
</div>
}
</div>
</li>
<p>
<input type="submit" value="Save" />
</p>
}
Action
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Save(List<SearchOptionsViewModel> param)
{
foreach (var item in param)
{
// Do stuff
}
}
View Model
public class SearchOptionsViewModel
{
public string Text { get; set; }
public string Value { get; set; }
public bool Selected { get; set; }
}
You need to provide a numeric indexer (from a for loop) to the HTML Helper. If you use foreach you don't get that. This is because you want the HTML Helpers to render in the form foo[1].Value so that the Model Parser can correctly convert the POSTed HTML form field values into your strongly-typed collection (which is your ViewModel):
#for( int i = 0; i < this.Model.Count; i++ )
{
<div>
#Html.CheckBoxFor( m => m[i].Selected)
#Html.HiddenFor( m => m[i].Value)
#Html.DisplayFor( m => m[i].Text)
</div>
}

ASP.Net MVC Postback from View to Controller shows null values

I've a problem with ViewModel posting back to a controller, but the ViewModel not being mapped correctly from the View to the Controller.
TopicId and Content should contain values, however, when posted back, they do not:
VS Debug:
ViewModels:
public class PostViewModel
{
public int PostId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Author { get; set; }
public DateTime DateOfTopic { get; set; }
}
public class ReplyViewModel
{
public int TopicId { get; set; }
public string Content { get; set; }
}
public class PostListAndReplyVM
{
public List<PostViewModel> PostViewModel { get; set; }
public ReplyViewModel ReplyViewModel { get; set; }
}
View:
#model centreforum.Models.PostListAndReplyVM
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Post</legend>
#Html.HiddenFor(model => model.ReplyViewModel.TopicId)
<div class="editor-label">
#Html.LabelFor(model => model.ReplyViewModel.Content)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.ReplyViewModel.Content)
#Html.ValidationMessageFor(model => model.ReplyViewModel.Content)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
Generated HTML:
<form action="/Post/List/7/" method="post"><input name="__RequestVerificationToken" type="hidden" value="xxxxxxxxxxxxx" /> <fieldset>
<legend>Post</legend>
<input data-val="true" data-val-number="The field TopicId must be a number." data-val-required="The TopicId field is required." id="ReplyViewModel_TopicId" name="ReplyViewModel.TopicId" type="hidden" value="7" />
<div class="editor-label">
<label for="ReplyViewModel_Content">Content</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="ReplyViewModel_Content" name="ReplyViewModel.Content" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="ReplyViewModel.Content" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
</form>
As you can see from the generated HTML, the TopicId definitely has a value: value="7"
Can anyone see where the problem is between the form post, and the controller, which is expecting the ReplyViewModel?
Thank you,
Mark
Your input field names are prefixed with ReplyViewModel (because of the model => model.ReplyViewModel.* lambda), so you need to indicate this information to the model binder:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult List([Bind(Prefix = "ReplyViewModel")] ReplyViewModel model)
{
...
}
Alternatively have your List action take the PostListAndReplyVM model:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult List(PostListAndReplyVM model)
{
// obviously only model.ReplyViewModel will be bound here because
// those are the only input fields in your form
...
}
The problem is the fact that your view is typed to PostListAndReplyVM - so it creates names such as ReplyViewModel.Content - but, because your controller action expects a ReplyViewModel, these fields can't be bound (i.e. there is no such thing as ReplyViewModel.ReplyViewModel.Content).
Change your controller action:
public ActionResult List(PostListAndReplyVM reply)
Alternatively - if that's your whole view - just type it to ReplyViewModel instead (and update your HtmlHelper expressions accordingly).
Its null because you bound it to another model
In view
#model centreforum.Models.PostListAndReplyVM
In Action ReplyViewModel
try to bind like
public ActionResult SomeAction(PostListAndReplyVM model)
{
}

Categories

Resources