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
Related
Situation
I'm using Identity ASP.NET Core 3.1 with Angular 8 Template. I want to extend the ASPNETUserRoles table and add another custom key column CompanyId in it.
By Default Identity provides:
public virtual TKey UserId { get; set; }
public virtual TKey RoleId { get; set; }
As I modified my DbContext from UserId (string) to UserId (long), DbContext looks like:
public class CompanyDBContext : KeyApiAuthorizationDbContext<User, Role, UserRole, long>
{
public CompanyDBContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
public DbSet<Company> Companies { get; set; }
}
KeyApiAuthorizationDbContext
public class KeyApiAuthorizationDbContext<TUser, TRole, IdentityUserRole, TKey> : IdentityDbContext<TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>, IPersistedGrantDbContext
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where IdentityUserRole : IdentityUserRole<TKey>
where TKey : IEquatable<TKey>
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
/// <summary>
/// Initializes a new instance of <see cref="ApiAuthorizationDbContext{TUser, TRole, TKey}"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptions"/>.</param>
/// <param name="operationalStoreOptions">The <see cref="IOptions{OperationalStoreOptions}"/>.</param>
public KeyApiAuthorizationDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions)
: base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
/// <summary>
/// Gets or sets the <see cref="DbSet{PersistedGrant}"/>.
/// </summary>
public DbSet<PersistedGrant> PersistedGrants { get; set; }
/// <summary>
/// Gets or sets the <see cref="DbSet{DeviceFlowCodes}"/>.
/// </summary>
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
}
}
Entities
public class User : IdentityUser<long> {}
public class Role : IdentityRole<long> {}
public class UserRole : IdentityUserRole<long>
{
public long CompanyId { get; set; }
}
Problem Occur
When I registered my user and it returns true then I'll add a current user in UserRole table like below but when my debugger reached await _context.SaveChangesAsync(); method its shows me an exception
if (result.Succeeded)
{
foreach (var role in model.Roles.Where(x => x.IsChecked = true))
{
var entity = new Core.Entities.Identity.UserRole()
{
UserId = model.User.Id,
RoleId = role.Id,
CompanyId = companycode
};
_context.UserRoles.Add(entity);
}
await _context.SaveChangesAsync();
}
I don't know where I'm making my mistake? If above steps of overriding userrole are wrong then assist me on that.
I'm also sharing my migration details for your information may be something I'm doing wrong there.
Migrations
migrationBuilder.CreateTable(
name: "Companies",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(nullable: false),
Code = table.Column<string>(nullable: false),
Logo = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Companies", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<long>(nullable: false),
RoleId = table.Column<long>(nullable: false),
CompanyId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId, x.CompanyId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_Companies_CompanyId",
column: x => x.CompanyId,
principalTable: "Companies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
Migration Snapshot
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
Now I came up with an answer to my own question.
Recall Question
I want to extend the User Role (IdentityUserRole) and adding a few foreign keys, but faced two errors:
Error 1
Invalid column name 'discriminator'
Error 2
A key cannot be configured on ‘UserRole’ because it is a derived type. The key must be configured on the root type ‘IdentityUserRole’. If you did not intend for ‘IdentityUserRole’ to be included in the model, ensure that it is not included in a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation property on a type that is included in the model.
This is because if you look into the definition of IdentityUserRole, you’ll find out that it already has primary keys:
public virtual TKey UserId { get; set; }
public virtual TKey RoleId { get; set; }
So, what's the solution?
To fix this, we have to override the user role implementation.
Now if you see the code:
public class CompanyDBContext : IdentityDbContext<User, Role, long, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IPersistedGrantDbContext
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
public CompanyDBContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
modelBuilder.Entity<UserRole>(b =>
{
b.HasKey(ur => new { ur.UserId, ur.RoleId, ur.CompanyId });
});
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
public DbSet<PersistedGrant> PersistedGrants { get; set; }
}
That's it. As #EduCielo suggested I re-consider my code and do the proper mapping and configuration and override the modelbuilder to map the proper relationship for user role.
For new developers, I'm sharing entity classes which I used above to make it fully worked with your code.
public class User : IdentityUser<long> { //Add new props to modify ASP.NETUsers Table }
public class Role : IdentityRole<long> { }
public class UserClaim: IdentityUserClaim<long> { }
public class UserRole : IdentityUserRole<long>
{
[Key, Column(Order = 2)]
public long CompanyId { get; set; }
public Company Company { get; set; }
}
public class UserLogin: IdentityUserLogin<long> { }
public class RoleClaim : IdentityRoleClaim<long> { }
public class UserToken : IdentityUserToken<long> { }
Conclusion
add-migration FirstMigration -context CompanyDbContext
update-database -context CompanyDbContext
Happy Coding :)
Ref Link
inherit from IdentityRole
public class myRoles: IdentityRole
{
public bool ACTIVE { get; set; }
public bool LOCKED { get; set; }
public int SORT_ORDER { get; set; }
public bool DEFAULT_FLAG { get; set; }
public string USER_PRIVILEGES { get; set; }
public string CREATED_BY { get; set; }
public DateTime? CREATED_DATE { get; set; }
public string UPDATED_BY { get; set; }
public DateTime? UPDATED_DATE { get; set; }
}
refer your roleclass in startUp
services.AddIdentity<RegisterUser, myRoles>
.AddEntityFrameworkStores<ContextClass>()
ali-ahsan
public class UserRole : IdentityUserRole<Guid>
{
public DateTime StartDate { get; set; }
public DateTime DueDate { get; set; }
}
I expanded UserRole as you explained above. How do I add values to the fields I add while assigning a role? Because where we assign a role to the user, I can't give anything other than the role name.
var roleAdmin = "Admin";
var adminRoleCreatedResult = await _userManager.AddToRoleAsync(user, roleAdmin);
Is it possible to set an attribute to specific tables/classes in EF and then query a list of those tables out based on that attribute?
Add the attribute the their partial classes. Then get a list of types with that attribute in the current assembly
Assembly thisAsm = Assembly.GetExecutingAssembly();
var tableTypes = thisAsm.GetTypes()
.Where(t => t.IsDefined(typeof(MyAttribute), false));
Ok, I think I got it working without using Assembly:
public class FoodAttribute : Attribute { }
public class Fruit { }
public class Coin { }
public class Cereal { }
public class FooContext : DbContext {
[Food]
public virtual DbSet<Fruit> Fruits { get; set; }
public virtual DbSet<Coin> Coins { get; set; }
[Food]
public virtual DbSet<Cereal> Cereals { get; set; }
}
To get a list of tables with the attribute Food:
var tableList = typeof(FooContext).GetProperties()
.Where(n => n.IsDefined(typeof(ClientTraitAttribute)))
.Select(n => n.PropertyType.GetGenericArguments()[0].Name).ToList();
Thanks Yuriy for showing me the IsDefined method! :D
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 am using Glass Mapper for Umbraco. While trying to model something I have a class like:
[UmbracoType(AutoMap = true)]
public class ImageWithLink : BaseUmbracoItem
{
public virtual Image Image { get; set; }
public virtual ?? Link { get; set; }
public virtual string Copy { get; set; }
}
There does not seem to be a 'Link' data type like there is in the Sitecore implementation. I saw This post ( http://bluetubeinc.com/blog/2014/6/glass-mapper-and-umbraco-7) and they use the 'RelatedLink' data type, but that does not exsist ( i checked in the glass repository).
Do I have to model it my self?
Edit: This is the Related Links property type.
Assuming that Umbraco hasn't changed in a while and that you mean a link as in a html anchor...
I may be wrong on this one, but from memory, Umbraco has / had no concept of a link when it comes to the return from the api. It is returned as a node id (or list of) and you must use an extension method to return the url from.
Like you, as far as I can see in the Glass code base, it doesn't have an explicit implementation for a 'link' type like we have in Sitecore.
My guess is that you would have to either roll your own using a custom class and the Delegate feature OR use an integer mapping and call the umbraco api method.
I would also guess is that RelatedLink in that example is mapping to another class which would use the UmbracoPropertyTypeMapper in a similar way to what we do in Sitecore between types, not a 'Link' as in anchor.
We are due to look at umbraco again during the V4 process I believe so talk to Mike about adding it as a feature.
I Found a (rather horrible) solution. Umbraco returns Json, so i had to de-serialize it. You could turn this into a mapper.
[UmbracoType(AutoMap = true)]
public class BaseUmbracoItem : IUmbracoItem
{
public virtual string Links { get; set; }
public List<UmbracoLink> TypedLink
{
get
{
return JsonConvert.DeserializeObject<List<UmbracoLink>>(Links);
}
}
}
public class UmbracoLink
{
public string link { get; set; }
public string type { get; set; }
public string title { get; set; }
public bool newWindow { get; set; }
public bool isInternal { get; set; }
}
Here is a mapper version:
public class UmbracoLinkMapper : AbstractDataMapper
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoLinkMapper"/> class.
/// </summary>
public UmbracoLinkMapper()
{
ReadOnly = true;
}
/// <summary>
/// Maps data from the .Net property value to the CMS value
/// </summary>
/// <param name="mappingContext">The mapping context.</param>
/// <returns>The value to write</returns>
/// <exception cref="System.NotSupportedException"></exception>
public override void MapToCms(AbstractDataMappingContext mappingContext)
{
throw new NotSupportedException();
}
/// <summary>
/// Maps data from the CMS value to the .Net property value
/// </summary>
/// <param name="mappingContext">The mapping context.</param>
/// <returns>System.Object.</returns>
public override object MapToProperty(AbstractDataMappingContext mappingContext)
{
var scContext = mappingContext as UmbracoDataMappingContext;
var scConfig = Configuration as UmbracoLinkConfiguration;
var properties = scContext.Content.Properties.Where(x => x.Alias == Configuration.PropertyInfo.Name.ToLower()).ToList();
if (properties.Any())
{
var property = properties.First().Value as string;
return JsonConvert.DeserializeObject<List<UmbracoLink>>(property).First();
}
return null;
}
/// <summary>
/// Indicates that the data mapper will mapper to and from the property
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="context">The context.</param>
/// <returns><c>true</c> if this instance can handle the specified configuration; otherwise, <c>false</c>.</returns>
public override bool CanHandle(AbstractPropertyConfiguration configuration, Context context)
{
return configuration is UmbracoLinkConfiguration;
}
}
public class UmbracoLinkConfiguration : AbstractPropertyConfiguration
{
public bool IsLazy { get; set; }
public bool InferType { get; set; }
}
public class UmbracoLinkAttribute : AbstractPropertyAttribute
{
public bool IsLazy { get; set; }
public bool InferType { get; set; }
public override AbstractPropertyConfiguration Configure(PropertyInfo propertyInfo)
{
var config = new UmbracoLinkConfiguration { IsLazy = IsLazy, InferType = InferType };
Configure(propertyInfo, config);
return config;
}
}
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());
}
}