ASP.NET Core 6 MVC : HTML table to view model collection - asp.net-core-mvc

Starting up with ASP.NET Core 6 MVC and in my case I have one view which lists few properties of one object and then some other for their children in a editable table (user can edit the values)
View model has the properties and an IEnumerable of the children:
public class MyObjectViewModel
{
public String Id { get; set; }
public String Descr { get; set; }
public IEnumerable<ChildrenObject> Children { get; set; }
public MyObjectViewModel()
{
Children = Enumerable.Empty<ChildrenObject>();
}
public class ChildrenObject
{
public String? Id { get; set; }
public String? Name { get; set; }
}
}
All under the same form:
#using (Html.BeginForm("Save", "Controller", FormMethod.Post))
{
<input type="submit" value="SAVE"/>
<br />
#Html.LabelFor(model => model.Id)
#Html.TextBoxFor(model => model.Id, new { #readonly = "readonly" })
<br />
#Html.LabelFor(model => model.Descr)
#Html.EditorFor(model => model.Descr)
<br />
<table>
<tbody>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
#foreach (var child in Model.Children)
{
<tr>
<td>#child.Id</td>
<td><input class="ef-select" type="text" value="#child.Name"></td>
</tr>
}
</tbody>
</table>
}
So when the button is pressed the data is dumped back to the model to perform the SAVE action in the controller.
All ok for the simple fields (I can get the data in the controller as the view model), but not sure how to accomplish it with the table / children property...
Any simple way without needing to use JS to serialise or pick up the data from the table?
Thanks

If you want to pass id and name of ChildrenObject in table,you can try to add hidden inputs for the Id,and set name attribute for name inputs:
#{var count = 0; }
#foreach (var child in Model.Children)
{
<tr>
<td>
#child.Id
<input value="#child.Id" name="Children[#count].Id" />
</td>
<td><input class="ef-select" type="text" value="#child.Name" name="Children[#count].Name"></td>
</tr>
count++;
}

Related

I lost my data on submit MVC 3

I have a MVC 3 application with entity framework.
In my page I use a custom Model that contains all objects I use. The page is rendered perfectly, but when I press the submit button my object loses the data.
This is my custom model:
public class ControleAcessoModel
{
private List<Controle> controles = new List<Controle>();
public GRUPO_ACESSO_TB grupo_acesso_tb { get; set; }
public List<Controle> Controles
{
get
{
return controles;
}
}
public void AddTela(byte id, string nome)
{
Controle ctrl = new Controle();
ctrl.ID_TELA = id;
ctrl.NM_TELA = nome;
controles.Add(ctrl);
}
public class Controle
{
public bool Selecionado { get; set; }
public byte ID_TELA { get; set; }
public string NM_TELA { get; set; }
public bool FL_SALVAR { get; set; }
public bool FL_ALTERAR { get; set; }
public bool FL_EXCLUIR { get; set; }
}
}
this is my Razor Html code:
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<table>
<tr>
<th>Salvar</th>
<th>Editar</th>
<th>Excluir</th>
<th>Tela</th>
</tr>
#foreach (var item in Model.Controles)
{
<tr>
<td style="text-align: center">
#Html.EditorFor(modelItem => item.FL_SALVAR)
</td>
<td style="text-align: center">
#Html.EditorFor(modelItem => item.FL_ALTERAR)
</td>
<td style="text-align: center">
#Html.EditorFor(modelItem => item.FL_EXCLUIR)
</td>
<td>
#Html.DisplayFor(modelItem => item.NM_TELA)
</td>
</tr>
}
</table>
<p>
<input type="submit" value="Salvar" />
</p>
}
This is my create code, where I put the data on database.
Is in this part, that my object controleacessomodel is empty.
[HttpPost]
public ActionResult Create(ControleAcessoModel controleacessomodel, byte id)
{
if (ModelState.IsValid)
{
for (int i = 0; i < controleacessomodel.Controles.Count; i++)
{
if (ValidaSelecao(controleacessomodel.Controles[i]))
{
PERMISSAO_GRUPO_ACESSO_TELA_TB permissao = new PERMISSAO_GRUPO_ACESSO_TELA_TB();
permissao.ID_GRUPO_ACESSO = controleacessomodel.grupo_acesso_tb.ID_GRUPO_ACESSO;
permissao.ID_TELA = controleacessomodel.Controles[i].ID_TELA;
permissao.FL_SALVAR = controleacessomodel.Controles[i].FL_SALVAR;
permissao.FL_ALTERAR = controleacessomodel.Controles[i].FL_ALTERAR;
permissao.FL_EXCLUIR = controleacessomodel.Controles[i].FL_EXCLUIR;
db.PERMISSAO_GRUPO_ACESSO_TELA_TB.AddObject(permissao);
}
}
db.SaveChanges();
return RedirectToAction("Edit", "GrupoAcesso", new { id = id });
}
return View(controleacessomodel);
}
Why my object is empty after submit?
It's not possible to use the Foreach loop construct as the generated ids will be not be correct, therefore MVC is not able to map the values back to the model. You need to use a for loop instead:
#for (int i = 0 ; i < Model.Controles.Count; i++)
{
<tr>
<td style="text-align: center">
#Html.EditorFor(m => m.Controles[i].FL_SALVAR)
</td>
<td style="text-align: center">
#Html.EditorFor(m => m.Controles[i].FL_ALTERAR)
</td>
<td style="text-align: center">
#Html.EditorFor(m => m.Controles[i].FL_EXCLUIR)
</td>
<td>
#Html.DisplayFor(m => m.Controles[i].NM_TELA)
</td>
</tr>
}
Phil Haack write a good blog post about this. Also Scott Hanselman wrote a nice post too.

