7

We are making a marketplace like https://www.etsy.com/. And we have a problem in categorising the listings. We want to categories the item in the Listing in 3 levels, f.ex it has to be categories in this order:

 Category 1 
    Sub Category 1.1
       Sub Category 1.1.1

One of the important thing is that when you choose a category, f.ex. Electronics, then in the subcategory you can only see stuff like pc, smartphone, tv etc.

This is what we have now

 public class Listing
{
    public int ListingId { get; set; }
    public String Name { get; set; }

    public int Subcategory2Id { get; set; }
    public virtual Subcategory2 Subcategory2 { get; set; }
}



public class Category
{
    public int CategoryId { get; set; }
    public String CategoryName { get; set; }

    public virtual ICollection<Subcategory1> Subcategory1s { get; set; }
}



public class Subcategory1
{
    public int Subcategory1Id { get; set; }
    public String Subcategory1Name { get; set; }

    public int CategoryId { get; set; }
    public virtual Category Categories { get; set; }

    public virtual ICollection<Subcategory2> Subcategory2s { get; set; }
}



public class Subcategory2
{
    public int Subcategory2Id { get; set; }
    public String Subcategory2Name { get; set; }

    public int Subcategory1Id { get; set; }
    public virtual Subcategory1 Subcategory1s { get; set; }

    public virtual ICollection<Listing> Listings { get; set; }
}

and in the IdentityModels-ApplicationDbContext we have

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public DbSet<Listing> Listings { get; set; }
    public DbSet<Category> Categories { get; set; }
    public DbSet<Subcategory1> Subcategory1s { get; set; }
    public DbSet<Subcategory2> Subcategory2s { get; set; }

    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

The thing is we are not sure this is he right way to do it, and we dont know how to proceed, the meaning is that when you create a listing you have to have 3 drop down list, where you choose the respective categorys. So first you choose your category, and then you are able to select the subcategory 1 etc...

Endbo
  • 117
  • 2
  • 2
  • 11
  • In case you are using SQL Server, then you should try the HIERARCHYID DataType: https://msdn.microsoft.com/en-us/library/bb677213.aspx – Hackerman Oct 29 '15 at 14:08
  • @Hackerman: Unfortunately, Entity Framework does not support hierarchyid at this time. Although, the OP did not explicitly mention EF or tag the question, the presence of the `IdentityDbContext` means EF is in use. – Chris Pratt Oct 29 '15 at 14:57
  • @ChrisPratt, yes, but i mean, why do not let the sql server to handle all that logic and use EF with stored procedures just for this scenario? – Hackerman Oct 29 '15 at 15:02
  • Because as long as EF is responsible for the entity, it can't create a table that works properly with SQL Server's hierarchy functions. There's unfortunately no halfway. You either use EF or you completely detach the table from EF and do all your CRUD and querying with straight SQL. – Chris Pratt Oct 29 '15 at 15:06

3 Answers3

9

You should absolutely not have multiple category/subcategory entities. A category can have a parent and it can have children, but they're all "categories".

public class Category
{
    public int Id { get; set; }

    public int? ParentId { get; set; }
    public virtual Category Parent { get; set; }

    public virtual ICollection<Category> Children { get; set; }
}

ParentId is nullable, because top-level categories have no parent.

Entity Framework tends to get confused by self-referencing relationships, so you might need a little fluent config to help it out:

public class Category
{
    // properties

    public class Mapping : EntityTypeConfiguration<Category>
    {
        public class Mapping()
        {
            HasOptional(m => m.Parent).WithMany(m => m.Children);
        }
    }
}

Then, in your context:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Configurations.Add(new Category.Mapping());
}

With all that in place, when you're in your "Electronics" category, you'd show the subcategories simply by iterating over it's Children property.

UPDATE

If you need the full hierarchy rather than just one level at a time, you have a couple of options. First, you can just include multiple levels when querying:

db.Categories.Include("Children.Children");

That's not highly efficient, though, and I definitely would not recommend delving much deeper than tertiary children. However, that's all you're asking for, so this is still a workable method.

Second, you can create a stored procedure to walk the hierarchical structure for you. It's a little more complex, but with a combination of WITH and UNION ALL, you can create a flat representation of the hierarchy and then recursively use LINQ's GroupBy to work it back into a hierarchical structure.

