ASP.NET Web Forms 4.5 привязка модели, где модель содержит коллекцию

Я пытаюсь обновить старое приложение Web Forms, чтобы использовать новые возможности привязки к модели, добавленные в 4.5, аналогичные свойствам привязки MVC.

У меня возникли проблемы с созданием редактируемого FormView, который представляет собой единственную модель, содержащую простых членов плюс член, который представляет собой коллекцию других моделей. Мне нужно, чтобы пользователь мог редактировать простые свойства родительского объекта и свойства дочерней коллекции.

Проблема в том, что дочерняя коллекция (ProductChoice.Extras) всегда равна null после привязки к модели, когда код пытается обновить модель.

Вот мои модели:

[Serializable]
public class ProductChoice
{
    public ProductChoice()
    {
        Extras = new List<ProductChoiceExtra>();
    }

    public int Quantity { get; set; }
    public int ProductId { get; set; }
    public List<ProductChoiceExtra> Extras { get; set; }
}

[Serializable]
public class ProductChoiceExtra
{
    public int ExtraProductId { get; set; }
    public string ExtraName { get; set; }
    public int ExtraQuantity { get; set; }
}

И мой код управления пользователя:

public partial class ProductDetails : System.Web.UI.UserControl
{
    private Models.ProductChoice _productChoice;

    protected void Page_Load(object sender, EventArgs e)
    {
        _productChoice = new Models.ProductChoice()
        {
            Quantity = 1,
            ProductId = 1
        };
        _productChoice.Extras.Add(new Models.ProductChoiceExtra()
        {
            ExtraProductId = 101,
            ExtraName = "coke",
            ExtraQuantity = 1
        });
        _productChoice.Extras.Add(new Models.ProductChoiceExtra()
        {
            ExtraProductId = 104,
            ExtraName = "sprite",
            ExtraQuantity = 2
        });

    }

    public Models.ProductChoice GetProduct()
    {
        return _productChoice;
    }

    public void UpdateProduct(Models.ProductChoice model)
    {
        /* model.Extras is always null here, it should contain two ProductChoiceExtra objects */

        if (TryUpdateModel(_productChoice) == true)
        {
        }
    }
}

Моя контрольная разметка:

<div id="selectOptions">
    <asp:FormView runat="server" ID="fvProductSelection" DefaultMode="Edit"
        ItemType="Models.ProductChoice"
        SelectMethod="GetProduct"
        UpdateMethod="UpdateProduct" >

        <EditItemTemplate>
            <asp:linkbutton id="UpdateButton" text="Update" commandname="Update" runat="server"/>
            <asp:HiddenField runat="server" ID="ProductId" Value="<%# BindItem.ProductId %>" />
            <asp:TextBox Text ="<%# BindItem.Quantity %>" ID="Quantity" runat="server" />

            <asp:Repeater ID="Extras" ItemType="Models.ProductChoiceExtra" DataSource="<%# BindItem.Extras %>" runat="server">
                <ItemTemplate>
                    <asp:HiddenField Value="<%# BindItem.ExtraProductId %>" ID="ExtraProductId" runat="server"  />
                    <asp:Label Text="<%# BindItem.ExtraName %>" ID="Name" runat="server" />
                    <asp:TextBox Text="<%# BindItem.ExtraQuantity %>" ID="Quantity"  runat="server" />
                </ItemTemplate>
            </asp:Repeater>
        </EditItemTemplate>
    </asp:FormView>
</div>

Я попытался сделать свойство Extras a BindingList, а не List, но это не имело никакого значения, коллекция Extras не связана в методе UpdateProduct.

Ответы

Ответ 1

К сожалению, я точно не знаю, как это делается с веб-формами, поэтому я не уверен, как воспроизводить это с помощью ретранслятора, но в MVC для связывания модели требуется индекс для восстановления списка. Если мне придется угадать, как это делается в веб-формах, это будет нечто похожее на это:

<div id="selectOptions">
    <asp:FormView runat="server" ID="fvProductSelection" DefaultMode="Edit"
        ItemType="Models.ProductChoice"
        SelectMethod="GetProduct"
        UpdateMethod="UpdateProduct" >
        <EditItemTemplate>
            <asp:linkbutton id="UpdateButton" text="Update" commandname="Update" runat="server"/>
            <asp:HiddenField runat="server" ID="ProductId" Value="<%# BindItem.ProductId %>" />
            <asp:TextBox Text ="<%# BindItem.Quantity %>" ID="Quantity" runat="server" />

            <% for (int i = 0; i < BindItem.Extras.Count; i++)
            { %>
                    <asp:HiddenField Value="<%# BindItem.Extras[i].ExtraProductId %>" ID="ExtraProductId" runat="server"  />
                    <asp:Label Text="<%# BindItem.Extras[i].ExtraName %>" ID="Name" runat="server" />
                    <asp:TextBox Text="<%# BindItem.Extras[i].ExtraQuantity %>" ID="Quantity"  runat="server" />
            <% } %> 
        </EditItemTemplate>
    </asp:FormView>
