MVC4 bind model to ICollection or List in partial - c#

Given a Model
public class Task
{
public int TaskId { get; set; }
public string Title { get; set; }
public ICollection<SomeData> Information { get; set; }
}
where
public class SomeData
{
public int SomeDataId { get; set; }
public string Description { get; set; }
}
I have a view
#model myProject.Models.Task
<div>
#Html.LabelFor(model => model.Title)
</div>
<table>
#Html.Partial("_InformationEdit", Model.Information.ToList(), new ViewDataDictionary(Html.ViewDataContainer.ViewData) {
TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Information" }})
</table>
and my partial is
#model IList<myProject.Models.SomeData>
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#Html.EditorFor(modelItem => Model[i].Description)
</td>
</tr>
}
However
My Html fields are being rendered like
<input class="text-box single-line" id="Information__0__Description" name="Information.[0].Description" type="text">
Where the names should be Information[0].Description. It's got an additional dot in there, so is not being bound back to the model correctly when posted. How can I fix this?
As per Model binding to a list I can see what my Id's are supposed to be, but I just can't figure out the correct syntax.
Also, is there a more elegant way to achieve this with an IEnumerable using a #foreach ?
Related:
ASP.Net MVC4 bind a "create view" to a model that contains List
ASP.NET MVC model binding an IList<> parameter

You could use the <input... directly like this:
Page:
<table>
#Html.Partial("_InformationEdit", Model.Information)
</table>
Partial Page:
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
<input class="text-box single-line" id="Information[#i]Description" name="Information[#i].Description" type="text" value="#Model[i].Description" />
</td>
</tr>
}
Or, to be able to pass the prefix as in your example you could keep the Page code the same and change your partial like:
Page:
<table>
#Html.Partial("_InformationEdit", Model.Information,
new ViewDataDictionary(Html.ViewDataContainer.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Information" }
})
</table>
Partial Page:
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#{
string fieldName = string.Format("{0}[{1}].Description", ViewData.TemplateInfo.HtmlFieldPrefix, i);
<input class="text-box single-line" id="#fieldName" name="#fieldName" type="text" value="#Model[i].Description" />
}
</td>
</tr>
}

Changing my partial to
#model IList<myProject.Models.SomeData>
#{
var Information = Model;
}
#for (int i = 0; i < Information.Count(); i++)
{
<tr>
<td>
#Html.EditorFor(modelItem => Information[i].Description)
</td>
</tr>
}
Works, but this seems a bit odd!
I guess ensuring that the object being bound is of the same name as the property it needs to be bound to does some wizardry... Other suggestions or explanations are welcome!

your partial should be:
#model IList<myProject.Models.SomeData>
#for (int i = 0; i < Model.Count(); i++)
{
<tr>
<td>
#Html.EditorFor(model => model[i].Description)
</td>
</tr>
}
also it's easier to read if you replace i by the item name

Related

Can't pass an enumerable model to a controller?

