wegelagerer
wegelagerer

Reputation: 3710

Iterating ICollection from ViewModel within View

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

Answers (2)

user3559349
user3559349

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

Luke
Luke

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 posting or getting.

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

Related Questions