11

I have a view model that contains a Product class type and an IEnumerable< Product > type. On the post the first level product object comes back binded from the viewmodel whereas the product enum is coming back null.

Why is the IEnumerable< Prouduct> property not getting binded to my view model per the post? Thx!

Models:

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductIndexViewModel
{
    public Product NewProduct { get; set; }
    public IEnumerable<Product> Products { get; set; }
}

public class BoringStoreContext 
{
        public BoringStoreContext()
         {
             Products = new List<Product>();
             Products.Add(new Product() { ID = 1, Name = "Sure", Price = (decimal)(1.10) });
             Products.Add(new Product() { ID = 2, Name = "Sure2", Price = (decimal)(2.10) });
         }
         public List<Product> Products {get; set;}
}

View:

@model ProductIndexViewModel

@using (@Html.BeginForm())
{
    <div>
        @Html.LabelFor(model => model.NewProduct.Name)
        @Html.EditorFor(model => model.NewProduct.Name)
    </div>
    <div>
        @Html.LabelFor(model => model.NewProduct.Price)
        @Html.EditorFor(model => model.NewProduct.Price)
    </div>
    <div>
        <input type="submit" value="Add Product" />
    </div>

        foreach (var item in Model.Products)
        { 
         <div>
        @Html.LabelFor(model => item.ID)
        @Html.EditorFor(model => item.ID)
    </div>
    <div>
        @Html.LabelFor(model => item.Name)
        @Html.EditorFor(model => item.Name)
    </div>
    <div>
        @Html.LabelFor(model => item.Price)
        @Html.EditorFor(model => item.Price)
    </div>

        }
       }

Controller:

public class HomeController : Controller
{
    BoringStoreContext db = new BoringStoreContext();