I'm a bit confused because I thought this a very straight-forward thing, it's possibly something simple tripping me up.
I have a view:
#model IEnumerable<CarViewModel>
#using (Html.BeginForm("SummarySaveAll", "VroomVroom", FormMethod.Post))
{
#Html.AntiForgeryToken()
<div>
<table>
<thead>
<tr>
<th width="1">
#Html.DisplayNameFor(model => model.Driver)
</th>
<th width="1">
#Html.DisplayNameFor(model => model.Colour.Name)
</th>
</tr>
</thead>
<tbody>
#foreach (var element in Model)
{
<tr>
<td width="1">
#Html.DisplayFor(m => element.Driver)
</td>
<td width="1">
#Html.DropDownListFor(m => element.Colour, element.Colours, "Unknown")
</td>
</tr>
}
</tbody>
</table>
<div>
<input type="submit" value="Save Changes" class="btn" />
#Html.ActionLink("Cancel Changes", "Index", null, new { #class = "btn" })
</div>
</div>
}
and the list/enumerable of CarViewModel is supposed to bounce back to the VroomVroom controller, action SummarySaveAll which it does - but the viewmodel on the page doesn't get passed back to it:
[HttpPost]
public ActionResult SummarySaveAll(IEnumerable<CarViewModel> summaries)
{
// Want to do stuff with summaries but it's always null
return View();
}
I tried to encapsulate the List in another ViewModel and cycle through elements using a for i loop but that wouldn't pass back to the controller either.
Surely it's possible to send a List or IEnumerable of models back to a controller?
My CarVM:
public class CarViewModel
{
[MaxLength(150)]
[Display(AutoGenerateField = true, Name = "Entered By")]
public string Driver { get; set; }
[Display(AutoGenerateField = true)]
public Colour Colour { get; set; }
[Key]
[Display(AutoGenerateField = false)]
public int Id { get; set; }
[Display(AutoGenerateField = false)]
public bool IsDeleted { get; set; } = false;
public IEnumerable<SelectListItem> Colours { get; set; }
public CarViewModel() { }
public CarViewModel(Model CarModel summaryModel, CarPropertyCollection propertyCollection)
{
Driver = summaryModel.Driver;
Id = summaryModel.Id;
IsDeleted = summaryModel.IsDeleted;
Colour = summaryModel.Colour == null ? null :
propertyCollection.Colours.Where(x => x.Id == summaryModel.Colour.Id).FirstOrDefault();
Colours = propertyCollection.Colours.Select(x => new SelectListItem { Value = x.Id.ToString(), Text = x.Name });
}
}
}
Must stress that Colour is a custom class but only has Id and Name properties
Colours doesn't relate to a specific car, it relates to cars in general, so rather than using a collection as your view model, create a wrapper:
class EditCarsViewModel
{
public IEnumerable<SelectListItem> Colours { get; set; }
public IList<CarViewModel> Cars { get; set; }
}
Then your view:
#model EditCarsViewModel
#for (int i = 0; i < Model.Cars.Length; i++)
{
<td>
#Html.DropDownListFor(m => Model.Cars[i].Colour, Model.Colours, "Unknown")
</td>
}
Any other CarViewModel properties will need their own input as well. HiddenFor can be used if they should be readonly:
#model EditCarsViewModel
#for (int i = 0; i < Model.Cars.Length; i++)
{
#Html.HiddenFor(m => Model.Cars[i].Id)
#Html.HiddenFor(m => Model.Cars[i].Driver)
<!-- etc. -->
<td>
#Html.DropDownListFor(m => Model.Cars[i].Colour.Id, Model.Colours, "Unknown")
</td>
}
And your controller:
[HttpPost]
public ActionResult SummarySaveAll(EditCarViewModel model)
{
// model.Cars should be populated
return View();
}
Note that an indexable collection, such as IList<T> should be used, as the form field names need to include the index to differentiate the items.
Edit by OP
The Colour class consists of a [Key] int Id property and a string Name property. For DropDownList items I had to make sure the Id property was specified on the m => Model.Cars[i].Colour.Id line otherwise that particular prop was coming back as null even though other items were coming through fine.
try
[HttpPost]
public ActionResult SummarySaveAll(IList<CarViewModel> summaries)
{
// Want to do stuff with summaries but it's always null
return View(summaries);
}
I've also added this model as a param for your view
This how you do it:
First my View which posts back to a controller named Home and an action named ListView:
#model List<MyModel>
#{
ViewData["Title"] = "Using a list as model";
}
<h1>#ViewData["Title"]</h1>
#using (Html.BeginForm("ListView", "Home", FormMethod.Post))
{
#Html.AntiForgeryToken()
<div>
<table>
<thead>
<tr>
<th width="1">
Name
</th>
<th width="1">
Description
</th>
</tr>
</thead>
<tbody>
#for (var i = 0; i < Model.Count; i++)
{
<tr>
<td width="1">
#Html.DisplayFor(m => Model[i].Name)
</td>
<td width="1">
#Html.TextBoxFor(m => Model[i].Description)
</td>
</tr>
}
</tbody>
</table>
<div>
<input type="submit" value="Save Changes" class="btn" />
#Html.ActionLink("Cancel Changes", "Index", null, new { #class = "btn" })
</div>
</div>
}
Notice how I used an indexer to render the controls [i]
This is my model:
public class MyModel
{
public string Name { get; set; }
public string Description { get; set; }
}
This is my controller action:
[HttpPost]
public IActionResult ListView(IEnumerable<MyModel> model)
{
return View(model);
}
And this is the result:

View model with complex type is null when passed to controller

