I want to make a tree structured product category view. I have found this article by Ole Michelsen from 2011, which I think looks promising. The article describes a method similar to what I almost had in mind, using Razor to recursively render the tree structure.
My database table is almost identical to the one described in the article:
[ProductCategory]
Id
ParentId
Title
I'm not sure what my view model should look like. Should it be an IEnumerable of ProductCategory, or should it be an instance of ProductCategory and have an IEnumerable of ProductCategories in it?
Something like this ...
public class ViewModelProductCategory
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public int NumOfProducts { get; set; }
}
... or more like this?
public class ViewModelProductCategory
{
public IEnumerable<ProductCategory> ProductCategory { get; set; }
//public int NumOfProducts { get; set; }//<--Not too sure about this property
public IEnumerable<Product> Products { get; set; }
}
If the second one is closer to the "correct" one, how can I have SortOrder and NumOfProducts (how many products are in the current category)?
And the thing I'm most confused about, is how the razor rendering comes together. How do I send the viewmodel to the recursive partial view, and how does the partial view know which item to render?
I have some razor-code, but I can guarantee that posting it will NOT make anything clearer as of what I'm trying to achieve.
This pseudo-code is perhaps more describing:
_recursivePartial.cshtml
#model [my unknown viewmodel goes here, is it IEnumerable or not?]
<ul>
foreach item in model
{
<li>#Html.Partial("_recursivePartial.cshtml", [what goes here?])
// 1: How does _recursivePartial.cshtml know where in the tree it is?
// 2: How does this structure relate to the data structure in the DB?
}
</ul>
I’m not sure there is a “correct” way of doing things, but I like to keep my database models different to the view models. I often use AutoMapper to get the data between models.
In your situation I’d probably have two View Models:
public class ProductCategoryItemModel
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Title { get; set; }
public int SortOrder { get; set; }
public int NumOfProducts { get; set; }
}
and
public class ProductCategoriesModel
{
public List<ProductCategoryItemModel> Categories { get; set; }
}
I always convert ToList() rather than passing an IEnumerable to views, but again I’m not sure that’s the “correct” way.
Related
I am creating some view models for my ASP MVC web app.
I created "code first" models for database. Is it a good way to derive view models from database models?
Example database model:
public class Project
{
[Key]
public int Id { get; set; }
public int? CustomerId { get; set; }
public int TypeId { get; set; }
public string Number { get; set; }
public string Name { get; set; }
}
View model:
public class ViewModelProject : Project
{
[NotMapped]
public DateTime? Start { get; set; }
[NotMapped]
public DateTime? End { get; set; }
[NotMapped]
public string Manager { get; set; }
}
Is this the right way or is it completely false?
EDIT (subquestion):
I have some very simple database models like ProjectType, which only contains i.e. two properties. Should I also fragment those models in model view or can I make it that way:
Simple database model:
public class ProjectType
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public int? Code { get; set; }
}
Can I use it like so:
public class ProjectVM
{
public string Name { get; set; }
public int Number { get; set; }
public ProjectType Type { get; set; }
}
Or does it have to be fragmented like so:
public class ProjectVM
{
public string Name { get; set; }
public int Number { get; set; }
public string Type { get; set; }
public int TypeCode { get; set; }
}
I would not recommend doing it this way. I (and many others) have tried it and it doesn't work well. You will inadvertedly run into troubles, since an MVC model has to be tailored to the view and what you get from the DB rarely fits. Sure, you can hammer it into place, but the code quickly gets messy and store-related and UI code starts to mangle together. This even shows in your example, since you have to put the NotMappedAttribute (which is related to data storage), to ViewModelProject (a class at UI level).
There are many other examples to show this problem, but an especially good one I find when you want to serialize a model object to JSON and send it to a JavaScript client. The JSON serializer takes the values of all public properties and adds them to the JSON. If you want to exclude a property, you have to mark it with a ScriptIgnoreAttribute, which you would also have to apply to the base class, which breaks separation between UI and store-related code.
The better way to go is to keep the staorage model and the MVC model separated and to map the data from one to the other (there are already pre-existing frameworks that help you with that, such as Automapper). This comes with additional advantages, for example better testability, since you are now not dependent on a specific data store to create model instances.
This is my first question on this community, I hope someone can help.
I have 3 models: Parent, child and grandchild. Resource, Parameter and Metric. I create the controlers with entity framework and it generated all CRUD operations. My problem its basically the grandchild. When I go to create a new Metric (grandchild), I have the dropdownlist of all the Parameters (child), but I want first to choose the Resource, then that will list all the parameters from that resource so I can create the metric. I'm new on asp MVC and this might be a lil bit basic question but I couldn't find anything like this. Thanks
Here are my classes just in case.
public class Resource
{
public int ResourceID { get; set; }
public string Name { get; set; }
public virtual ICollection<Parameter> Parameters { get; set; }
}
public class Parameter
{
public int ParameterID { get; set; }
public string Name { get; set; }
public int ResourceID { get; set; }
public virtual Resource Resource { get; set; }
public virtual ICollection<Metric> Metrics { get; set; }
}
public class Metric
{
public int MetricID { get; set; }
public string Name { get; set; }
public int ParameterID { get; set; }
public virtual Parameter Parameter { get; set; }
}
What you are trying to do sounds like either a linked or cascading drop down list. I did a quick search and found this link that may help you.
You will need some JavaScript that listens to the selection changed event for the first list (parent), and then updates the second list (child) with the available values. Depending on your situation, you may then need to link the third list (grandchild) to load when the second list has its selection changed as well.
Ok, I have 3 models. WorkoutViewModel has a one to many relationship with WorkoutExerciseViewModel. WorkoutExerciseViewModel has a one to many relationship with ExerciseSetViewModel. I need a dynamic “Create View”, that will allow me dynamically add Exercises to Workouts, and Sets to Exercises. I then want to save a Workout including all exercise and set records back to the database. I just need to validate that there is at least 1 exercise for the workout created and at least 1 set for the exercise created. Ultimately I just need to push a Workout View Model back to the controller with all of the populated nested IEnumberable objects present. Can anyone point me in the right direction?
public class WorkoutViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtal IEnumerable<WorkoutExerciseViewModel> WorkoutExercises { get; set;}
}
public class WorkoutExerciseViewModel
{
public int Id { get; set; }
public int WorkoutId { get; set; }
public int ExerciseId { get; set; }
public virtual ExerciseViewModel Exercise { get; set; }
public virtual IEnumerable<ExerciseSetViewModel> ExerciseSets { get; set; }
public string ExerciseFullname
{
get
{
return Exercise.Equipment.Name + " " + Exercise.Name;
}
}
}
public class ExerciseSetViewModel
{
public int Id { get; set; }
public int WorkoutExerciseId { get; set; }
public int Set { get; set; }
public int Reps { get; set; }
public int Weight { get; set; }
public string WeightValueType { get; set; }
}
There's really more to this than can reasonably be discussed in a StackOverflow answer, but I'll give you enough to start with.
As far as adding new exercises and sets within those exercises go, that's just JavaScript. You'll need to have some button that the user can click to add a new one, and tie the click event on that button to a handler that will add the appropriate HTML (form fields and such) to the page. There's many different ways to go about doing that, some more difficult than others. Most likely you want to look into some JavaScript templating library or a more full stack JS library like Knockout to make things easier. The only other thing to keep in mind is the conventions the modelbinder uses to wire everything from the post body to an instance of your model. For collections, it expects fields to have name attributes in the form of CollectionPropertyName[N].PropertyBeingEdited, where N is the position within the collection. So, the name attribute for ExerciseFullName for the first exercise would be WorkoutExercises[0].ExerciseFullName.
Your post action would simply take your same main view model:
[HttpPost]
public ActionResult Create(WorkoutViewModel model)
{
...
}
As long as you follow the property naming conventions for all the fields in your form, the modelbinder will happily wire everything from the post body onto your WorkoutViewModel instance.
I have setup a couple of objects:
Product.cs
namespace Print_Solutions.Models
{
public class Product
{
public int ID { get; set; }
public string Manufacturer { get; set; }
public string Model { get; set; }
public string PartNumber { get; set; }
public int ProductCategoryID { get; set; }
public virtual ICollection<ProductCategory> ProductCategory { get; set; }
}
}
ProductCategory.cs
namespace Print_Solutions.Models
{
public class ProductCategory
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
}
The DB Context
namespace Print_Solutions.DAL
{
public class ApplicationContext : DbContext
{
public ApplicationContext() : base("DefaultConnection")
{
}
public DbSet<ProductCategory> ProductCategories { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductDetail> ProductDetails { get; set; }
public DbSet<ProductDocument> ProductDocuments { get; set; }
public DbSet<ProductImage> ProductImages { get; set; }
public DbSet<RelatedProduct> RelatedProducts { get; set; }
}
}
The Controller
namespace Print_Solutions.Controllers
{
public class DashboardController : Controller
{
private ApplicationContext db = new ApplicationContext();
public ActionResult Index()
{
ViewBag.Products = db.Products.ToList();
ViewBag.ProductCategories = db.ProductCategories.ToList();
return View();
}
}
}
The Problem
I have tried a couple of things in the view. Neither one of these seems to work. How can I access products from productcategories, and vice versa?
<ul>
#foreach(Print_Solutions.Models.ProductCategory category in #ViewBag.ProductCategories)
{
<li>#category.Name
<ul>
#foreach(var product in #category.Products)
{
<li>#product.Name</li>
}
</ul>
</li>
}
</ul>
or
<ul>
#foreach (Print_Solutions.Models.Product product in #ViewBag.Products)
{
<li>#product.Name - #product.ProductCategory.Name</li>
}
</ul>
I can access products, and product categories, but not through their related objects. What am I missing?
Your problem in the first example is that you're using the # in front of category.Products. That's going to raise a syntax error.
Your problem in the second example is that the inappropriately named ProductCategory property is actually a collection, not a single object. In other words, you can't access a property like Name directly off it. You would have to loop through this collection as well or otherwise join the values. For example, in this scenario, you'd probably just want to list all the categories the product belongs to, so you could do something like:
#string.Join(", ", product.ProductCategory.Select(m => m.Name))
Which creates an enumerable containing only the value for Name of each ProductCategory in the collection, and then joins them all together delineated by a comma and a space: Category1, Category2, Category3, ....
While I've got you, though, there's some important changes you should make:
Since these are related things, don't do two separate database queries (one for products and one for categories), instead, select one (probably the products based on your usage), and then join the other in the same query:
var products = db.Products.Include("ProductCategory").ToList();
Don't use ViewBag. Like ever. There's some times where its use may not be so bad, but until you can properly distinguish a good usage from a bad usage, it's better to just put a moratorium on the whole concept. The problem with ViewBag is that it uses dynamics; in other words, it is not evaluated at all at compile time, and either works or fails at runtime. The golden rule of software development is to always fail at compile. You want to know when you're building that something doesn't work, not when it's been deployed to your user days, weeks, months or even years after it's been developed. So, yeah, ViewBag is evil. Instead, use a strongly-typed view and pass your "model", in this case, your products into it:
Controller
var products = db.Products.Include("ProductCategory").ToList();
return View(products);
View
#model IEnumerable<Namespace.To.Product>
#foreach (var product in Model)
{
...
}
Since Product Category is a collection, you may wish to loop thru the ProductCategory List (within the Product Object) within the product loop to print them off.
Alternately, you can create a View Model which contains only what you need. (Product Name, List of ProductCategoryNames) and pass that into things.
If you intend to do a post back, you can add ProductID to this view model (or CategoryID as well for that matter) and put them into a #Html.HiddenFor() so you can reference them later as needed.
I have some hierarchal data. The Model class I use looks like this:
public class Category
{
[Key]
public int CategoryID { get; set; }
[Required]
[StringLength(64)]
public string Name { get; set; }
public int? ParentCategoryID { get; set; }
[ForeignKey("ParentCategoryID")]
public Category ParentCategory { get; set; }
[Required]
public int ListOrder { get; set; }
// left/right
public int TreeLeft { get; set; }
public int TreeRight { get; set; }
} // eo class Category
I've used the techniques outlined here to store my data, and inserting and retrieving data is not a problem.
What I would like to do, is add a Category collection to this class:
public virtual IEnumerable<Category> {get; set; }
I've used this technique in the past (learned from the Mvc tutorials), to include related tables when getting data. However, when I tried this I received an error with regard to IEnumerable<> being abstract (which is understandable, I guess the framework couldn't figure out what I want to do)...
... and indeed, being new to LINQ, I have no idea what the LINQ would look like that would give me back a collection of Category instances each of which had their children inside them.
If it's not possible I guess I can construct the list manually, use a regular LINQ query to get all the categories at a particular position (and their children) and manually populate it all.
I was wondering if LINQ could do this for me?
Thanks in advance!
If you have a self reference fk than the collection should be generated automatically when you add the table to the dbml file
And will look something like this:
[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Category_Category", Storage="Categories", ThisKey="pkCategoryID", OtherKey="ParentCategoryID")]
public EntitySet<Category> Categories
{
get
{
return this._Categories;
}
set
{
this._Categories.Assign(value);
}
}
Dont use IEnumerable<Category> but Collection<Category> . If that is your problem..