私のモデル
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; }
}
そして、OnModelCreatingで私は追加しました
modelBuilder.Entity<FlightPassenger>().HasKey(x => new { x.FlightBookingId, x.CustomerId });
これにより、データベースに3つのテーブルが作成されます。顧客、FlightBookingおよびFlightPassenger。 EF7の多対多の関係を表すのにこれはすべてうまくいきます。今私はユーザーからこの入力を取ろうとしています。
私の見解
<select asp-for="Passengers" asp-items="Enumerable.Empty<SelectListItem>()" class="form-control customer"></select>
私はAjaxを使用してデータを正しく取得しており、ドロップダウンで複数の値を選択することができます。しかしコントローラーでは、乗客に値が渡されず、その数は0になります。ポストする前にドロップダウンの値をチェックし、選択した顧客のIDをコンマで表示します。私は乗客が整数配列ではないことを知っていますが、整数配列をモデルに追加すると別のエラーが発生するため、別の方法が必要であると考えていました。私は、私のビューモデルに文字列を追加し、この整数配列を文字列に追加する前に小さなハックを行いました。この文字列は、コントローラ内のすべての値(カンマsep)を持ちます。しかし、もっと良い方法があるはずだと確信しています。ビューからこの値を取得し、最終的にデータベースに格納するためのガイダンスは素晴らしいでしょう。
私の現在のプロジェクトでは、多対多の関係がたくさんあります。私が知る限り、EF Coreはまだ多対多をサポートしていないので、手動で行う必要があると思います。私はその解決法を一般化した。
私はEF / MVCのフィードバックを歓迎しています。
まず、多対多のエンティティに必要なデータを保持するJoinContainer
を作成しました。
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;
}
}
ビューモデル(FlightBookingの):
[Display(Name = "Passengers")]
public SimpleJoinContainerViewModel PassengersJoinContainer { get; set; } = new SimpleJoinContainerViewModel();
GETアクションでは、 Load()
メソッドを使用してコンテナにデータを入力します。
viewModel.PassengerJoinContainer.Load(
DbContext.Customers
.Select(s => new SelectListItem
{
Text = s.LastName,
Value = s.Id.ToString()
}),
flightBookingEntity?.Passengers?.Select(p => p.CustomerId));
ビューでは、私は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>
次に、私は一般化されたUpdateクラス/メソッドを持っています。
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;
}
}
私はこのクラス/メソッドを呼び出すポストアクションで:
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. ...");
}