How to pass a list of objects instead of one object to a POST action method

I have the following GET and POST action methods:-
public ActionResult Create(int visitid)
{
VisitLabResult vlr = new VisitLabResult();
vlr.DateTaken = DateTime.Now;
ViewBag.LabTestID = new SelectList(repository.FindAllLabTest(), "LabTestID", "Description");
return View();
}
//
// POST: /VisitLabResult/Create
[HttpPost]
public ActionResult Create(VisitLabResult visitlabresult, int visitid)
{
try
{
if (ModelState.IsValid)
{
visitlabresult.VisitID = visitid;
repository.AddVisitLabResult(visitlabresult);
repository.Save();
return RedirectToAction("Edit", "Visit", new { id = visitid });
}
}
catch (DbUpdateException) {
ModelState.AddModelError(string.Empty, "The Same test Type might have been already created,, go back to the Visit page to see the avilalbe Lab Tests");
}
ViewBag.LabTestID = new SelectList(repository.FindAllLabTest(), "LabTestID", "Description", visitlabresult.LabTestID);
return View(visitlabresult);
}
Currently the view display the associated fields to create only one object,, but how i can define list of objects instead of one object to be able to quickly add for example 10 objects at the same “Create” request.
My Create view look like:-
#model Medical.Models.VisitLabResult
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#section scripts{
<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>VisitLabResult</legend>
<div class="editor-label">
#Html.LabelFor(model => model.LabTestID, "LabTest")
</div>
<div class="editor-field">
#Html.DropDownList("LabTestID", String.Empty)
Your viewModel
public class LabResult
{
public int ResultId { get; set; }
public string Name { get; set; }
//rest of the properties
}
Your controller
public class LabController : Controller
{
//
// GET: /Lab/ns
public ActionResult Index()
{
var lst = new List<LabResult>();
lst.Add(new LabResult() { Name = "Pravin", ResultId = 1 });
lst.Add(new LabResult() { Name = "Pradeep", ResultId = 2 });
return View(lst);
}
[HttpPost]
public ActionResult EditAll(ICollection<LabResult> results)
{
//savr results here
return RedirectToAction("Index");
}
}
Your view
#model IList<MvcApplication2.Models.LabResult>
#using (Html.BeginForm("EditAll", "Lab", FormMethod.Post))
{
<table>
<tr>
<th>
ResultId
</th>
<th>
Name
</th>
</tr>
#for (int item = 0; item < Model.Count(); item++)
{
<tr>
<td>
#Html.TextBoxFor(modelItem => Model[item].ResultId)
</td>
<td>
#Html.TextBoxFor(modelItem => Model[item].Name)
</td>
</tr>
}
</table>
<input type="submit" value="Edit All" />
}
Your view will be rendered as follows, this array based naming convention makes it possible for Defaultbinder to convert it into ICollection as a first parameter of action EditAll
<tr>
<td>
<input name="[0].ResultId" type="text" value="1" />
</td>
<td>
<input name="[0].Name" type="text" value="Pravin" />
</td>
</tr>
<tr>
<td>
<input name="[1].ResultId" type="text" value="2" />
</td>
<td>
<input name="[1].Name" type="text" value="Pradeep" />
</td>
</tr>
If I understand your question correctly,
you want to change your view to be a list of your model object #model List, then using a loop or however you wish to do it, create however many editors you need to for each object
then in your controller your receiving parameter of create will be a list of your model instead too.