There's a final potential third option in @Hackerman's recommendation of using HIERARCHYID, but unfortunately, to do that, you must completely remove Category from your EF context, which also means removing any direct relationships to it, as well. To relate a product to a category, you could only store the id (not as a foreign key), and then use that id to manually lookup the category in a second step. Unfortunately, while this solution makes dealing the the hierarchy easier, it makes doing everything else more difficult. Either way, it's up to you, though.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Thanks for your answer. We are beginners at this framwork, and have some questions. 1. The public class Mapping : EntityTypeConfiguration ---- class has to be in the Category class ? 2. Did you mean public class Mapping() or public Mapping() 3. Where did you put the OnModelCreating method ? Is it in the ApplicationDbContext : IdentityDbContext---class ?? 4. The db.Categories.Include("Children.Children"); -- is put in the controller ? – Endbo Oct 30 '15 at 10:01
  • The configuration doesn't *have* to be with the entity, but it's a good practice to follow as it keeps the entity and configuration for that entity in the same place. Yes, it's supposed to be a class. A class defined within a class, is essentially like a static method, in the sense that you don't have to new up the containing class to get at it. It just provides a namespace really, and keeps everything together. Yes, the OnModelCreating method goes in your context, and the Include call goes into the controller. – Chris Pratt Oct 30 '15 at 13:00
0

This seems to be a correct solution.

You can also use only one class (one DB table etc.) for all categories. Your "Category" class/table must then contain the reference of the parent category (nullable). That allows to make generic treatments for all categories.

For example, when the user create an item, you can display a dropdown list for the main category. If the user selects a category which contains other category, an other dropdownlist is displayed with the child categories, etc...

Paul DS
  • 859
  • 7
  • 13
  • Thanks. This is exactly the thing we are trying to make (3 dropdowns, based on category/subcategory). Do you have any code snippets/links/good advice how to diaplay this ? I think now we have a good idea how to make the models and save it correctly in the database. But how to display it in the views is a problem. – Endbo Oct 30 '15 at 10:07
  • The simplest solution may be to create a unique dropdownlist with all elements (display as a tree). If you want to implement the 3 dropdown lists solution, you can create 3 "category" properties in your product model, generate 3 drodpownlists, but display only the first one. Then you bind a javascript method on the first dropdown change event, which launch an AJAX request for loading the content of the second dropdown list, etc. You can optimize the code to have only one controller method, and one javascript method : http://jsfiddle.net/vwh32hvj/ (not working, just to explain my solution). – Paul DS Oct 30 '15 at 10:35
-2

I giving here a example for category and subcategory with image upload.

 public class ProductController : Controller
{
    ApplicationDbContext db = new ApplicationDbContext();
    // GET: Product
    public ActionResult Index()
    {

        return View();
    }

    public ActionResult insert(int? id)
    {
        ViewBag.categoryList = db.Product.Where(x => x.CategoryId == 0).Select(x => new SelectListItem { Text = x.name, Value = x.Id.ToString() }).ToList();
        var product = db.Product.Where(x => x.Id == id).Select(x => x).FirstOrDefault();
        if (product == null) { product = new Product(); product.CategoryId = 0; }
        return View(product);
    }
    [HttpPost]
    public ActionResult insert(Product model)
    {

        if (Request.Files.Count > 0)
            if (Request.Files["fileupload"].ContentLength > 0)
            {
                var fileupload = Request.Files[0];
                var fileName = Path.GetFileName(fileupload.FileName);
                model.Imagename = fileName;
                model.ImageUrl = DateTime.Now.Ticks.ToString() + "." + fileName.Split('.')[1];

                string baseurl = Server.MapPath("/") + "Images/" + model.ImageUrl;
                fileupload.SaveAs(baseurl);
            }
        if (model.Id > 0)
        {
            var productEntity = db.Product.Where(x => x.Id == model.Id).Select(x => x).FirstOrDefault();
            if (model.Imagename != null)
                productEntity.Imagename = model.Imagename;
            if (model.ImageUrl != null)
                productEntity.ImageUrl = model.ImageUrl;
            productEntity.name = model.name;
            productEntity.CategoryId = model.CategoryId;
        }
        else
        {
            db.Product.Add(model);
        }
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    public ActionResult ProductList()
    {
        var product = db.Product.Where(x => x.Id > 0).Select(x => x).ToList();
        return View(product);
    }

    public ActionResult getsubcategory(int id)
    {
        var list = db.Product.Where(x => x.CategoryId == id)
                     .Select(x => new SelectListItem { Text = x.name, Value = x.Id.ToString() }).ToList();
        return Json(list, JsonRequestBehavior.AllowGet);
    }

}

This upper controller for insert update record.

Below html code :

                @model WebApplication1.Models.Product

