Reputation: 3710
I have two autogenerated database models (Product and ProductDetails) which I merged into a ViewModel so I can edit all data at once.
What confuses me is the part where I am supposed to iterate through ICollection of Product_ProductCategoryAttributes (within ProductDetail model) inside a view to allow .NET automagically bind properties to the ViewModel. I have tried using for as well as foreach loop but without any success as controls are being created with wrong names (needed for auto binding).
Product model
public partial class Product
{
public Product()
{
this.ProductDetail = new HashSet<ProductDetail>();
}
public int idProduct { get; set; }
public int idProductCategory { get; set; }
public string EAN { get; set; }
public string UID { get; set; }
public bool Active { get; set; }
public virtual ProductCategory ProductCategory { get; set; }
public virtual ICollection<ProductDetail> ProductDetail { get; set; }
}
ProductDetail model
public partial class ProductDetail
{
public ProductDetail()
{
this.Product_ProductCategoryAttribute = new HashSet<Product_ProductCategoryAttribute>();
}
public int idProductDetail { get; set; }
public int idProductCategory { get; set; }
public int idMeta { get; set; }
public int idProduct { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual Meta Meta { get; set; }
public virtual Product Product { get; set; }
public virtual ICollection<Product_ProductCategoryAttribute> Product_ProductCategoryAttribute { get; set; }
public virtual ProductCategory ProductCategory { get; set; }
}
ProductViewModel - One product can have many ProductDetails
public class ProductViewModel
{
public Product Product { get; set; }
public List<ProductDetail> ProductDetails { get; set; }
}
View (some code is intentionally omitted)
@for (int i = 0; i < Model.ProductDetails.Count(); i++)
{
@Html.TextAreaFor(model => model.ProductDetails[i].Description, new { @class = "form-control", @rows = "3" })
@for (int j = 0; j < Model.ProductDetails[i].Product_ProductCategoryAttribute.Count(); j++)
{
@Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).idProductCategoryAttribute)
@Html.TextBoxFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).Value, new { @class = "form-control" })
}
}
All controls outside the second for loop are being named properly eg. ProductDetails[0].Description, however controls generated within the second for loop get their name by the property value which in this case are Value and idProductCategoryAttribute. If I'm not wrong one solution would be converting ICollection to IList, but having model autogenerated I don't think it would be the best option.
Upvotes: 3
Views: 4089
Reputation:
If your model is ICollection<T>
(and can't be changed to IList<T>
or use in a for
loop), then you need to use a custom EditorTemplate
for typeof T
In /Views/Shared/EditorTemplates/Product_ProductCategoryAttribute.cshtml
@model yourAssembly.Product_ProductCategoryAttribute
@Html.HiddenFor(m => m.idProductCategoryAttribute)
@Html.TextBoxFor(m => m.Value, new { @class = "form-control" })
In /Views/Shared/EditorTemplates/ProductDetail.cshtml
@model yourAssembly.ProductDetail
@Html.TextAreaFor(m => m.Description, new { @class = "form-control", @rows = "3" })
@Html.EditorFor(m => m.Product_ProductCategoryAttribute)
In the main view
@model yourAssembly.ProductViewModel
@using (Html.BeginForm())
{
...
@Html.EditorFor(m => m.ProductDetails)
...
The EditorFor()
method will recognize a collection (IEnumerable<T>
) and will render each item in the collection using the corresponding EditorTemplate
including adding the indexers in the controls name
attributes so that the collection an be bound when you post.
The other advantage of a custom EditorTemplate
for complex types is that they can be reused in other views. You can also create multiple EditorTemplate
's for a type by locating them in the view folder associated with a controller, for example /Views/YourControllerName/EditorTemplates/ProductDetail.cshtml
Side note. In any case, you should be using view models for each type that includes only those properties you want to edit/display in the view.
Upvotes: 2
Reputation: 23680
You can't use ElementAt()
within the lambda within the HTML helpers. The name that will be generated will just be the name of the field without indexes which allows the posted values to be populated.
You should use the indexes to traverse all the way through your view model so that the names that are generated actually match up.
So this:
@Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute.ElementAt(j).idProductCategoryAttribute)
Should be this, or similar:
@Html.HiddenFor(model => model.ProductDetails[i].Product_ProductCategoryAttribute[j].idProductCategoryAttribute)
As for changing your model from ICollection
to IList
, this will be fine as IList
inherits from ICollection
. But as you say it is auto generated, it would probably be ok if you were using code first entity framework or something like that.
The real solution is to map your incoming model (the view model) to the auto generated ICollection<>
lists and back again, depending on whether you're post
ing or get
ting.
In the example below, we are taking the posted values and mapping them to the auto generated Product
object and manipulating it.
///
/// ProductViewModel incoming model contains IList<> fields, and could be used as the view model for your page
///
[HttpPost]
public ActionResult Index(ProductViewModel requestModel)
{
// Create instance of the auto generated model (with ICollections)
var product = new Product();
// Map your incoming model to your auto generated model
foreach (var productDetailViewModel in requestModel)
{
product.ProductDetail.Add(new ProductDetail()
{
Product_ProductCategoryAttribute = productDetailViewModel.Product_ProductCategoryAttribute;
// Map other fields here
}
}
// Do something with your product
this.MyService.SaveProducts(product);
// Posted values will be retained and passed to view
// Or map the values back to your valid view model with `List<>` fields
// Or pass back the requestModel back to the view
return View();
}
ProductViewModel.cs
public class ProductViewModel
{
// This shouldn't be here, only fields that you need from Product should be here and mapped within your controller action
//public Product Product { get; set; }
// This should be a view model, used for the view only and not used as a database model too!
public List<ProductDetailViewModel> ProductDetails { get; set; }
}
Upvotes: 2