0

My main entity is the Recipe which contains a collection of Ingredient items as follows:

 public class Recipe {
    [Key]
    public virtual int RecipeId { get; set; }
    public string RecipeName { get; set; }
    ...
    public virtual ApplicationUser LastModifiedBy { get; set; }
    public virtual IList<Ingredient> Ingredients { get; set; }
}

 public class Ingredient {
    public virtual int IngredientId { get; set; }

    [Display(Name = "Name")]
    public string IngredientName { get; set; }
    ....
    public virtual IList<Recipe> Recipes { get; set; }
}

Which is fine. Then my controller and view for creating a new Recipe are as follows:

 [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "stuff to include")] Recipe recipe)
    {

        IList<int> ingredientIds = (ModelState.Values.ElementAt(1).Value.AttemptedValue).Split(',').Select(int.Parse).ToList(); //[1,2,3,4,5]

        foreach (int id in ingredientIds) {
            Ingredient ing = db.Ingredients.Where(i => i.IngredientId == id).FirstOrDefault() as Ingredient;
            recipe.Ingredients.Add(ing);
        }
            db.Recipes.Add(recipe);
            db.SaveChanges();
            return RedirectToAction("Index");
            ViewBag.Ingredients = new MultiSelectList(db.Ingredients,
            "IngredientId", "IngredientName", string.Empty);
            ViewBag.CreatedById = new SelectList(db.Users, "Id", "Email", recipe.CreatedById);
            return View(recipe);
    }

And the view:

    @for (Int16 i = 0; i < 5; i++) {
        <div class="form-group">
            @Html.LabelFor(model => model.Ingredients, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DropDownList("Ingredients", null, htmlAttributes: new { @class = "form-control" })
            </div>
        </div>
    }

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="button" value="Add Ingredients" class="btn btn-default" />
        </div>
    </div>

So this sets ModelState.Values.ElementAt(1).Value.AttemptedValue = "1,3,5,4,5" where this is a list of id numbers. I know I can come in before the if (ModelState.IsValid) and iterate through the above and place it into recipe.Ingredients which is fine except...

It feels just so un ASP.NET MVC like, as if there's no way they could have thought of so much and not thought of this scenario? Am I missing something here? The ingredients list will be too long to make a multi select list any use.

rory
  • 1,490
  • 3
  • 22
  • 50
  • 1
    What are you actually trying to do here? Why do you have a loop to create 5 dropdownlists (which have no relationship to your model and wont bind correctly to anything)? Why do you have `@Html.ValidationMessageFor(model => model.Ingredients)` when there are no validation attribute applied to property `Ingredients`. I suspect you not understanding what your doing here. You should update you question with an explanation of what your trying to do since none of your code make sense. –  Mar 26 '15 at 23:57
  • @Stephen Muecke A recipe can have many ingredients, and I want the user/creator to be able to select these ingredients from a predefined list of Ingredient objects. I have removed the validation message. The drop down lists do in fact bind to the Ingredient property in my model. – rory Mar 27 '15 at 08:14
  • 1
    No they **do not** bind to your model. When you post back, the value of `Ingredients` will be `null` or an empty list. You either need a `ListBoxFor` (but this may make it difficult for the user to see their selections if you have a lot), or probably better, have each ingredient listed with an associated checkbox. Not sure what would be best for your case? Either one is easy enough to implement and bind to a model on post back. –  Mar 27 '15 at 08:21
  • @StephenMuecke Thanks Stephen, but the list of ingredients could run into the hundreds so I have to go with dropdown lists for now (will upgrade to lookup textfields in the future). I guess I'll add the ingredients manually to the recipe.Ingredients from the list of id's received – rory Mar 27 '15 at 09:43
  • 1
    You can still do this with dropdowns and have it bind correctly, although you have limited the number of ingredients to 5 (what if the user wants to include more?) You would need to handle dynamically adding new ingredients with javascript/jquery, but I'll post an answer a little later to at least get your started and hopefully help you to understand some basics of model binding. –  Mar 27 '15 at 09:47
  • I have an 'add more ingredients' button that has yet to be implemented. I look forward to your posted answer. Also, I've updated my question with better code in the meantime. Thanks – rory Mar 27 '15 at 09:56

1 Answers1

1

You are creating arbitrary dropdownlists that all have the same id (invalid html) and name attribute that has no relationship to your model and wont bind on post back. You first need to create view models that represent what you want to display.

public class RecipeVM
{
  [Required]
  public string Name { get; set; }
  [Display(Name = Ingredient)]
  [Required]
  public List<int?> SelectedIngredients { get; set; }
  public SelectList IngredientList { get; set; }
}

Then in the controller

public ActionResult Create()
{
  RecipeVM model = new RecipeVM();
  // add 5 'null' ingredients for binding
  model.SelectedIngredients = new List<int?>() { null, null, null, null, null };
  ConfigureViewModel(model);
  return View(model);
}

[HttpPost]
public ActionResult Create(RecipeVM model)
{
  if (!ModelState.IsValid)
  {
    ConfigureViewModel(model);
    return View(model);
  }
  // Initialize new instance of your data model
  // Map properties from view model to data model
  // Add values for user, create date etc
  // Save and redirect
}

private void ConfigureViewModel(RecipeVM model)
{
  model.IngredientList = new SelectList(db.Ingredients, "IngredientId", "IngredientName");
}

View

@model RecipeVM
@using (Html.BeginForm())
{
  @Html.LabelFor(m => m.Name)
  @Html.TextBoxFor(m => m.Name)
  @Html.ValidationMessageFor(m => m.Name)
  for (int i = 0; i < Model.SelectedIngredients.Count; i++)
  {
    @Html.LabelFor(m => m.SelectedIngredients[i])
    @Html.DropDownListFor(m => m.SelectedIngredients[i], Model.IngredientList, "-Please select-")
  }
}

Note this is based on your current implementation of creating 5 dropdowns to select 5 ingredients. In reality you will want to dynamically add ingredients (start with none). The answers here and here give you a few options to consider.

Community
  • 1
  • 1