I am trying to pass a view model with complex types to my controller. I have researched everything I can top to bottom over this subject and I am still confused.
The Problem:
When I click my submit button, the view model is passed in but the List of MacroInfo property is null.
UpdateIndexViewModel
public class UpdateIndexViewModel
{
//This view model will become larger later
public List<MacroInfo> MacrosToUpdate { get; set; }
}
MacroInfo
public class MacroInfo
{
public bool IsSelected { get; set; }
public string FullPath { get; set; }
public string Id { get; set; }
public DateTime CreatedAt { get; set; }
}
Controller Action
[HttpPost]
public ActionResult Submit(UpdateIndexViewModel updateIndexViewModel)
{
//updateIndexViewModel.MacrosToUpdate is null ??
}
Index View
#model EplanInterface.Core.ViewModels.UpdateIndexViewModel
#using (Html.BeginForm("Submit", "Update", FormMethod.Post))
{
<table style="width:100%" , class="table-bordered">
<thead>
<tr>
<th>#</th>
<th>Macro Path</th>
<th>Created At</th>
<th>Update</th>
</tr>
</thead>
#for (int i = 1; i < Model.MacrosToUpdate.Count; i++)
{
<tr>
<td>#Html.TextBoxFor(m =>Model.MacrosToUpdate[i].FullPath)</td>
<td>#Html.TextBoxFor(m => Model.MacrosToUpdate[i].CreatedAt)</td>
<td>#Html.CheckBoxFor(b => Model.MacrosToUpdate[i].IsSelected)</td>
</tr>
}
</table>
<input type="submit" class="btn btn-primary" value="Submit"/>
}
What I have tried
I tried changing the controller actions property being passed in to List<MacroInfo> macrosToUpdate, but when doing this the property is still null.
Chrome network inspection
Final Remarks
I am not sure if I need to be using an AJAX post to do this, or if my variable names just are not formatted correctly. I am pretty sure it is a binding issue I am not understanding.
If anyone could point me in the right direction I would really appreciate it.
This part of your template is bit wrong.
#for (int i = 1; i < Model.MacrosToUpdate.Count; i++)
{
<tr>
<td>#Html.TextBoxFor(m =>Model.MacrosToUpdate[i].FullPath)</td>
<td>#Html.TextBoxFor(m => Model.MacrosToUpdate[i].CreatedAt)</td>
<td>#Html.CheckBoxFor(b => Model.MacrosToUpdate[i].IsSelected)</td>
</tr>
}
Please change with following and try again.
#for (int i = 0; i < Model.MacrosToUpdate.Count;
{
<tr>
<td>#i</td>
<td>#Html.TextBoxFor(m => m.MacrosToUpdate[i].FullPath)</td>
<td>#Html.TextBoxFor(m => m.MacrosToUpdate[i].CreatedAt)</td>
<td>#Html.CheckBoxFor(b => b.MacrosToUpdate[i].IsSelected)</td>
</tr>
}
First you were starting the loop with 1, which was the root cause. Model binder wasn't able to bind the list properly due to missing zeroth index.

Model item bind in List

I have passed the model to partial view,And then i need to bind some text fields to the model in the view.
#model SRINews.Domain.NewsTotal
#using (Html.BeginForm("UpdateNewsItem", "Home", FormMethod.Post))
{
<table class="table table-borderless table-cart" id="mytable" data-addclass-on-smdown="table-sm">
<tbody>
#foreach (var item in Model.items)
{
<tr class="newsRow" id="#item.ItemId">
<td class="cart-img nostretch">
<img src="#item.ImageUrl" alt="">
</td>
</tr>
<tr>
<td>
<input type="text" class="form-control" placeholder="Personalized Name">
//#Html.TextboxFor(x=>x)
// I want to bind PersonalizedName to model
</td>
</tr>
<tr>
<td>
<input type="text" class="form-control" placeholder="Country">
// I want to bind Country to model
</td>
</tr>
}
</tbody>
</table>
<input type="submit" class="btn btn-primary" value="Personal Details" />
}
Model
public class Items
{
public int ItemId { get; set; }
public string ItemCode { get; set; }
public string PersonalizedName {get;set;}
public string Country {get;set;}
}
public class NewsTotal
{
public int BaseItem { get; set; }
public string BaseName {get;set;}
public List<Items> items { get; } = new List<Items>();
}
Public ActionResult UpdateNewsItem(NewsTotal nTotal)
{
return View();
}
You want to use a traditional for loop so you can use the index to bind to your List<T> in the model, you'll also need to make items mutable, so you'll need to have a set for it as well or else you won't be able to submit anything:
//You'll need to make this mutable, so it can post the edited values
public List<Items> items { get; set; } = new List<Items>();
Then in your View:
#for(int i = 0; i < Model.items.Count; i++)
{
#Html.HiddenFor(x => Model.items[i].ItemId)
#Html.HiddenFor(x => Model.items[i].ItemCode)
<tr class="shoppingCartRow" id="#Model.items[i].ItemId">
<td class="cart-img nostretch">
<img src="#Model.items[i].ImageUrl" alt="">
</td>
</tr>
<tr>
<td>
#Html.TextboxFor(x=> Model.items[i].PersonalizedName, new { #placeholder = "Personalized Name"})
</td>
</tr>
<tr>
<td>
#Html.TextboxFor(x=> Model.items[i].Country, new { #placeholder = "Country"})
</td>
</tr>
}