    public ActionResult Index()
    {
        ProductIndexViewModel viewModel = new ProductIndexViewModel
        {
            NewProduct = new Product(),
            Products = db.Products
        };
        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(ProductIndexViewModel viewModel)
    {
        // ???? viewModel.Products is NULL here
        // ???? viewModel.NewProduct comes back fine

        return View();
    }
genxgeek
  • 13,109
  • 38
  • 135
  • 217

2 Answers2

14

You are not using your lambda expression properly. You need to be accessing the Products list through the model. Try doing it like this:

@count = 0
foreach (var item in Model.Products)
    { 
     <div>
    @Html.LabelFor(model => model.Products[count].ID)
    @Html.EditorFor(model => model.Products[count].ID)
</div>
<div>
    @Html.LabelFor(model => model.Products[count].Name)
    @Html.EditorFor(model => model.Products[count].Name)
</div>
<div>
    @Html.LabelFor(model => model.Products[count].Price)
    @Html.EditorFor(model => model.Products[count].Price)
</div>
@count++
    }

Edit

Controller:

BoringStoreContext db = new BoringStoreContext();

    public ActionResult Index()
    {
        ProductIndexViewModel viewModel = new ProductIndexViewModel
        {
            NewProduct = new Product(),
            Products = db.Products
        };
        return View(viewModel);
    }

    [HttpPost]
    public ActionResult Index(ProductIndexViewModel viewModel)
    {
        // work with view model

        return View();
    }

Model

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductIndexViewModel
{
    public Product NewProduct { get; set; }
    public List<Product> Products { get; set; }
}

public class BoringStoreContext
{
    public BoringStoreContext()
    {
        Products = new List<Product>();
        Products.Add(new Product() { ID = 1, Name = "Sure", Price = (decimal)(1.10) });
        Products.Add(new Product() { ID = 2, Name = "Sure2", Price = (decimal)(2.10) });
    }
    public List<Product> Products { get; set; }
}

View:

@model Models.ProductIndexViewModel


@using (@Html.BeginForm())
{
<div>
    @Html.LabelFor(model => model.NewProduct.Name)
    @Html.EditorFor(model => model.NewProduct.Name)
</div>
<div>
    @Html.LabelFor(model => model.NewProduct.Price)
    @Html.EditorFor(model => model.NewProduct.Price)
</div>


for (int count = 0; count < Model.Products.Count; count++ )
{ 
    <div>
    @Html.LabelFor(model => model.Products[count].ID)
    @Html.EditorFor(model => model.Products[count].ID)
    </div>
    <div>
    @Html.LabelFor(model => model.Products[count].Name)
    @Html.EditorFor(model => model.Products[count].Name)
    </div>
    <div>
    @Html.LabelFor(model => model.Products[count].Price)
    @Html.EditorFor(model => model.Products[count].Price)
    </div>
}

<div>
    <input type="submit" value="Add Product" />
</div>
}
Travis J
  • 81,153
  • 41
  • 202
  • 273
  • 1
    Why would you use a foreach loop to mimic a regular for loop? And why would you repeatedly call model.Products[count] when you already have the item in the foreach loop? – rossisdead Mar 28 '12 at 20:58
  • @Xander - Thank you. The Helpers don't really respond well to using the `.` expansion so `model.Products.item.ID` wont work hence the indexing. I forgot to remove the `.item` however from the solution and editted it to reflect the change you commented on in your first comment. – Travis J Mar 28 '12 at 20:59
  • 1
    @rossisdead - As commented above, `model.Products.item` is not valid here as nice as it would be to use. A `for(int count = 0; count < Model.Products.Count; count++)` would also suffice. – Travis J Mar 28 '12 at 21:03
  • Ah, I went and tested it out. I didn't realize that using the indexer caused the rendered name attribute to also include the index. Neat! – rossisdead Mar 28 '12 at 21:13
  • @TravisJ: Thx for the reply. Not sure if I'm missing something here but even after correcting the [count] (can't use [] indexing on IEnumerables to model.Products.ElementAt(count).ID (etc...) the product list still comes back as null in the view model. – genxgeek Mar 28 '12 at 21:19
  • @JaJ try removing the extra `@` sign in the view markup ... `@using (@Html.BeginForm())` – Alex Mar 28 '12 at 21:27
  • @JaJ - Actually you are correct, this should be held in a List, see edit for a completed, testing, working, example. Note the change from `IEnumerable` to `List`, and the `for(){}` loop change. – Travis J Mar 28 '12 at 21:45
  • Oops, previous comment should read ..., tested, ... because I actually ran that code and in the debugger the viewmodel did hold the list of products. – Travis J Mar 28 '12 at 21:53
  • @TravisJ: Awesome! Tested and works! So, in a nutshell my original wasn't working because I wasn't accessing the model correctly? I mean via item.ID, etc,. per foreach (var item in ViewModel.Prodcuts) is not referencing the model correctly? – genxgeek Mar 28 '12 at 22:15
  • 1
    @JaJ - Well, in the lambda expression, when you start with `model =>` you have to access that in the next part. MSDN: "The lambda expression x => x * x is read "x goes to x times x." So you would need to use that model in the second half of the expression if you wanted the binding. As for using the `foreach` with a dot notation for access, this is, in my opinion, a missing feature that badly needs implementation from the MVC/Razor team. – Travis J Mar 28 '12 at 22:49
  • @TravisJ: Makes sense. Agreed with your last comment! Thx again! – genxgeek Mar 28 '12 at 22:52
  • Nice! Good work folks. My issue is simple and it kicks my a$$ everytime. foreach (var item in Model) needs to know what to access from the viewmodel @foreach (var item in Model.Products) – JoshYates1980 Aug 09 '14 at 16:15
7

Travis's answer will address the issue. Here is another approach using EditorTemplates

You can use an Editor template to display the Products and it will work fine.

1) Create a Folder called "EditorTemplates" under your Views/Home folder

2 ) Add a view called "Products" inside that. Add this code to that view

@model SO_MVC.Models.Product
@{
   Layout = null;
}
<p>
  @Html.LabelFor(x=>x.ID)
  @Html.EditorFor(x=>x.ID) 
</p>
<p>
  @Html.LabelFor(x=>x.Name)
  @Html.EditorFor(x=>x.Name)
</p>
<p>
  @Html.LabelFor(x=>x.Price)
  @Html.EditorFor(x=>x.Price)
</p> 
@Html.HiddenFor(x=>x.Id)

and in your Main View, you can call this Editor template like this

  @Html.EditorFor(m=>m.Products)
Community
  • 1
  • 1
Shyju
  • 214,206
  • 104
  • 411
  • 497