0

MVC5 EF6

I have a Product. A product can have multiple Titles, A title has a Type which is an Enum.

I am working on the Create View for a Product - The Model is the Product

View:

            @for (int x = 0; x < Model.ProdTitles.Count; x++)
            {                    
                <tr>
                    <td>
                        @Html.TextBoxFor(model => model.ProdTitles.ToArray()[x].Title, new { @class = "form-control" })
                        @Html.ValidationMessageFor(model => model.ProdTitles.ToArray()[x].Title, "", new { @class = "text-danger" })
                    </td>
                    <td>
                        @Html.EnumDropDownListFor(model => model.ProdTitles.ToArray()[x].TitleTypeID, new { @class = "form-control" })
                    </td>
                    <td>
                        @Html.EnumDropDownListFor(model => model.ProdTitles.ToArray()[x].CultureID, new { @class = "form-control" })
                    </td>
                </tr>
            }

In the Controller - when I create a product to return to the view, I create one title for each title type and add it to the product. The view displays everything as I expect.

Working as required

When I hit the Create button, the product and the titles are returned to the controller as expected and I validate the titles (different validation depending on the type). I add any errors to the ModelState and therefore, ModelState.IsValid is false.

I return back to the View return View(product); Debugging this product, all the titles are in the product and they all still have their correct types but the View now displays the first Enum in the list, for all titles and not the one that is actually in the model!

Showing first enum for all titles

If I change the EnumDropDown to a text box, the correct type is displayed, so the model is definitely correct:

proves model has the correct type

I'm not sure why this is happening and I hope someone can suggest a fix? Is it a bug in the EnumDropDownFor? or am I doing something wrong?

