I have two classes an Entry and Paradigm. The Entry class has a ParadigmId and a Paradigm property. So in my view I have #Model.Entry.Paradigm. How do I build a DropDownListFor using the newer syntax for the Paradigm model?
// Entry Model
[Bind(Exclude = "EntryId")]
public class Entry
{
[ScaffoldColumn(false)]
public int EntryId { get; set; }
.
[Display(Name = "Type")]
public int ParadigmId { get; set; }
public virtual Paradigm Paradigm { get; set; }
}
// Paradigm Model
public class Paradigm
{
[ScaffoldColumn(false)]
public int ParadigmId { get; set; }
[Required]
public string Name { get; set; }
public List<Entry> Entries { get; set; }
}
In my view I have #Html.DropDownListFor(model => model.Entry.ParadigmId, model.Entry.Paradigm). But the model is of type Paradigm not IEnumerable. Since Paradigm is part of my class (for Entity Framework Code First) I do not need to use a separate ViewData/ViewBag that is listed in most examples.
I Googled a bit and saw individuals using Helper/Extension methods to convert a model into a SelectList. What is the best way to use DropDownListFor in my model?
#* Create View *#
<div class="editor-label">
#Html.LabelFor(model => model.Entry.ParadigmId)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Entry.ParadigmId, model.Entry.Paradigm)
#Html.ValidationMessageFor(model => model.Entry.ParadigmId)
</div>
Your link Entry.Paradigm lazy loads a single Paradigm, the one referenced by the foreign key. It does not load all the Paradigm's in the database.
If you want to have a dropdown list of all the paradigms, bound to the selected one. Then you will need a separate ViewBag or Model property that contains a list of the them all.
I've been using:
public abstract class DropdownVm
{
/// <summary>
/// Set up a dropdown with the indicated values
/// </summary>
/// <param name="value">the current value, for determining selection</param>
/// <param name="options">list of options to display</param>
/// <param name="prependLabelAndValues">list of alternating label/value entries to insert to the beginning of the list</param>
public List<SelectListItem> SetDropdown<T>(T value, IEnumerable<KeyValuePair<T, string>> options, params object[] prependLabelAndValues)
{
var dropdown = options.Select(o => new SelectListItem { Selected = Equals(o.Key, value), Value = o.Key.ToString(), Text = o.Value }).ToList();
// insert prepend objects
for (int i = 0; i < prependLabelAndValues.Length; i += 2)
{
dropdown.Insert(0, new SelectListItem { Text = prependLabelAndValues[i].ToString(), Value = prependLabelAndValues[i + 1].ToString() });
}
return dropdown;
}
}
/// <summary>
/// ViewModel with a single dropdown representing a "single" value
/// </summary>
/// <typeparam name="T">the represented value type</typeparam>
public class DropdownVm<T> : DropdownVm
{
/// <summary>
/// Flag to set when this instance is a nested property, so you can determine in the view if `!ModelState.IsValid()`
/// </summary>
public virtual bool HasModelErrors { get; set; }
/// <summary>
/// The user input
/// </summary>
public virtual T Input { get; set; }
/// <summary>
/// Dropdown values to select <see cref="Input"/>
/// </summary>
public virtual List<SelectListItem> Dropdown { get; set; }
/// <summary>
/// Set up <see cref="Dropdown"/> with the indicated values
/// </summary>
/// <param name="availableOptions">list of options to display</param>
/// <param name="prependLabelAndValues">list of alternating label/value entries to insert to the beginning of the list</param>
public virtual void SetDropdown(IEnumerable<KeyValuePair<T, string>> availableOptions, params object[] prependLabelAndValues)
{
this.Dropdown = SetDropdown(this.Input, availableOptions, prependLabelAndValues);
}
public override string ToString()
{
return Equals(Input, default(T)) ? string.Empty : Input.ToString();
}
}
Which you create with:
var vm = new DropdownVm<string>();
vm.SetDropdown(new Dictionary<string, string> {
{ "option1", "Label 1" },
{ "option2", "Label 2" },
}, "(Choose a Value)", string.Empty);
or, more specifically in your case:
var models = yourDataProvider.GetParadigms(); // list of Paradigm
var vm = new DropdownVm<int>();
vm.SetDropdown(
models.ToDictionary(m => m.ParadigmId, m => m.Name),
"(Choose a Value)", string.Empty
);
And render in the view with:
<div class="field">
#Html.LabelFor(m => m.Input, "Choose")
#Html.DropDownListFor(m => m.Input, Model.Dropdown)
#Html.ValidationMessageFor(m => m.Input)
</div>
Related
My Model
public class FlightBooking
{
public int Id { get; set; }
public ICollection<FlightPassenger> Passengers { get; set; }
public DateTime DateJourney { get; set; }
public virtual City FromCity { get; set; }
public virtual City ToCity { get; set; }
}
public class FlightPassenger
{
public int FlightBookingId { get; set; }
public FlightBooking FlightBooking { get; set; }
public int CustomerId { get; set; }
public Customer Passenger { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public DateTime BirthDate { get; set; }
public ICollection<FlightPassenger> FlightPassengers { get; set; }
}
And in the OnModelCreating I have added
modelBuilder.Entity<FlightPassenger>().HasKey(x => new { x.FlightBookingId, x.CustomerId });
This creates the 3 tables in the database. Customer, FlightBooking and FlightPassenger. All this is fine to represent the many to many relationship in EF7. Now I am trying to take this input from the user.
My view
<select asp-for="Passengers" asp-items="Enumerable.Empty<SelectListItem>()" class="form-control customer"></select>
I am getting the data properly using Ajax and able to select the multiple values in the dropdown. But in the controller no value is passed in Passengers and its count is 0. I checked for the value in the dropdown before posting and it shows ids of the selected customers with comma. I know Passengers is not an integer array but adding an integer array to the model gives another error, so I was thinking there has to be another way. I did a small hack to by adding a string to my view model and before posting adding this integer array to the string. This string has all the values (comma sep) in the controller. But I am sure there should be a better way. Any guidance on getting this value from the view and eventually storing in the database would be great.
In my current project I have a lot of many-to-many relationships. As far as I know EF Core does not yet support many-to-many so I assume it has to be done manually. I generalized the solution.
As I'm new to EF/MVC feedback is welcome:
First I created a JoinContainer to hold the necessary data for the many-to-many entity.
public class SimpleJoinContainerViewModel
{
public int[] SelectedIds { get; set; }
public IEnumerable<SelectListItem> SelectListItems { get; set; }
// keeping track of the previously selected items
public string PreviousSelectedHidden { get; set; }
public int[] PreviousSelectedIds
{
get
{
// if somebody plays around with the hidden field containing the ints the standard exception/error page is ok:
return PreviousSelectedHidden?.Split(' ').Where(s => !string.IsNullOrEmpty(s)).Select(int.Parse).ToArray();
}
private set { PreviousSelectedHidden = value == null ? "" : string.Join(" ", value); }
}
/// <summary>
/// Call when form is loaded - not on post back
/// </summary>
/// <param name="selectListItems"></param>
/// <param name="selectedIds">Currently selected referenced ids. Get via m:n/join-table</param>
public void Load(IEnumerable<SelectListItem> selectListItems, IEnumerable<int> selectedIds)
{
SelectListItems = selectListItems;
SelectedIds = selectedIds?.ToArray();
PreviousSelectedIds = SelectedIds;
}
}
In the view model (of FlightBooking):
[Display(Name = "Passengers")]
public SimpleJoinContainerViewModel PassengersJoinContainer { get; set; } = new SimpleJoinContainerViewModel();
In the GET action I use the Load() method to fill the Container with the data:
viewModel.PassengerJoinContainer.Load(
DbContext.Customers
.Select(s => new SelectListItem
{
Text = s.LastName,
Value = s.Id.ToString()
}),
flightBookingEntity?.Passengers?.Select(p => p.CustomerId));
In the view I use the properties of the JoinContainer:
<div class="form-group">
<label asp-for="PassengersJoinContainer" class="col-sm-3 control-label"></label>
<div class="col-sm-9">
<div class="nx-selectize">
#Html.ListBoxFor(m => m.PassengersJoinContainer.SelectedIds, Model.PassengersJoinContainer.SelectListItems)
</div>
#Html.HiddenFor(m => m.PassengersJoinContainer.PreviousSelectedHidden)
<span asp-validation-for="PassengersJoinContainer" class="text-danger"></span>
</div>
</div>
Then I have a generalized Update class/method.
public class SimpleJoinUpdater<T> where T : class, new()
{
private DbContext DbContext { get; set; }
private DbSet<T> JoinDbSet { get; set; }
private Expression<Func<T, int>> ThisJoinIdColumn { get; set; }
private Expression<Func<T, int>> OtherJoinIdColumn { get; set; }
private int ThisEntityId { get; set; }
private SimpleJoinContainerViewModel SimpleJoinContainer { get; set; }
/// <summary>
/// Used to update many-to-many join tables.
/// It uses a hidden field which holds the space separated ids
/// which existed when the form was loaded. They are compared
/// to the current join-entries in the database. If there are
/// differences, the method returns false.
/// Then it deletes or adds join-entries as needed.
/// Warning: this is not completely safe. A race condition
/// may occur when the update method is called concurrently
/// for the same entities. (e.g. 2 persons press the submit button at the same time.)
/// </summary>
/// <typeparam name="T">Type of the many-to-many/join entity</typeparam>
/// <param name="dbContext">DbContext</param>
/// <param name="joinDbSet">EF-context dbset for the join entity</param>
/// <param name="thisJoinIdColumn">Expression to the foreign key (Id/int) which points to the current entity</param>
/// <param name="otherJoinIdColumn">Expression to the foreign key (Id/int) which points to the joined entity</param>
/// <param name="thisEntityId">Id of the current entity</param>
/// <param name="simpleJoinContainer">Holds selected ids after form post and the previous selected ids</param>
/// <returns>True if updated. False if data has been changed in the database since the form was loaded.</returns>
public SimpleJoinUpdater(
DbContext dbContext,
DbSet<T> joinDbSet,
Expression<Func<T, int>> thisJoinIdColumn,
Expression<Func<T, int>> otherJoinIdColumn,
int thisEntityId,
SimpleJoinContainerViewModel simpleJoinContainer
)
{
DbContext = dbContext;
JoinDbSet = joinDbSet;
ThisJoinIdColumn = thisJoinIdColumn;
OtherJoinIdColumn = otherJoinIdColumn;
ThisEntityId = thisEntityId;
SimpleJoinContainer = simpleJoinContainer;
}
public bool Update()
{
var previousSelectedIds = SimpleJoinContainer.PreviousSelectedIds;
// load current ids of m:n joined entities from db:
// create new boolean expression out of member-expression for Where()
// see: http://stackoverflow.com/questions/5094489/how-do-i-dynamically-create-an-expressionfuncmyclass-bool-predicate-from-ex
ParameterExpression parameterExpression = Expression.Parameter(typeof (T), "j");
var propertyName = ((MemberExpression) ThisJoinIdColumn.Body).Member.Name;
Expression propertyExpression = Expression.Property(parameterExpression, propertyName);
var value = Expression.Constant(ThisEntityId);
Expression equalExpression = Expression.Equal(propertyExpression, value);
Expression<Func<T, bool>> thisJoinIdBooleanExpression =
Expression.Lambda<Func<T, bool>>(equalExpression, parameterExpression);
var joinedDbIds = JoinDbSet
.Where(thisJoinIdBooleanExpression)
.Select(OtherJoinIdColumn).ToArray();
// check if ids previously (GET) and currently (POST) loaded from the db are still the same
if (previousSelectedIds == null)
{
if (joinedDbIds.Length > 0) return false;
}
else
{
if (joinedDbIds.Length != previousSelectedIds.Length) return false;
if (joinedDbIds.Except(previousSelectedIds).Any()) return false;
if (previousSelectedIds.Except(joinedDbIds).Any()) return false;
}
// create properties to use as setters:
var thisJoinIdProperty = (PropertyInfo) ((MemberExpression) ThisJoinIdColumn.Body).Member;
var otherJoinIdProperty = (PropertyInfo) ((MemberExpression) OtherJoinIdColumn.Body).Member;
// remove:
if (joinedDbIds.Length > 0)
{
DbContext.RemoveRange(joinedDbIds.Except(SimpleJoinContainer.SelectedIds).Select(id =>
{
var e = new T();
thisJoinIdProperty.SetValue(e, ThisEntityId);
otherJoinIdProperty.SetValue(e, id);
return e;
}));
}
// add:
if (SimpleJoinContainer.SelectedIds?.Length > 0)
{
var toAddIds = SimpleJoinContainer.SelectedIds.Except(joinedDbIds).ToList();
if (toAddIds.Count > 0)
{
DbContext.AddRange(SimpleJoinContainer.SelectedIds.Except(joinedDbIds).Select(id =>
{
var e = new T();
thisJoinIdProperty.SetValue(e, ThisEntityId);
otherJoinIdProperty.SetValue(e, id);
return e;
}));
}
}
return true;
}
}
In the Post action I call this class/method:
var flightPassengersUpdater = new SimpleJoinUpdater<FlightPassenger>(
DbContext,
DbContext.FlightPassengers,
mm => mm.FlightBookingId,
mm => mm.CustomerId,
model.Id, // model = current flightBooking object
viewModel.PassengersJoinContainer);
if (!flightPassengersUpdater .Update())
{
ModelState.AddModelError("PassengersJoinContainer", "Since you opened this form the data has already been altered by someone else. ...");
}
I have problem with this Conference table. The error is :
Invalid column name 'Conference_ConferenceID'
namespace MeetingBoard.Model
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Script.Serialization;
using MeetingBoard.Model.Helpers;
using System.ComponentModel.DataAnnotations.Schema;
/// <summary>
/// A model of the Conference entity. Contains functionality to serialize the entity to JSON as well.
/// </summary>
public class Conference
{
[Key]
public int ConferenceID { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int CreatorID { get; set; }
public string Location { get; set; }
public DateTime SubmissionDate { get; set; }
[ForeignKey("CreatorID")]
public virtual User Creator { get; set; }
public int[] RelatedProjectsIDs { get; set; }
public virtual ICollection<ProjectTag> RelatedProjectTags { get; set; }
public DateTime CreatedOn
{
get { return (this.dateCreated == default(DateTime)) ? DateTime.UtcNow : this.dateCreated; }
set { this.dateCreated = value; }
}
private DateTime dateCreated = default(DateTime);
public virtual ICollection<Group> RelatedGroups { get; set; }
public Conference()
{
RelatedGroups = new List<Group>();
}
/// <summary>
/// Generates an object that can be serialized by the JSON serializer of MVC
/// </summary>
/// <param name="happening">An Conference.</param>
/// <returns></returns>
public static Object ToJsonObject(Conference conference)
{
int[] project_ids = conference.RelatedProjectTags.Select<ProjectTag, int>(pt => pt.ProjectID).ToArray();
return new Conference_JSON
{
id = conference.ConferenceID,
title = conference.Title,
Content = conference.Content,
created_timestamp_UTC = Util.DateTimeToMilliTimeStamp(conference.CreatedOn),
SubmissionDate = conference.SubmissionDate,
Location = conference.Location,
creator_avatar = conference.Creator.Avatar,
creator_fullname = conference.Creator.Name,
creator_id = conference.Creator.UserID,
project_ids = project_ids,
};
}
/// <summary>
/// Instantiates a new Conference object based on the json data.
/// </summary>
/// <param name="json_data">The json data needs to have the structure as specified in the private Conference_JSON object.</param>
/// <returns>A new Conference object. The related projects are referenced using an integer array containing project ids.</returns>
public static Conference FromJson(String json_data)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
Conference_JSON conference_object = serializer.Deserialize<Conference_JSON>(json_data);
return FromJsonObject(conference_object);
}
/// <summary>
/// Instantiates a new Conference object based on the private Conference_JSON object.
/// </summary>
/// <param name="json_data">The object needs to be an instance of the private Conference_JSON object.</param>
/// <returns>A new Conference object. The related projects are referenced using an integer array containing project ids.</returns>
public static Conference FromJsonObject(Object conference_object)
{
Conference_JSON conference_json = (Conference_JSON)conference_object;
Conference conference = new Conference
{
ConferenceID = conference_json.id,
Title = conference_json.title,
Content = conference_json.Content,
RelatedProjectsIDs = conference_json.project_ids,
Location = conference_json.Location,
SubmissionDate = conference_json.SubmissionDate,
};
return conference;
}
/// <summary>
/// Defines the structure of the json objects that ar communicated to and from the Frontend.
/// </summary>
private class Conference_JSON
{
/// <summary>
/// The Conference identifier.
/// </summary>
public int id;
public string title;
public string Content;
/// <summary>
/// An numeric representation of the time, in milliseconds from Unix Epoch, UTC timezone.
/// </summary>
public double created_timestamp_UTC;
public string creator_fullname;
public int creator_id;
public string creator_avatar;
/// <summary>
/// Related projects.
/// </summary>
public int[] project_ids;
public string Location;
public DateTime SubmissionDate;
}
}
}
I get this error when there is a mismatch between the code and the DB, in the sense that the code expects to find columns in the DB but they don't exist there. This happens when the DB isn't updated to match the changes in the code. I'd suggest looking at the database that is being hit when you get that error, maybe it's not looking where you expect.
I just recently switched from EF5 to NHibernate due to a few features that I want in my ORM but didn't find in EF. So, I'm new to NHibernate. I'm working in ASP.Net MVC.
I'm using Automapper to map FNH objects to my view models, but I'm having issues translating previously how I did things in EF to FNH. For example, I have a self referencing table that is a menu system.
Here is the model:
public partial class Menu {
private int _Id;
private string _Title;
private string _Link;
private int _SortOrder;
private System.Nullable<int> _ParentMenuId;
private Iesi.Collections.ISet _ChildMenus;
private Menu _ParentMenu;
#region Extensibility Method Definitions
partial void OnCreated();
#endregion
public Menu()
{
this._ChildMenus = new Iesi.Collections.HashedSet();
OnCreated();
}
/// <summary>
/// There are no comments for Id in the schema.
/// </summary>
public virtual int Id
{
get
{
return this._Id;
}
set
{
this._Id = value;
}
}
/// <summary>
/// There are no comments for Title in the schema.
/// </summary>
public virtual string Title
{
get
{
return this._Title;
}
set
{
this._Title = value;
}
}
/// <summary>
/// There are no comments for Link in the schema.
/// </summary>
public virtual string Link
{
get
{
return this._Link;
}
set
{
this._Link = value;
}
}
/// <summary>
/// There are no comments for SortOrder in the schema.
/// </summary>
public virtual int SortOrder
{
get
{
return this._SortOrder;
}
set
{
this._SortOrder = value;
}
}
/// <summary>
/// There are no comments for ParentMenuId in the schema.
/// </summary>
public virtual System.Nullable<int> ParentMenuId
{
get
{
return this._ParentMenuId;
}
set
{
this._ParentMenuId = value;
}
}
/// <summary>
/// There are no comments for ChildMenus in the schema.
/// </summary>
public virtual Iesi.Collections.ISet ChildMenus
{
get
{
return this._ChildMenus;
}
set
{
this._ChildMenus = value;
}
}
/// <summary>
/// There are no comments for ParentMenu in the schema.
/// </summary>
public virtual Menu ParentMenu
{
get
{
return this._ParentMenu;
}
set
{
this._ParentMenu = value;
}
}
}
Here is the Mapping:
public class MenuMap : ClassMap<Menu>
{
public MenuMap()
{
Schema(#"dbo");
Table(#"Menus");
LazyLoad();
Id(x => x.Id)
.Column("Id")
.CustomType("Int32")
.Access.Property()
.CustomSqlType("int")
.Not.Nullable()
.Precision(10)
.GeneratedBy.Identity();
Map(x => x.Title)
.Column("Title")
.CustomType("String")
.Access.Property()
.Generated.Never()
.CustomSqlType("varchar");
Map(x => x.Link)
.Column("Link")
.CustomType("String")
.Access.Property()
.Generated.Never()
.CustomSqlType("varchar")
.Not.Nullable()
.Length(50);
Map(x => x.SortOrder)
.Column("SortOrder")
.CustomType("Int32")
.Access.Property()
.Generated.Never()
.Not.Nullable()
.UniqueKey("KEY1");
Map(x => x.ParentMenuId)
.Column("ParentMenuId")
.CustomType("Int32")
.Access.Property()
.Generated.Never()
.UniqueKey("KEY1");
HasMany<Menu>(x => x.ChildMenus)
.Access.Property()
.AsSet()
.Cascade.None()
.LazyLoad()
.Inverse()
.Not.Generic()
.KeyColumns.Add("ParentMenuId", mapping => mapping.Name("ParentMenuId")
.SqlType("int")
.Nullable());
References(x => x.ParentMenu)
.Class<Menu>()
.Access.Property()
.Cascade.None()
.LazyLoad()
.Columns("ParentMenuId");
}
}
Here is my View Model or DTO:
public class MainMenuItemViewModel
{
public Int32 Id { get; set; }
public string Title { get; set; }
public string Link { get; set; }
public Int32 SortOrder { get; set; }
public Int32? ParentMenuId { get; set; }
public IList<MainMenuItemViewModel> ChildMenus { get; set; }
}
When I try to map the domain object to the view model, using this:
Mapper.CreateMap<Menu, MainMenuItemViewModel>();
I get the following error on I check if the configuration is valid on run:
The following property on WinStream.WebUI.Models.MainMenuItemViewModel cannot be mapped: ChildMenus
Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type WinStream.WebUI.Models.MainMenuItemViewModel.
Context:
Mapping to property ChildMenus from System.Object to WinStream.WebUI.Models.MainMenuItemViewModel
Mapping to property ChildMenus from Iesi.Collections.ISet to System.Collections.Generic.IList`1[[WinStream.WebUI.Models.MainMenuItemViewModel, WinStream.WebUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]
Mapping from type WinStream.Services.Entities.Menu to WinStream.WebUI.Models.MainMenuItemViewModel
Exception of type 'AutoMapper.AutoMapperConfigurationException' was thrown.
I thought it might be related to converting ISet to IList, so I put in an ISet in my view model, but still had the issue.
Thank you for your help - I realize this could be a complete newbie question, but I couldn't find much help through Google. I've been struggling with this for several days now.
Thanks!
EDIT:
I have gotten past the error above, but now when I'm querying the database, the ChildMenus collection for the root object includes a null object for every child object in the database, including the associated child objects, instead of just the actual related child objects.
For example:
Root Menu
ChildMenus collection is supposed to have 3 child objects, but it has 8 (5 null and 3 populated)
List item
ChildMenus collection is supposed to have 1 child objects, but it has 8 (7 null and 1 populated)
List item
ChildMenus collection is supposed to have 0 child objects, and it has no child objects.
This is the code:
IList<Menu> menus = session.Query<Menu>().Where(x => x.ParentMenuId== null).ToList()
Any ideas on this, or do I need to put it into another question? Thank you!
NHibernate doesn't need a lot of the workarounds from EF. You basicly have a menu with ordered childmenues having a parent reference.
public class Menu
{
public int Id { get; protected set; }
public string Title { get; set; }
public string Link { get; set; }
public IList<Menu> ChildMenus { get; protected set; }
public Menu ParentMenu { get; set; }
public Menu()
{
ChildMenus = new List<Menu>();
}
}
public class MenuMap : ClassMap<Menu>
{
public MenuMap()
{
Table(#"Menus");
Id(x => x.Id).GeneratedBy.Identity();
Map(x => x.Title).Length(100);
Map(x => x.Link).Length(50);
HasMany<Menu>(x => x.ChildMenus)
.AsList("SortOrder")
.Inverse()
.KeyColumn("ParentMenuId");
References(x => x.ParentMenu).Column("ParentMenuId");
}
}
Notes:
Schema should be defined by convention or as default schema/catalog in the Configuration object
remove all unnessesary declarations from the mapping because it often introduces portability issues (eg. customsqltypes), complicates the code and prevents conventions
customsqltype() renders length() useless
Sortorder is not really needed because the list already defines the order
parentId is duplicate for Parent.Id and can be implemented if needed ParentId { get { return ParentMenu == null ? null : (int?)ParentMenu.Id } }, no need to map or store it in field
if the parentreference is not needed remove it and .Inverse() from the collection mapping
I have a List of Categories (entities) with each of them having parents. Here is the Model for the category :
public class Category
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CategoryId { get; set; }
public int? ParentId { get; set; }
public string Name { get; set; }
public virtual Category Parent { get; set; }
}
I need to populate a SelectList such that the Parents come first and the childrens go indented Like:
Category 1
SubCategory 1
SubCategory 2
Category 2
I have to use this selectlist to poulate a dropdown in the edit and update forms. How do I go about doing this? clientside? serverside?
Thanks for any help you can offer.
This got me thinking about implementing this without the dependency of an interface, which I still think is reasonable. Here is an alternative solution using an extension method that does not require an interface to be implemented.
Extension Method
public static class ExtensionMethods
{
/// <summary>
/// Returns a single-selection select element containing the options specified in the items parameter.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <param name="helper">The class being extended.</param>
/// <param name="items">The collection of items used to populate the drop down list.</param>
/// <param name="parentItemsPredicate">A function to determine which elements are considered as parents.</param>
/// <param name="parentChildAssociationPredicate">A function to determine the children of a given parent.</param>
/// <param name="dataValueField">The value for the element.</param>
/// <param name="dataTextField">The display text for the value.</param>
/// <returns></returns>
public static MvcHtmlString DropDownGroupList<T>(
this HtmlHelper helper,
IEnumerable<T> items,
Func<T, bool> parentItemsPredicate,
Func<T, T, bool> parentChildAssociationPredicate,
string dataValueField,
string dataTextField)
{
var html = new StringBuilder("<select>");
foreach (var item in items.Where(parentItemsPredicate))
{
html.Append(string.Format("<optgroup label=\"{0}\">", item.GetType().GetProperty(dataTextField).GetValue(item, null)));
foreach (var child in items.Where(x => parentChildAssociationPredicate(x, item)))
{
var childType = child.GetType();
html.Append(string.Format("<option value=\"{0}\">{1}</option>", childType.GetProperty(dataValueField).GetValue(child, null), childType.GetProperty(dataTextField).GetValue(child, null)));
}
html.Append("</optgroup>");
}
html.Append("</select>");
return new MvcHtmlString(html.ToString());
}
}
Usage based on your Category class
#this.Html.DropDownGroupList(YourCollection, x => !x.ParentId.HasValue, (x, y) => { return x.ParentId.Equals(y.CategoryId); }, "CategoryId", "Name")
By the time I finished writing this post I wasn't so sure this was all that valuable but thought I'd post it anyways.
As you can see, your class must know the id of it's parent and the display name of both the child and parent should use the same property as indicated by the dataTextField parameter. So, essentially, your class needs the properties: Id, ParentId, and Name and you use the Func<T, bool> and Func<T, T, bool> parameters to determine relationships.
Don't forget to add in the necessary validation!
I would do this server-side using optgroup like this in razor syntax.
<select>
#foreach(var parent in categories.Where(x => !x.ParentId.HasValue)
{
<optgroup label="#parent.Name">
#foreach(var child in categories.Where(x => x.ParentId.Equals(parent.CategoryId))
{
<option value="#child.CategoryId">#child.Name</option>
}
</optgroup>
}
</select>
I would also make this an extension methods so it would like the other available HTML helper methods.
Edit
An extension method could accept a collection of items which implement a simple interface like this:
public interface IGroupable
{
int Id { get; set; }
string Name { get; set; }
int? ParentId { get; set; }
}
public class Category : IGroupable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CategoryId { get; set; }
public int? ParentId { get; set; } //implements IGroupable.ParentId
public string Name { get; set; } //implements IGroupable.Name
public virtual Category Parent { get; set; }
#region [IGroupable Specific Implementation]
public int Id { get { return this.CategoryId; } }
#endregion
}
public static class ExtensionMethods
{
public static MvcHtmlString DropDownGroupList(this HtmlHelper helper, IEnumerable<IGroupable> items)
{
var html = new StringBuilder("<select>");
foreach (var item in items.Where(x => !x.ParentId.HasValue))
{
html.Append(string.Format("<optgroup label=\"{0}\">", item.Name));
foreach(var child in items.Where(x => x.ParentId.Equals(item.Id)))
{
html.Append(string.Format("<option value=\"{0}\">{1}</option>", child.Id, child.Name));
}
html.Append("</optgroup>");
}
html.Append("</select>");
return new MvcHtmlString(html.ToString());
}
}
How would I create a SelectList in my controller and pass it to my view? I need to give the "-- Select --" option a value of 0.
I'm responding to the replies that I got from Jeremey of Fluent Validation.
This is what I currently have. My view model:
[Validator(typeof(CreateCategoryViewModelValidator))]
public class CreateCategoryViewModel
{
public CreateCategoryViewModel()
{
IsActive = true;
}
public string Name { get; set; }
public string Description { get; set; }
public string MetaKeywords { get; set; }
public string MetaDescription { get; set; }
public bool IsActive { get; set; }
public IList<Category> ParentCategories { get; set; }
public int ParentCategoryId { get; set; }
}
My controller.
public ActionResult Create()
{
List<Category> parentCategoriesList = categoryService.GetParentCategories();
CreateCategoryViewModel createCategoryViewModel = new CreateCategoryViewModel
{
ParentCategories = parentCategoriesList
};
return View(createCategoryViewModel);
}
This is what I have in my view:
#Html.DropDownListFor(x => x.ParentCategoryId, new SelectList(Model.ParentCategories, "Id", "Name", Model.ParentCategoryId), "-- Select --")
How do I create a dropdown list in the controller or view model and pass it to the view? I need the "-- Select --" option to have a value of 0.
In your model, change the IList<Category> to SelectList and then instantiate it like this...
List<ParentCategory> parentCategories = categoryService.GetParentCategories();
parentCategories.Insert(0, new ParentCategory(){ Id = "0", Name = "--Select--"});
ParentCategories = new SelectList(parentCategories, "Id", "Name");
Then in your view you can simply call
#Html.DropDownListFor(m => m.ParentCategoryId, Model.ParentCategories);
One way I've seen it done is to create an object to wrap the id and value of the drop down item, like a List<SelectValue>, and pass it in your ViewModel to the view and then use an HTML helper to construct the dropdown.
public class SelectValue
{
/// <summary>
/// Id of the dropdown value
/// </summary>
public int Id { get; set; }
/// <summary>
/// Display string for the Dropdown
/// </summary>
public string DropdownValue { get; set; }
}
Here is the view model:
public class TestViewModel
{
public List<SelectValue> DropDownValues {get; set;}
}
Here is the HTML Helper:
public static SelectList CreateSelectListWithSelectOption(this HtmlHelper helper, List<SelectValue> options, string selectedValue)
{
var values = (from option in options
select new { Id = option.Id.ToString(), Value = option.DropdownValue }).ToList();
values.Insert(0, new { Id = 0, Value = "--Select--" });
return new SelectList(values, "Id", "Value", selectedValue);
}
Then in your view you call the helper:
#Html.DropDownList("DropDownListName", Html.CreateSelectListWithSelect(Model.DropDownValues, "--Select--"))