1

I recently got into Asp.Net MVC development (using MVC 6 and Asp 5).

I am trying to create a relationship between 2 model classes. A Product and a Category. The Product belongs to a category, and thus a category can contain many products.

This is my model code:

public class Category
{
    public int id { get; set; }
    public string name { get; set; }
    public virtual ICollection<Product> product { get; set; }
}

public class Product
{
    public int id { get; set; }
    public string name { get; set; }
    public decimal price { get; set; }
    public virtual Category category { get; set; }
}

Then i created 2 controllers from the models using entity framework with views and then i perform the migration. I then have CRUD operations for both which works fine, but the problem is that i cant assign a product to a category. I can only set the name and price of a product using the generated views.

Hope someone can help. Obviously i'm expecting some sort of a drop-down that shows all categories when creating a product, so i can link them together.

Thanks.

* EDIT * ProductsController - create method:

// POST: Products/Create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create(Product product)
    {
        if (ModelState.IsValid)
        {
            _context.Product.Add(product);
            _context.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(product);
    }

Again, please note that i am not trying to add the functionality manually. I am trying to get the whole scaffolding / migration procedure to work with object relationships, so it can generate it for me automatically and speed up my development process.

* EDIT 2 * The Product model was changed to:

public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public decimal ProductPrice { get; set; }

    [ForeignKey("Category")]
    public int CategoryID { get; set; }
    public virtual Category category { get; set; }
}

This will now create a dropdown list of categories when i'm creating a product. HOWEVER - the list is empty even if i create a category, so something is still missing? Maybe the category model needs additional information? This is how it displays the list in the GET create:

// GET: Products/Create
    public IActionResult Create()
    {
        ViewData["CategoryID"] = new SelectList(_context.Category, "id", "category");
        return View();
    }

Which is just an empty list... Almost there, but not quite yet.

9
  • "i cant assign a product to a category" How did you try to assign it? Include that code too please Commented Feb 15, 2016 at 9:54
  • 1
    do your product table has a CategoryId column? Commented Feb 15, 2016 at 9:56
  • 2
    As said janina, uses CategoryId as a foreign key. And please put a uppercase letter to your properties, these are methods not variables. Commented Feb 15, 2016 at 9:59
  • The code for CRUD operations is generated using Scaffolding when the Controller is created. My point is, that in this scaffolding process, i would think it would create the way to associate products to categories automatically. If it doesn't, i fail to see the real use of the scaffolding procedure. Commented Feb 15, 2016 at 10:31
  • 1
    When everything is correctly defined, the scaffolding should create a dropdown list for your category in your create product view, so yeah you are right you are having a hard time to believe this. Commented Feb 15, 2016 at 10:48

3 Answers 3

1

Your product class should be something like below

public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public decimal ProductPrice { get; set; }

[ForeignKey("Category")]
public int CategoryID {get;set;}
public virtual Category category { get; set; }
}

Now you will have a column in your Product table which would store which category it would belong to.

To get a dropdown with all available Categories to enable selection , you could try the following

 ViewBag.CategoryID= new SelectList(db.Category, "CategoryID", "CategoryName");

And in the View you could use it as follows

@Html.DropDownList("CategoryID", null, htmlAttributes: new { @class = "form-control" })
Sign up to request clarification or add additional context in comments.

7 Comments

This got me closer! It now actually does create a dropdown list for the categories when i'm creating a new product. HOWEVER - the dropdown list is empty? See my edit 2 of post for more info.
ViewData["CategoryID"] = new SelectList(_context.Category, "id", "name"); . You dod not have anything with the name category in the Category mdoel class.
I tried that, still not working. Kinda feels weird that the scaffolding would do it like this: ViewData["CategoryId"] = new SelectList(_context.Category, "CategoryId", "Category"); Makes me feel something is still wrong with my model relationships.. just don't know what.
OK. i too find it weird. Let me go through the codes that I have. Have you tried ViewBag? The one in the answer?
ViewBag is the ´"old way" of doing things as far as i understand, so that won't make a difference. I just need it to auto-generate it, and it should do it if i follow the naming convention, which i clearly must be getting wrong. I just don't see where.
|
0