Not able to see ViewModel property in View?

I'm still trying to get my head wrapped around using ViewModels and IEnumerable/List as properties. I'm obviously missing something that is not allowing me to see my IEnumerable in my View.
What I'm ultimately trying to do is have a view that will show a list of unassigned users not assigned to a Group (called Patrols in this case and would have PatrolId=0). The table will have a checkbox next to each member. Above that table will be a DropDownList of the available Patrols. The Admin will come to the page to see those that aren't assigned, select a Patrol from the DDL at the top, check the users he want's to assign to that Patrol, and then submit the form that will pass the PatrolID from the DDL and find all the selected members in order to update their record with that PatrolId.
In the View below when I go to loop through the Scout property, I would assume I would be able to use Model.Scout in this part in order to loop through and write out all the members in that IEnumerable.
#for (var i = 0; i < Model.Count(); i++)
However, when I try to use intellisense to see the Scout property when using the Model, I don't see the property. In addition to that I would think I would need to first check the Scout property to see if there is a count before I write out all the rows. Again, I can't see the Scout property in order to check it's count.
ViewModel
public class PatrolMemberViewModel
{
[Key]
public int MemberId { get; set; }
public int PatrolId { get; set; }
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "Phone")]
public string PhonePrimary { get; set; }
[Display(Name = "Email")]
public string EmailPrimary { get; set; }
public bool IsSelected { get; set; }
}
public class PatrolUnassignedViewModel
{
public SelectList Patrols { get; set; }
public IEnumerable<PatrolMemberViewModel> Scout { get; set; }
}
Controller
// GET:
public ViewResult Unassigned()
{
PatrolUnassignedViewModel unassinged = new PatrolUnassignedViewModel();
unassinged.Patrols = new SelectList(repository.SelectAllPatrols());
unassinged.Scout = repository.SelectAllUnassigned();
return View(unassinged);
}
Repository
public IEnumerable<PatrolMemberViewModel> SelectAllUnassigned()
{
using (DataContext db = new DataContext())
{
var results = (from p in db.Person
where p.IsActive == true
&& p.IsScout == true
&& p.PatrolId == 0
select new PatrolMemberViewModel
{
MemberId = p.PID,
FirstName = p.FirstName ?? string.Empty,
LastName = p.LastName ?? string.Empty,
EmailPrimary = p.EmailPrimary ?? string.Empty,
PhonePrimary = p.PhonePrimary ?? string.Empty,
PatrolId = p.PatrolId,
IsSelected = false
}
).OrderBy(o => o.LastName).ThenBy(o => o.FirstName).ToList();
return results;
}
}
public IEnumerable<PatrolName> SelectAllPatrols()
{
return db.PatrolNames;
}
View
#model IList<ProjectName.ViewModels.PatrolUnassignedViewModel>
#{
ViewBag.Title = "Unassigned";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Patrols</h2>
<h4>Assign Scouts to a Patrol.</h4>
#using (Html.BeginForm("Update", "Patrol", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(false, "", new { #class = "alert alert-danger" })
<table class="table table-bordered table-striped table-hover table-condensed tbackground">
<tr>
<th class="text-center">
</th>
<th class="text-center">
First Name
</th>
<th class="text-center">
Last Name
</th>
<th class="text-center">
Email
</th>
<th class="text-center">
Phone
</th>
</tr>
#for (var i = 0; i < Model.Count(); i++)
{
<tr>
<td class="text-center">
#Html.CheckBoxFor(m => m[i].IsSelected)
</td>
<td>
#Html.DisplayFor(m => m[i].FirstName)
</td>
<td>
#Html.DisplayFor(m => m[i].LastName)
</td>
<td>
#Model[i].EmailPrimary
</td>
<td class="text-center">
#Html.DisplayFor(m => m[i].PhonePrimary)
</td>
</tr>
}
</table>
<div class="control-wrapper">
<input type="submit" id="btnSubmit" value="Assign Scouts" class="btn btn-success" />
</div>
}
<p> </p>
You controller is returning a single instance of class PatrolUnassignedViewModel
public ViewResult Unassigned()
{
PatrolUnassignedViewModel unassinged = new PatrolUnassignedViewModel();
unassinged.Patrols = new SelectList(repository.SelectAllPatrols());
unassinged.Scout = repository.SelectAllUnassigned();
return View(unassinged);
}
Your view is expecting an IList
#model IList<ProjectName.ViewModels.PatrolUnassignedViewModel>
When it should be expecting
#model ProjectName.ViewModels.PatrolUnassignedViewModel
Your scout is an IEnumerable so doesn't have a count method so should be
public IList<PatrolMemberViewModel> Scout { get; set; }
public IList<PatrolMemberViewModel> SelectAllUnassigned()
{
}
You should be doing your loop like this
#for (var i = 0; i < Model.Scout.Count(); i++)
{
<tr>
<td class="text-center">
#Html.CheckBoxFor(m => m.Scout[i].IsSelected)
</td>
<td>
#Html.DisplayFor(m => m.Scout[i].FirstName)
</td>
<td>
#Html.DisplayFor(m => m.Scout[i].LastName)
</td>
<td>
#Model.Scout[i].EmailPrimary
</td>
<td class="text-center">
#Html.DisplayFor(m => m.Scout[i].PhonePrimary)
</td>
</tr>
}
Am I missing something here?
From your GET action method, you are passing a single object of PatrolUnassignedViewModel. But your view is bound to a collection of PatrolUnassignedViewModel. So change your view to be like
#model PatrolUnassignedViewModel
Now you can use the Model.Scout property which is a collection.
#model PatrolUnassignedViewModel
<h2>Total : #Model.Scout.Count()</h2>
#foreach(var item in Model.Scout)
{
<label>#item.FirstName</label>
}