mvc3 submit model empty

I have a problem which I don't understand and there doesn't seem to be an easy way to debug the problem. I'm sure it's simple.
#model StartStop.ServiceResources.UserSettings
my MVC3 view is bound a specific model;
public class Setting
{
public Int64 SettingID { get; set; }
public Int64 UserID { get; set; }
public int PreferenceType { get; set; }
public string PreferenceName { get; set; }
public bool PreferenceBool { get; set; }
public int PreferenceInt { get; set; }
public string PreferenceString { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime ModifiedOn { get; set; }
}
public class UserSettings
{
public Int64 UserID { get; set; }
public List<Setting> Settings { get; set; }
}
the view lists out the check boxes which represent the list;
#using (Html.BeginForm("ManageAccount","Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
foreach (var item in Model.Settings.ToList())
{
<tr>
<td>#item.PreferenceName
</td>
<td>
#if (item.PreferenceType == 2)
{
#Html.CheckBoxFor(modelItem => item.PreferenceBool)
}
</td>
</tr>
}
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
All good, I load the data into the view it renders the view and picks up the correct settings. However, when I do a post at the bottom, the view model returns a null! I'm not sure why...
[HttpPost]
[Authorize]
public ActionResult ManageAccount(StartStop.ServiceResources.UserSettings model)
{
if (ModelState.IsValid)
{
foreach (StartStop.ServiceResources.Setting oSetting in model.Settings)
{
StartStop.Helpers.UserPreferences.SaveUserSetting(oSetting);
}
}
return View(model);
}
Can anyone help?
The problem is on the following line in your view:
#Html.CheckBoxFor(modelItem => item.PreferenceBool)
I see people writing the following lambda expression modelItem => item.SomeProperty in their views very often and asking why the model binder doesn't correctly bind collection properties on their view models.
This won't generate proper name for the checkbox so that the default model binder is able to recreate the Settings collection. I would recommend you reading the following blog post to better understand the correct format that the model binder expects.
Try like this:
#model StartStop.ServiceResources.UserSettings
#using (Html.BeginForm("ManageAccount", "Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
for (var i = 0; i < Model.Settings.Count; i++)
{
<tr>
<td>#Model.Settings[i].PreferenceName</td>
<td>
#if (Model.Settings[i].PreferenceType == 2)
{
#Html.CheckBoxFor(x => x.Settings[i].PreferenceBool)
}
</td>
</tr>
}
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
This being said, I would recommend you using editor templates, like so:
#using (Html.BeginForm("ManageAccount","Account", FormMethod.Post))
{
<table class="tbl" cellspacing="0">
<tr>
<th>Preference</th>
<th>Setting</th>
</tr>
#if (Model != null)
{
#Html.EditorFor(x => x.Settings)
}
</table>
<input type="submit" value="Save Changes" class="action medium" />
}
and then define a custom editor template which will automatcially be rendered for each element of the Settings collection (~/Views/Shared/EditorTemplates/Setting.cshtml):
#model StartStop.ServiceResources.Setting
<tr>
<td>#Model.PreferenceName</td>
<td>
#if (Model.PreferenceType == 2)
{
#Html.CheckBoxFor(x => x.PreferenceBool)
}
</td>
</tr>
Also the only input field that I can see in this form is the checkbox which is bound to the PreferenceBool property on your model. So inside your POST controller action you will get the Settings list property initialized but don't expect to find any values for the other properties in this Setting class unless of course you include input fields for them in the form (and more precisely in the editor template that I have shown).

Posting to a list<modeltype> MVC3

I am trying to get my view to post a List back to the action however it keeps coming in as null.
So my Model has a List of WeightEntry objects.
Exercise Model
public class Exercise
{
public List<WeightEntry> Entries { get; set; }
public int ExerciseID { get; set; }
public int ExerciseName { get; set; }
}
WeightEntry Model
public class WeightEntry
{
public int ID { get; set; }
public int Weight { get; set; }
public int Repetition { get; set; }
}
My View contains the ExerciseName and a forloop of WeightEntry objects
#model Mymvc.ViewModels.Exercise
...
<span>#Model.ExerciseName</span>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<table class="left weight-record">
<tr>
<th>Reps</th>
<th>Weight</th>
</tr>
#foreach (var item in Model.Entries)
{
<tr>
<td>
#Html.EditorFor(x => item.Repetition)
</td>
<td>
#Html.EditorFor(x => item.Weight)
</td>
</tr>
}
</table>
<input type="submit" value="Save" />
}
The Controller Action (Post) Does nothing at the moment. I am just trying to get the binding working before I add the save code.
[HttpPost]
public ActionResult WeightEntry(Exercise exercise)
{
try
{
//Add code here to save and check isvalid
return View(exercise);
}
catch
{
return View(exercise);
}
}
I have seen a few little tricks with adding a numerator to the form elements' names used in MVC2 but I was wondering if MVC3 was any different? I was hoping it would all bind nicely with ID's being 0 or null but instead the whole List is null when I inspect it after the form posts. Any help is appreciated.
Thanks.
Replace the following loop:
#foreach (var item in Model.Entries)
{
<tr>
<td>
#Html.EditorFor(x => item.Repetition)
</td>
<td>
#Html.EditorFor(x => item.Weight)
</td>
</tr>
}
with:
#for (var i = 0; i < Model.Entries.Count; i++)
{
<tr>
<td>
#Html.EditorFor(x => x.Entries[i].Repetition)
</td>
<td>
#Html.EditorFor(x => x.Entries[i].Weight)
</td>
</tr>
}
or even better, use editor templates and replace the loop with:
#Html.EditorFor(x => x.Entries)
and then define a custom editor template that will automatically be rendered for each element of the Entries collection (~/Views/Shared/EditorTemplates/WeightEntry.cshtml):
#model WeightEntry
<tr>
<td>
#Html.EditorFor(x => x.Repetition)
</td>
<td>
#Html.EditorFor(x => x.Weight)
</td>
</tr>
The the generated input elements will have correct names and you will be able to successfully fetch them back in your POST action.