Controller code:

    public ActionResult Create()
    {
        Product product = new Product();

        foreach (var enm in Utils.Enums.EnumHelper.GetValues<Utils.Enums.TitleType>())
        {
            product.ProdTitles.Add(new ProdTitle()
            {
                CultureID = Utils.Enums.CultureID.English_United_Kingdom,                    
                DateCreated = DateTime.Now,
                Title = "",
                TitleTypeID = enm
            });
        }

        return View(product);
    }


    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "ProdID,DateCreated")] Product product, ICollection<ProdTitle> prodTitles)
    {
        //ensure titles are all valid before saving
        for (int x = 0; x < prodTitles.Count; x++)
        {
            ProdTitle title = prodTitles.ToArray()[x];
            if (!title.IsValid)
            {
                ModelState.AddModelError(string.Empty, title.TitleTypeID + " title is invalid.");
            }
            product.ProdTitles.Add(title);
        }


        if (ModelState.IsValid)
        {
            db.Products.Add(product);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        return View(product);
    }

ProdTitle model

public partial class ProdTitle
{
    public long TitleID { get; set; }
    public long ProdID { get; set; }
    public Utils.Enums.TitleType TitleTypeID { get; set; }
    public string Title { get; set; }
    public Utils.Enums.CultureID CultureID { get; set; }
    public System.DateTime DateCreated { get; set; }

    public virtual Product Product { get; set; }
    public virtual DataSource DataSource { get; set; }
}
Percy
  • 2,855
  • 2
  • 33
  • 56
  • show your controller code – Ehsan Sajjad Apr 25 '15 at 16:00
  • What about the culture column? Are the values correct after the postback? I assume some Javascript problem, maybe the oncahnge event resetting the selected values... – VladL Apr 25 '15 at 16:11
  • @VladL the culture currently only has one option but I'll try adding another to test – Percy Apr 25 '15 at 16:14
  • @EhsanSajjad I'm away from computer now but will add ASAP. But the returned product in the controller is populated as expected. – Percy Apr 25 '15 at 16:15
  • Make sure to check the Post Method. Normally you would have 2 Methods for this. When you click submit, You will depend on the [HttpPost] attribute Method not on the Original one. So if you did not tell from that method to control the correct type, then it will return the default type. – Aizen Apr 25 '15 at 20:35
  • Firstly its just `@Html.TextBoxFor(model => model.ProdTitles[x].Title)` (no `.ToArray()`. When dealing with dropdownlists in a collection, you need to use a custom `EditorTemplate` for the type. You need to show the model for property `ProdTitles` –  Apr 26 '15 at 08:44
  • @StephenMuecke ProdTitles is an ICollection so that doesn't work but I can use .ElementAt(x) so fair enough. – Percy Apr 26 '15 at 10:22
  • @Rick, That will not work at all (and I was referring to the pointless use of `.ToArray()`). You need to use an `EditorTemplate` but you have not shown the model as I requested so I can't help –  Apr 26 '15 at 10:25
  • just added controller code, now adding model for ProdTitle. model.ProdTitles[x] doesn't work for an ICollection. `Cannot apply indexing with [] to an expression of type 'System.Collections.Generic.ICollection'` – Percy Apr 26 '15 at 10:27

1 Answers1

2

When dealing with dropdownlist in a collection you need a custom EditorTemplate.

In /Views/Shared/EditorTemplates/ProdTitle.cshtml

@model yourAssembly.ProdTitle

<tr>
  <td>
    @Html.TextBoxFor(m => m.Title, new { @class = "form-control" })
    @Html.ValidationMessageFor(m => m.Title, new { @class = "text-danger" })
  </td>
  <td>
    @Html.EnumDropDownListFor(m => m.TitleTypeID, new { @class = "form-control" })
  </td>
  <td>
    @Html.EnumDropDownListFor(m => m.CultureID, new { @class = "form-control" })
  </td>
</tr>

Then in the main view

@model yourAssembly.Product
@using(Html.BeginForm())
{
  .... // other controls for properties of Product
  @Html.EditorFor(m => m.ProdTitles) // not in a loop!
}

and then modify the controller to

public ActionResult Create(Product product)

Note: Your current [Bind] attributes excludes the ProdTitle property from binding, and in any case you should be using view models to represent only what you want to display/edit

  • Perfect - works exactly as expected/required. I'm new to MVC so I wasn't aware this method. My method almost did what I needed but now I see your solution it was obviously not the correct approach. Could you please expand on your last comment about `[Bind]` and view models. I've used the scaffolded Product Create and tried to expand on it. – Percy Apr 26 '15 at 11:00
  • 1
    Generally you should never use data models in your views if they are for creating or editing objects. Always use view models that contain only those properties you need in the view (see [What is ViewModel in MVC?](http://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc)) which means you never need the `[Bind(Include="..")]` attribute. Based on what you have shown, you would have `class ProdTitleVM` with properties `Title`, `TitleTypeID` and `CultureID` and `class ProductVM` which contains property `List ProdTitle`. –  Apr 26 '15 at 11:09
  • 1
    You then map properties to the view model in the GET method and from the view model to a data model in the POST method - using tools such as [automapper](https://github.com/AutoMapper/AutoMapper) make it easier. Also, look at your current attribute - you exclude every property except `"ProdID, DateCreated"` but its a method for creating a new product - values for an ID and DateCeated properties would not even exist yet (it has not been saved to the database!) so you effectively are excluding all properties from binding –  Apr 26 '15 at 11:14
  • Great, thanks for the info - I'll check out the info you've linked to. As a follow up, suppose in your example, I wanted to make TitleTypeID readonly (but still keep the appearance of the dropdown), I can add `@readonly="readonly", @disabled="disabled"` to the attributes and add a hiddenfield to the template: `@Html.HiddenFor(m => m.TitleTypeID)` but doing this gets me back to the value being reset to the first enum in the list once I hit the create button withut filling in the titles. How would you achieve that – Percy Apr 26 '15 at 11:45
  • Sorry, not entirely clear what your asking. If the initial value of `TitleTypeID` is (say) `A` and you have disabled the dropdownlist, only the value of the hidden input `A` will be posted back which will then be displayed again when you return the view. How are you changing it? –  Apr 26 '15 at 12:13