Another null collection being passed to MVC Controller

I need additional eyes to see:
What I am doing wrong as I try to pass a collection of objects to a MVC controller and all I get is sgList = null.
How can I check so that I only save the rows that being changed.
[HttpPost]
public ActionResult Index(IList<EZone_ServiceGroup> sgList)
{
try
{
foreach (EZone_ServiceGroup sg in sgList)
svcGroupRepo.UpdateServiceGroup(sg);
return RedirectToAction("Index");
}
catch
{
return View();
}
}
View:
#model IEnumerable<KTCEzone.Domain.Entities.EZone_ServiceGroup>
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
<div class="row">
<table class="table table-condensed table-bordered table-hover table-striped small" id="sgTable">
<tr>
<th class="col-sm-12">#Html.DisplayNameFor(model => model.GroupID)</th>
<th>#Html.DisplayNameFor(model => model.GroupName)</th>
<th>#Html.DisplayNameFor(model => model.ParentGroupID)</th>
<th>#Html.DisplayNameFor(model => model.Active)</th>
<th>#Html.DisplayNameFor(model => model.OrderIndex)</th>
</tr>
#{var items = Model.ToArray();}
#for (int i = 0; i < items.Length; i++)
{
<tr>
<td>#Html.DisplayFor(modelItem => items[i].GroupID)</td>
<td>#Html.EditorFor(modelItem => items[i].GroupName) </td>
<td>#Html.EditorFor(modelItem => items[i].ParentGroupID) </td>
<td>#Html.CheckBoxFor(modelItem => items[i].Active) </td>
<td>#Html.EditorFor(modelItem => items[i].OrderIndex) </td>
</tr>
}
</table>
</div>
}
Model:
public class EZone_ServiceGroup
{
public int GroupID { get; set; }
public string GroupName { get; set; }
public bool Active { get; set; }
public int OrderIndex { get; set; }
public int ParentGroupID { get; set; }
}
Change your model to #model IList<KTCEzone.Domain.Entities.EZone_ServiceGroup>, and remove #{var items = Model.ToArray();} from the view and use
#for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>#Html.DisplayFor(m => m[i].GroupID)</td>
<td>#Html.EditorFor(m=> m[i].GroupName)</td>
<td>#Html.EditorFor(m=> m[i].ParentGroupID)</td>
<td>#Html.CheckBoxFor(m=> m[i].Active) </td>
<td>#Html.EditorFor(m=> m[i].OrderIndex) </td>
</tr>
}
which will correctly name your elements. If you cannot change the collection to IList, then you need to use a custom EditorTemplate for the type of the model, and use in conjunction with #Html.EditorFor()
As for "How can I check so that I only save the rows that being changed", all controls will be posted back, so you need to compare the posted values with the original values in the controller.

Categories

Resources