MVC3 textbox submits null value?

I am creating a dynamic list of text boxes. when the user submits the value in the fields come back null. I think I'm missing something.
This is my product model:
public class EnqProduct
{
public string Id { get; set; }
public string Product { get; set; }
public string Quantity { get; set; }
}
This is the page model which includes a list of the above.
public IList<EnqProduct> EnqProduct { get; set; }
This is how I am setting the model:
IList<EnqProduct> items2 = Session["enquiry"] as IList<EnqProduct>;
var EnquiryModel = new Enquiry {
EnqProduct = items2
};
return View(EnquiryModel);
and this is how I display the fields:
foreach (var item in Model.EnqProduct)
{
<tr>
<td>
<span class="editor-field">
#Html.TextBox(item.Id, item.Product)
#Html.ValidationMessageFor(m => m.A1_Enquiry)
</span>
<br><br>
</td>
<td>
<span id = "field" class="editor-field">
#Html.TextBox(item.Id, item.Quantity)
</span>
<br><br>
</td>
</tr>
}
When the user submits the fields go back to the controller null?
I would recommend you using editor templates and replace your foreach loop with the following:
#model Enquiry
<table>
<thead>
<tr>
<th>product name</th>
<th>quantity</th>
</tr>
</thead>
<tbody>
#Html.EditorFor(x => x.EnqProduct)
</tbody>
</table>
and then define an editor template which will automatically be rendered for each element of the EnqProduct collection (~/Views/Shared/EditorTemplates/EnqProduct.cshtml):
#model EnqProduct
<tr>
<td>
#* We include the id of the current item as hidden field *#
#Html.HiddenFor(x => x.Id)
<span class="editor-field">
#Html.EditorFor(x => x.Product)
#Html.ValidationMessageFor(x => x.Product)
</span>
</td>
<td>
<span id="field" class="editor-field">
#Html.EditorFor(x => x.Quantity)
</span>
</td>
</tr>
Now when you submit the form you will get correct values:
public class HomeController: Controller
{
public ActionResult Index()
{
var model = new Enquiry();
model.EnqProduct = ...
return View(model);
}
[HttpPost]
public ActionResult Index(Enquiry model)
{
// the model.EnqProduct will be correctly populated here
...
}
}
As far as the correct wire format that the default model binder expects for your input fields I would recommend you taking a look at the following article. It will allow you to more easily debug problems in the feature when some model is not properly populated. It suffice to look with FireBug and the name of the values being POSTed and you will immediately know whether they are OK or KO.

Resources