3

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.

wegelagerer
  • 3,600
  • 11
  • 40
  • 60

2 Answers2

2

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; }
}
Luke
  • 22,826
  • 31
  • 110
  • 193
  • Indexes can't be used with ICollections so unfortunately this isn't the answer. – wegelagerer Apr 15 '15 at 15:21
  • 2
    Then it's not possible. ICollection doesn't have an indexer and it requires an index to match them up. It can't match specific fields up when there is no order to the list.... – Luke Apr 15 '15 at 15:22
  • IList inherits from ICollection, so changing your model will likely be fine – Luke Apr 15 '15 at 15:23
  • Wouldn't changing ICollection to IList in case of model autoupdate (eg. in case of changes within database tables) change the IList back to ICollection. I mean it wouldn't be such a problem to change it manually but I'd have to remind myself to do it every time it gets updated. – wegelagerer Apr 15 '15 at 15:26
  • Setting IList in a ViewModel isn't possible as I'm passing existing database model to the ViewModel. In short, problematic ICollection can be found in **ProductDetail** model. – wegelagerer Apr 15 '15 at 15:33
  • You shouldn't pass the database model to the view model unless it can be changed to use IList<> or an indexed array. Your `ProductViewModel` should contain an object that is only used as a view model. Not a view model and a database object. – Luke Apr 15 '15 at 15:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/75309/discussion-between-hrvach-and-coulton). – wegelagerer Apr 15 '15 at 15:50
2

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.