EF7有很多很多關係。如何將數據從多選下拉菜單傳遞給控制器?

asp.net-core-mvc asp.net-mvc entity-framework entity-framework-core

我的模特

      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。我知道Passengers不是一個整數數組,但是在模型中添加一個整數數組會產生另一個錯誤,所以我認為必須採用另一種方​​式。我通過在我的視圖模型中添加一個字符串並在將此整數數組添加到字符串之前做了一個小黑客。該字符串包含控制器中的所有值(逗號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()方法用數據填充Container:

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;
    }
}

在Post動作中我調用這個類/方法:

    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. ...");
    }


Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因