Product needs to contain CategoryId (foreign key). Then you can tell EF how relationship looks like (even though EF will probably figure that out by itself if namings are done correct):

public class Category
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; } 
}

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public int CategoryId { get; set; }
    public virtual Category Category { get; set; }
}

public class StoreContext : DbContext  
{ 
    public DbSet<Category> Categories{ get; set; } 
    public DbSet<Product> Products { get; set; } 
    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
        // optionally, even EF can follow convention and detect 1-m relationship between product and category. 
        modelBuilder.Entity<Product>().HasRequired(p => p.Category) 
            .WithMany(c => c.Products) 
            .HasForeignKey(p => p.CategoryId);
    }
}

When creating a new product, set CategoryId property with correct value, that will connect it to the category.

Your ViewModel should look something like this (can contain whole Product class or individual properties that you have to map later):

public class ProductViewModel
{
    public ProductViewModel() 
    { 
        EditableProduct = new Product();
    }
    public List<SelectListItem> Categories { get; set; }
    public Product EditableProduct { get; set; }
}

And ProductController will fill all the categories and prepare the view model:

public ActionResult Edit(int? id)
{
    var model = new ProductViewModel();
    model.Categories = dbContext.Categories.Select(x => new SelectListItem() {Text = x.Name, Value = x.Id.ToString()}).ToList();

    // check if product even exists
    model.EditableProduct = id.HasValue ? dbContext.Products.Find(id.Value) : new Product();

    return View(model);
}

[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(ProductViewModel model)
{
    // check validation
    Product product;
    if (model.EditableProduct.Id > 0)
        product = dbContext.Products.Find(model.EditableProduct.Id);
    else
    {
        product = new Product();
        dbContext.Products.Add(product);
    }

    product.CategoryId = model.EditableProduct.CategoryId;
    product.Name = model.EditableProduct.Name;

    // add try/catch 
    dbContext.SaveChanges();

    return RedirectToAction("Edit", new {id = product.Id});
}

Razor view can then render categories:

@Html.DropDownListFor(x => x.EditableProduct.CategoryId, Model.Categories)

This is the general pattern you can follow with all look-up lists and one-to-many relationships.

Word of advice: don't "leak" your database/domain model into view. Put name, id, etc properties to View Model class, and map them into EF model when request is received and validation is done on them. Try to avoid putting Product object into view model, like I did in this example!

7 Comments

Not quite what i am looking for - as you say so yourself, EF should do this automatically, and that is what i am after. So the issue must be in my models and their relationships, but not sure where. Read my Edit, the answer is closer but not there yet.
I gave a full example now. Regarding EF and mapping; EF will know how to create relations properly only if you follow naming convention. In your question you didn't follow the convention, so in that case you have to help EF with additional mapping information. In my experience, it's ok to rely on default convention and automatic mapping only for simple models and schema, but it's important to know how to configure them manually!
Regarding ViewModels - that's one of the most important element of MVC architecture. Don't mix domain/database model with your presentation model - if app gets just little bit more complicated they will start to diverge and you'll have a lot of problems in transferring your EF model to view and back. ViewModel can be aggregation of several EF models, can contain additional data needed for view, and that's what you'll need in most cases. Avoid ViewBags unless absolutely necessary. Use AutoMapper framework to map from viewmodel to EF model and back.
But this just seems to go way ahead of the tutorial i read from Microsoft? They simply say that you can create your model, create a controller which will be scaffolded with views + methods for all CRUD operations and then you can simply perform a migration and you are done. This seems like a lot of extra steps you take manually?
Its just a tutorial for MVC basics, and I would say, intentionally bad code. In real world, that would be anti-pattern, specially if you building anything more complex, that has more than 2 table in db and 2 screens. Im showing you how to do it properly, so take it as a reference or a good practice, and see if it fits your requirements.
|
0

This seems to be a bug in MVC Scaffolding, you can find more details here: https://github.com/aspnet/Scaffolding/issues/149

Try something like this in your view:

@{ var categories = (IEnumerable<SelectListItem>)ViewData["CategoryID"]; }
<select asp-for="CategoryID" asp-items="categories" class="form-control"></select>

This solved similar issue for me.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.