3

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" name=@String.Format("DishList[{0}].Id", i) value=@Model.DishList.ElementAt(i).Id />
            <input type="hidden" name=@String.Format("DishList[{0}].Name", i) value=@Model.DishList.ElementAt(i).Name />
            <input type="checkbox" name=@String.Format("DishList[{0}].value", i) />
            <input type="hidden" name=@String.Format("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.

Kahn Kah
  • 1,389
  • 7
  • 24
  • 49
  • The first thing you need to understand is [what is a view model](http://stackoverflow.com/questions/11064316/what-is-viewmodel-in-mvc)/ A view model should never contain properties that are data model. You `MenuViewModel` needs to contain the 2 properties of `Menu` that you want to display in the view plus a property `IList` where `DishViewModel` will contain properties for the dish ID, Name and a `bool` IsSelected` property. –  Jun 02 '16 at 01:30
  • Thanks for your answer Stephen, but you mention that `Menu` needs 2 properties in the ViewModel? Isn't just a new object of `Menu` enough? And is it common that a ViewModel interacts with another one? Or is this just a exception, because I haven't seen much of this in the examples I saw here or on the internet. ** Last question. You mention that a view model should never contain properties that are data model, but isn't that in my case obligated since I need to show all the dishes (from the database) that could possibly added to the new `Menu` I'm creating? – Kahn Kah Jun 02 '16 at 01:32
  • Suggest you start by looking at [this answer](http://stackoverflow.com/questions/29542107/pass-list-of-checkboxes-into-view-and-pull-out-ienumerable/29554416#29554416) for the basic structure of what your view models, controller code and view should be. –  Jun 02 '16 at 01:32
  • No, A view model never contains properties which are data models. In the POST method you map the properties of the view model to a new (if its a Create method) or an existing instance of the data model and then save the data model. –  Jun 02 '16 at 01:37
  • Have a look at the link, and if you still not understanding the basic concept, let me know and I'll add an answer. –  Jun 02 '16 at 01:39
  • Okay thanks again! I will start studying the related answers. – Kahn Kah Jun 02 '16 at 01:41
  • Well I'm back again with a huge headache. With the fact that I don't even understand using 1 ViewModel (with a database), and now knowing I even have to use 2 ViewModels. What's the messed up part is that various sources are claiming I can't use data properties in my ViewModel but SOMEWHERE in the controller there needs to be ` ... = db.Dishes.ToList()`, aka retrieving values from a database. I'm actually sure that I'm skipping this ViewModel part at the exam since my teacher didn't even explain it to use and it will be only a small part on the exam. And also sorry for 'being a bad learner' :( – Kahn Kah Jun 02 '16 at 02:13
  • Give me an hour or so and I'll add an answer. But do you even have a table in your database for the 1-many relationships (e.g. `MenuDishes` that has fields `MenuId` and `DishId`)? –  Jun 02 '16 at 02:22
  • Thanks for the quick answer again but I'm afraid I won't be even understanding your provided code and on the exam the teacher will be asking it in a more psychedelic way. I do have all the 'relationship tables' (that I've gotten thanks to the Data-models above) but I really think I will never be understanding this part after nearly being occupied by it for days. But I do want to thank you for all the quick answers! I will be focusing more on the different aspects of this strange web-programming language. – Kahn Kah Jun 02 '16 at 02:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/113574/discussion-between-stephen-muecke-and-kahn-kah). –  Jun 02 '16 at 02:31

2 Answers2

3

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!

Andy Mason
  • 53
  • 6
  • Thank you very much for you answer. But doesn't a Entity Framework Object need a Key (primary key) in order to be created? And I thought that ViewModels and Entity Framework had nothing to do with each other? – Kahn Kah Jun 02 '16 at 00:10
  • They do have nothing to do with each other, however, Entity framework uses the data annotations which you already have on your view model to decide what the data types will be in the database. – Andy Mason Jun 02 '16 at 01:16
  • Hmm okay I guess i'll try to play a bit with your answer approach then. thanks again! – Kahn Kah Jun 02 '16 at 01:24
3

Firstly a view model should not contain properties which are data models. It should contains only properties which you display/edit in the view, and I recommend you read What is ViewModel in MVC?.

Based in the image of the form you have shown, your view models needs to be (display and validation attributes omitted for simplicity)

public class MenuVM
{
    public int? ID { get; set; } // included so this can be used for editing as well as creating
    public string Name { get; set; }
    public int AmountPersons { get; set; }
    public List<DishVM> Dishes { get; set; }
}
public class DishVM
{
    public int ID { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
}

and the controller GET method

public ActionResult CreateMenu( )
{
    // Get all the dishes from the database
    var dishes = db.Dishes; // modify to suit
    // Initialize the view model
    var model = new MenuVM()
    {
        Dishes = dishes.Select(x => new DishVM()
        {
            ID = x.Id,
            Name = x.Name
        }).ToList()
    };
    return View(model);
}

Then in the view (LabelFor() and ValidationFor() methods omitted for simplicity)

@model MenuVM
@using (Html.BeginForm())
{
    @Html.TextBoxFor(m => m.Name)
    @Html.TextBoxFor(m => m.AmountPersons )
    for(int i = 0; i < Model.Dishes.Count; i++)
    {
        <div>
            @Html.HiddenFor(m => m.Dishes[i].ID)
            @Html.HiddenFor(m => m.Dishes[i].Name)
            @Html.CheckBoxFor(m => m.Dishes[i].IsSelected)
            @Html.LabelFor(m => m.Dishes[i].IsSelected, Model.Dishes[i].Name)
        </div>
    }
    <input type="submit" value="Create" />
}

And finally the POST method will be

public ActionResult CreateMenu(MenuVM model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    // Initialize and save the Menu
    Menu menu = new Menu()
    {
        Name = model.Name,
        AmountPersons = model.AmountPersons
    };
    db.Menus.Add(menu);
    db.SaveChanges(); // you now have the ID of the new menu
    // Save the dishes associated with the menu
    IEnumerable<int> selectedDishes = model.Dishes.Where(x => x.IsSelected).Select(x => x.ID);
    foreach(int id in selectedDishes)
    {
        // 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);
    }
    db.SaveChanges(); // save the selected dishes
    return RedirectToAction(...); // redirect somewhere
}

Side note: Remove the [NotMapped] public bool Checked { get; set; } property from your data model.

Community
  • 1
  • 1
  • Thanks for the answer Stephen! I have re-edited my question. I'm hoping I could get more clarifications about this subject. – Kahn Kah Jun 02 '16 at 20:32
  • 1
    @KahnKah, SO is not a forum or discussion site. Its one question at a time, and if you have another question, then ask another question (and I suggest you roll back your edits because it will now attract down votes over time). –  Jun 04 '16 at 04:45