MVC 5: ViewModel / Passing lists for the create

Kahn Kah picture Kahn Kah · Jun 2, 2016 · Viewed 30.1k times · Source

So I am currently studying and analyzing the use of ViewModels.

In my Application (a so called "Restaurant") I want the ability for my "users" to create a menu.

When they want to create a menu: They can choose the name + the amount of persons that can join the menu. BUT also, they can add an amount of dishes that are already in the restaurant. This will be in the style of checkboxes and an 'Create'-Button at the end.

This means I had to use a ViewModel. I am currently trying to give the possibility to add a list of dishes to a menu for the creation. But I'm stuck at the for loop, used to loop through the dishes. Or better, I'm stuck at the whole concept:

  • What is the best way to display all the already created dishes to the CreateMenu View? Is it still possible to loop through a ViewBag if I will add them in a ViewBag?

  • Lets say I successfully tried to do what I wanted to do. How would I create a new Menu based (or extracted?) from the ViewModel?

In my Code, please note that the Menu - Model cannot be changed really because I already use a list of Dishes from it (In another view, where I display all the menu's and their dishes).

also ignore the possibility of wrong names or spelling mistakes in data, since I translated everything from Flemish

Models

public class Menu
{
    [Key]
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    [Range(0,10)]
    public int AmountPersons { get; set; }
    [Range(0,double.MaxValue)]
    public double Price { get; set; }
    public virtual List<Dish> Dishes { get; set; }
}

public class Dish
{
    [Required]
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    public enum Types { VOORGERECHT, HOOFDGERECHT, DRANK, DESSERT}
    public Types Type { get; set; }
    public double Price { get; set; }
    public virtual List<Menu> Menus { get; set; }
    public virtual List<Table> Tables { get; set; }
    //Checked used for the 'checkbox' in the CreateMenu-View
    [NotMapped]
    public bool Checked { get; set; }
}

public class MenuViewModel
{
    public Menu Menu { get; set; }
    public List<Dish> AddedDishes { get; set; }
}

Controller

public ActionResult CreateMenu( )
{
    MenuViewModel gm = new MenuViewModel();
    // Assign ALL already created dishes to the list that the user can choose.
    // AddedDishes is wrong? ViewBag preferred?
    gm.AddedDishes = db.Dishes.ToList();
    return View(gm);
}

// Add the Menu to all the Menu's in the Database.
[HttpPost]
public ActionResult MenuAanmaken(MenuModel gm)
{
    // code to save the menu with all the added dishes to the database
    // Conflict!? Cannot convert the MenuViewModel to the Menu-model How do we need to extract the Menu and the AddedDishes list 
    // to a menu and save that one to the database?
    db.Menus.Add(gm);
    return View(gm);
}

View

@using VBExamen.Models
@model MenuViewModel
....

@Html.LabelFor(m => m.Menu.Name)
@Html.EditorFor(m => m.Menu.Name)
@Html.LabelFor(m => m.Menu.AmountPersons)
@Html.EditorFor(m => m.Menu.AmountPersons)

@for(int i = 0; i < Model.AddedDishes.Count; i++)
{
    <tr>
        <td>
            @Html.DisplayFor( .Name)
            @Html.HiddenFor(item => .Id)
            @Html.CheckBoxFor(item => .Checked)
        </td>
    </tr>
}

E D I T E D _ U P D A T E (SEE BELOW) Okay So I think I'm close now,

I edited my classes as the following:

public class MenuViewModel<T>
{
    public Menu Menu { get; set; }
    public List<T> DishList { get; set; }
    public MenuViewModel()
    {
        this.Lijst = new List<T>();
    }
}

Controller

public ActionResult CreateMenu(MenuViewModel<Dish> model )
{
    model.DishList = db.Gerechten.ToList();
    return View(model);
}

[HttpPost]
public ActionResult CreateMenu(MenuViewModel<Dish> model,List<Gerecht> SelectedList)
{
    Menu t = new Menu();
    t.Naam = gm.Menu.Naam;
    t.AmountPersons = gm.Menu.AmountPersons;
    t.Dishes = SelectedList;
    db.Menus.Add(t);
    return View("Menus", model);
}

View function creating list

@for (int i = 0; i < Model.DishList.Count(); i++)
{
    <tr>
        <td>
            @Html.Label(Model.DishList[i].Naam)
            <input type="hidden" [email protected]("DishList[{0}].Id", i) [email protected](i).Id />
            <input type="hidden" [email protected]("DishList[{0}].Name", i) [email protected](i).Name />
            <input type="checkbox" [email protected]("DishList[{0}].value", i) />
            <input type="hidden" [email protected]("DishList[{0}].value", i) value="false" />
        </td>
        <br />
    </tr>
}

I did this after watching about 10 tutorials about ViewModels, is my next approach better than the first one?

I think so because i get the following on my screen:

enter image description here

I was thinking what the next approach would be. I was thinking about comparing the 2 lists (1 of the viewmodel, 1 passed) and see the checkbox statuses?

UPDATE

After Stephen Muecke's answer I re-edited my code but found a problem that I can't seem to understand.

The answer says I should be in the position of a 1-to-many table in the form as a class:

// You have not indicated the 1-many table the dishes are saved to so adjust as required
        MenuDish dish = new MenuDish()
        {
            MenuId = menu.ID,
            DishId = dish
        };
        db.MenuDishes.Add(dish);

However, what we've learned at school was, that if you create lists in the data-models of the entities, linked tables will be automatically generated in the Database. And that is exactly what my DB has done (without the creation of the MenuDish class):

MenuGerechts stands for MenuDish. This is the automatically created table done by the entity framework. enter image description here That brings me to the following questions. I have re-edited the controller to the following:

 [HttpPost]
        public ActionResult MenuAanmaken(MenuVM model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            IEnumerable<int> selectedDishes = model.Dishes.Where(x => x.IsSelected).Select(x => x.ID);
            Menu menu = new Menu()
            {
                Naam = model.Name,
                AantalPersonen = model.AmountPersons
            };

             foreach (int id in selectedDishes)
                {
                    Dish g = db.Dishes.Find(id);
                    menu.Dishes.Add(g);

                };

            db.Menus.Add(menu);
            db.SaveChanges(); 

            return RedirectToAction("Menus", "Menu");
        }

I get the Object reference not set to an instance of an object error and I'm understanding why ofcourse.

I have done the changes since the Data-Model Menu, already has a List of Dishes. But assuming the answer of S. Muecke, this isn't the correct way to solve this ViewModel since he proposes the use of a New Class (that is created to support the one-to-many relationship)?

This brings me to the conclusion of the following questions:

  • Why is it impossible or not-recommended to directly add the selected dishes to the menu instance?

  • Is it always needed to create the in between table 'MenuDish' in a Data-model?

  • Will the following code still work (showing the menu's and their dishes) after creating new Menu's?:

Controller:

   public ActionResult Menus()
            {
                List<Menu> menus = db.Menus.ToList();
                return View(menus);
            }

View:

@model IEnumerable<VBExamen.Models.Menu>

@{
    ViewBag.Title = "Menus";
}

<h2>Menus</h2>

<p>
    @Html.ActionLink("Create New Menu", "CreateMenu")
</p>

@foreach (var item in Model)
{
    <table>


        <ul>
            <p>@Html.DisplayFor(modelItem => item.Name)</p>

            @foreach (var g in item.Dishes)
            {
                <li>
                    @Html.DisplayFor(modelItem => g.Name)
                </li>
            }
        </ul>
    </table>
}

Which outputs the following:

enter image description here

What would be good motivations to do this?

UPDATE 2

So I have included the following in my project: ** I have used the Table()- annotation to make it use the one that's already created**

  • **Model: **

    [Table("MenuGerechts")] public class MenuGerechts { [Key] [ForeignKey("Menu")] public virtual int? MenuId { get; set; } public virtual Menu Menu { get; set; }

        [ForeignKey("Dish")]
        public virtual int? DishId { get; set; }
        public virtual Dish Dish { get; set; }
    }
    

I have then actually created new menus successfully! But when I go to the overview menu page (from the pic above), it only shows the Name of the menu, and not the list of meals that it includes.

The Database however didn't allow my MenuDish link table to be used for my newly created class (it created a new one, and renamed the old one with the 'old' menus with a '1' behind it:

enter image description here

Hence why I was asking my previous questions. Does this mean my whole approach to this exercise was wrong?

New Question: My menuCreate ViewModel only works if i Select 1 dish? Why is this so? I get the following error The role 'MenuGerechts_Menu_Source' of the relationship 'VBExamen.Models.MenuGerechts_Menu' has multiplicity 1 or 0..1.

Answer

Andy Mason picture Andy Mason · Jun 2, 2016

This is only the answer to your first question... I gotta get back to work :)

I strongly advise you to use Entity Framework for storing this information, as creating the data context, Initializer(Entity Framework Requirements) and View Model will allow you to scaffold everything in your project including controllers and views. This means you take the information from the ViewModel class rather than from the view bag.

Scaffolding means that Visual Studio will create all your code to CRUD(Create, Read, Update, Delete) Controllers and Views to allow this to happen. Freeing you from either 45 mins of furious typing or hours of banging your head against a wall.

So lets do this, First we create our own context class inheriting from DbContext (A part of Entity Framework):

    public class MenuContext : DbContext {

    public MenuContext() : base("MenuContext") { 

}

The base referenced here specifies the name in your web.config file of your connection string which we will set up momentarily. Alternatively you can specify your connection string in place of the name.

    public DbSet<MenuViewModel> Menus { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder) {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

    }

}

The Initializer class we will set up next populates the database if the database does not already exist at the connection string location.

    class MenuInitializer : CreateDatabaseIfNotExists<MenuContext> {

    protected override void Seed(MenuContext context) {

        // This sets up your database to populate with whatever you type into this method.
    }
}

Now you are able to go to your solution explorer, right click on your controllers folder and click add - Controller. Then specify that you want MVC 5 Controller with views, using Entity Framework. Click - Add.

A dialog will show, specify your view model, the context class we set up, make sure "Generate Views" is selected, name your controller and BAM! Build your project and view your auto created everything!