</div>

Примечание. Я заменил ретранслятор циклом for, который выполняет итерацию через коллекцию с индексом, используемым для доступа к каждому Экстра. Это похоже на то, как мне нужно делать то, что вы хотите в ASP.NET MVC. Индекс отправляется вместе с остальной частью веб-формы при отправке формы, что позволяет связующему устройству модели восстанавливать упорядоченный список объектов.

Надеюсь, это какая-то помощь и прости меня за любые ошибки, так как у меня нет проекта веб-форм, чтобы проверить это на данный момент.

Ответ 2

Копаем в System.Web.ModelBinding показывает, что CollectionModelBinder ожидает, что значения, переданные в FormValueProvider, будут в том же формате, что и для MVC, то есть: MyCollection [i]

public static string CreateIndexModelName(string parentName, string index)
{
    if (parentName.Length != 0)
    {
        return (parentName + "[" + index + "]");
    }
    return ("[" + index + "]");
}

К сожалению, ваши имена элементов ретранслятора не будут соответствовать этим критериям.

Несмотря на то, что вы, безусловно, неортодоксальный, вы все равно можете достичь этого, написав текстовые поля без сервера, а затем давая им имя, начинающееся с вашего контейнера именования данных, а затем индекс. И благодаря "Request.Unvalidated" (также представленному в 4.5) у вас есть возможность привязывать данные к данным, даже если они не представлены элементами управления на стороне сервера.

Ответ 3

[Serializable]
public class ProductChoice
{
public int Quantity { get; set; }
    public int ProductId { get; set; }
    public ProductChoiceExtra Extras { get; set; }
}

[Serializable]
public class ProductChoiceExtra
{
    public int ExtraProductId { get; set; }
    public string ExtraName { get; set; }
    public int ExtraQuantity { get; set; }
    public List<ProductChoiceExtra> listProducts{get;set;}
}

Ответ 4

Вы должны указать шаблон редактирования элемента во внутреннем каталоге данных, поскольку свойства в шаблоне элемента не могут быть возвращены в модели, которая автоматически создается и передается методу обновления. У меня не было времени попробовать, но это должно сработать...

<div id="selectOptions">
    <asp:FormView runat="server" ID="fvProductSelection" DefaultMode="Edit"
        ItemType="Models.ProductChoice"
        SelectMethod="GetProduct"
        UpdateMethod="UpdateProduct" >

        <EditItemTemplate>
            <asp:linkbutton id="UpdateButton" text="Update" commandname="Update" runat="server"/>
            <asp:HiddenField runat="server" ID="ProductId" Value="<%# BindItem.ProductId %>" />
            <asp:TextBox Text ="<%# BindItem.Quantity %>" ID="Quantity" runat="server" />

            <asp:DataList ID="Extras" DataSource="<%# DataBinder.Eval(Container.DataItem, "Extras") %>" runat="server">
                <EditItemTemplate>
                    <asp:HiddenField Value="<%# BindItem.ExtraProductId %>" ID="ExtraProductId" runat="server"  />
                    <asp:Label Text="<%# BindItem.ExtraName %>" ID="Name" runat="server" />
                    <asp:TextBox Text="<%# BindItem.ExtraQuantity %>" ID="TextBox1"  runat="server" />
                </EditItemTemplate>
                <ItemTemplate>
                    <asp:HiddenField Value="<%# BindItem.ExtraProductId %>" ID="ExtraProductId" runat="server"  />
                    <asp:Label Text="<%# BindItem.ExtraName %>" ID="Name" runat="server" />
                    <asp:TextBox Text="<%# BindItem.ExtraQuantity %>" ID="Quantity"  runat="server" />
                </ItemTemplate>
            </asp:Repeater>
        </EditItemTemplate>
    </asp:FormView>
</div>

Ответ 5

Передайте значения дополнительных значений в строке запроса, добавьте значения параметров в метод обновления. Используйте атрибуты данных в HTML 5, чтобы сохранить эти 3 значения в элементе вида формы.

Пример

UpdateMethod="UpdateProduct/104/coke/2"

или

UpdateMethod="UpdateProduct/?ExtraProductId=104&ExtraName=coke&ExtraQuantity=2"

для первого подхода вам нужно написать правило маршрутизации в Route Config.

Внутри пользовательского элемента управления, как показано ниже

public void UpdateProduct(Models.ProductChoice model, int ExtraProductId, string ExtraName, int ExtraQuantity)
{
    /* model.Extras is always null here, it should contain two ProductChoiceExtra objects */

    if (TryUpdateModel(_productChoice) == true)
    {
      model.Extras.Add(new Models.ProductChoiceExtra()
      {
        ExtraProductId = ExtraProductId,
        ExtraName = ExtraName,
        ExtraQuantity = ExtraQuantity
      });
    }
}