            @{
                ViewBag.Title = "insert";
                Layout = "~/Views/Shared/_Layout.cshtml";
            }

            <h2>insert</h2>

            @using (Html.BeginForm("insert","product", FormMethod.Post,new { enctype = "multipart/form-data" }))
            {
                @Html.AntiForgeryToken()

                <div class="form-horizontal">
                    <h4>Product</h4>
                    <hr />
                    @Html.HiddenFor(x=>x.Id)
                    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
                    <div class="form-group">
                        <label class="control-label col-md-2">SubCategory</label>
                        <div class="col-md-10">
                            @Html.DropDownList("SubCategory", new SelectList(ViewBag.categoryList, "Value", "Text", Model.CategoryId), "-Select-", new { @onchange = "categoryselect()", htmlAttributes = new { @class = "form-control" } })
                            @Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(model => model.CategoryId, htmlAttributes: new { @class = "control-label col-md-2" })
                        <div class="col-md-10">

                            @Html.DropDownListFor(model => model.CategoryId, new SelectList(ViewBag.categoryList, "Value", "Text", Model.CategoryId),"-Select-", new { htmlAttributes = new { @class = "form-control" } })
                            @Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
                        </div>
                    </div>


                    <div class="form-group">
                        @Html.LabelFor(model => model.name, htmlAttributes: new { @class = "control-label col-md-2" })
                        <div class="col-md-10">
                            @Html.TextBoxFor(model => model.name, new { htmlAttributes = new { @class = "form-control" } })
                            @Html.ValidationMessageFor(model => model.name, "", new { @class = "text-danger" })
                        </div>
                    </div>


                    <div class="form-group">
                        @Html.LabelFor(model => model.Imagename, htmlAttributes: new { @class = "control-label col-md-2" })
                        <div class="col-md-10">
                            <input id="Imagename" name="fileupload" type="file" class = "form-control"  />
                            @*@Html.(model => model.Imagename, new { htmlAttributes = new { @class = "form-control" } })*@
                            @Html.ValidationMessageFor(model => model.Imagename, "", new { @class = "text-danger" })
                        </div>
                    </div>

                    <div class="form-group">
                        <div class="col-md-offset-2 col-md-10">
                            <input type="submit" value="Create" class="btn btn-default" />
                        </div>
                    </div>
                </div>
            }

            <div>
                @Html.ActionLink("Back to List", "Index")
            </div>
            <script>

                function categoryselect () {

                    var d = $("#SubCategory option:selected").val();
                    $.ajax({
                        url: "/product/getsubcategory?id="+d
                    , type: "get"
                        , success: function (data) {

                            // alert(data)
                            $("#CategoryId").html('<option value="">-select- </option>');
                            for(var i=0;i<data.length;i++)
                                $("#CategoryId").append('<option value="' + data[i].Value + '">' + data[i].Text + '</option>')
                        }
                    })

                    } 
            </script>

model:

                namespace WebApplication1.Models
            {
                public class Product
                {
                    [Key]
                    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
                    public int Id { get; set; }
                    public int CategoryId { get; set; }
                    public string name { get; set; }
                    public string ImageUrl { get; set; }
                    public string Imagename { get; set; }
                }

                public class Category
                {
                    [Key]
                    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
                    public int Id { get; set; }
                    public int PrentId { get; set; }
                    public string name { get; set; }

                }
            }

Index Page:

                @{
                ViewBag.Title = "Index";
                Layout = "~/Views/Shared/_Layout.cshtml";
            }

            <h2>Index</h2>

            <div id="productList">

            </div>
            <script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>

            <script>
                $(document).ready(function () {
                    $.ajax({
                        url:"/product/productlist"
                        , type: "GET"
                        ,success:function(data)
                        {
                            $("#productList").html(data)
                        }
                    })

                })
            </script>

List Page:

                @model IEnumerable<WebApplication1.Models.Product>

            <p>
                @Html.ActionLink("Create New", "Insert")
            </p>
            <table class="table">
                <tr>
                    <th>
                        @Html.DisplayNameFor(model => model.CategoryId)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.name)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.ImageUrl)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.Imagename)
                    </th>
                    <th></th>
                </tr>

            @foreach (var item in Model) {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => item.CategoryId)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.name)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.ImageUrl)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => item.Imagename)
                    </td>
                    <td>
                        @Html.ActionLink("Edit", "insert", new { id=item.Id }) 

                    </td>
                </tr>
            }

            </table>
Rajesh Kumar
  • 602
  • 6
  • 20