ASP.NET Web Forms 4.5模型绑定模型包含集合的位置

时间:2014-02-10 01:21:53

标签: asp.net webforms model-binding

我正在尝试更新旧的Web窗体应用程序,以使用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属性设为BindingList而不是List,但它没有任何区别,Extras集合不受约束UpdateProduct方法。

6 个答案:

答案 0 :(得分:0)

不幸的是我不确切知道如何使用Web Forms完成这一操作,因此我不确定如何使用转发器重现这一点,但在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循环替换了转发器,该循环使用用于访问每个Extra的索引遍历集合。这类似于我需要在ASP.NET MVC中执行您想要的操作。提交表单时,索引将与Web表单的其余部分一起发布,这允许模型绑定器重建有序的对象列表。

我希望这是一些帮助并原谅我的任何错误,因为我目前没有一个Web表单项目来测试它。

答案 1 :(得分:0)

深入研究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 + "]");
}

不幸的是,您的转发器的元素名称与该条件不符。

虽然肯定是非正统的,但你仍然可以通过编写非服务器文本框来实现这一点,然后从你的datalist命名容器开始给它们一个名称,然后是索引。并且感谢&#34; Request.Unvalidated&#34; (也在4.5中介绍),即使服务器端控件没有表示数据,你也可以对这些数据进行数据绑定。

答案 2 :(得分:0)

我找到了一种解决方法来绑定 UpdateMethod 中对象的集合属性:

我的模型:

public partial class Student
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Student()
    {
        this.Enrollments = new List<Enrollment>();
    }

    public int Id { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public Nullable<int> YearId { get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual List<Enrollment> Enrollments { get; set; }
    public virtual AcademicYear AcademicYear { get; set; }
}


public partial class Enrollment
{
    public int Id { get; set; }
    public Nullable<decimal> Grade { get; set; }
    public Nullable<int> CourseId { get; set; }
    public Nullable<int> StudentId { get; set; }

    public virtual Course Course { get; set; }
    public virtual Student Student { get; set; }
}

控制:

<asp:FormView runat="server" ID="addStudentForm"
    DataKeyNames="Id"
    ItemType="WebApplication3.Student" 
    UpdateMethod="addStudentForm_UpdateItem" DefaultMode="Edit"
    RenderOuterTable="false" OnItemUpdated="addStudentForm_ItemUpdated"
    SelectMethod="addStudentForm_GetItem">
    <EditItemTemplate>            
        <fieldset>
        
            <asp:HiddenField ID="Id" Value="<%# BindItem.Id %>" runat="server" />
            <asp:Panel ID="Panel1" runat="server">

                <asp:Label ID="Label1" runat="server" Text="FirstName: "></asp:Label>
                <asp:TextBox ID="FirstName" runat="server" Width="70%" Text="<%# BindItem.FirstName %>" ></asp:TextBox>

            </asp:Panel>

            <asp:Panel ID="Panel2" runat="server">

                <asp:Label ID="Label2" runat="server" Text="LastName: "></asp:Label>
                <asp:TextBox ID="LastName" runat="server" Width="70%" Text="<%# BindItem.LastName %>" ></asp:TextBox>

            </asp:Panel>

            <asp:Panel ID="Panel4" runat="server"  >
                <% for (int i = 0; i < enrollments.Count; i++)
                    { %>

                    // ```name``` is constructed like in MVC
                    <input hidden id="test" type="text" name="Enrollments[<%= i %>].Id" value='<%= enrollments[i].Id %>'/>
                    <input id="test1" type="text" name="Enrollments[<%= i %>].Grade" value='<%= enrollments[i].Grade %>' />
                <%  } %>

            </asp:Panel>
                            
            <asp:Button runat="server" Text="Edit" CommandName="Update" />
            <asp:Button runat="server" Text="Cancel" CausesValidation="false" OnClick="Cancel_Click" />
        </fieldset>
    </EditItemTemplate>
</asp:FormView>

背后的代码:


public List<Enrollment> enrollments = new List<Enrollment>(); // Contain the data after ```SelectMethod``` from db, is for ```for``` loop in Controls page

protected void Page_Load(object sender, EventArgs e)
{
}

public void addStudentForm_UpdateItem(Student student) // At this line, the ```student``` will be bound with data from ASP SerVer Control only
{

    // Add these 3 lines of code to use MVC approach.
    // THis kind of binding uses prefix like [] or ., which ASP server controls are not accepted
    NameValueCollection nameValues = Request.Form;

    IValueProvider provider = new NameValueCollectionValueProvider(nameValues, System.Globalization.CultureInfo.CurrentUICulture);

    TryUpdateModel(student, provider); // After this, the ```student``` will be bound with Form Values that have their ```key``` (```name``` attribute) being constructed like MVC approach (see above Control code).

    // Database update go here
    
}

结果:

在 TryUpdateMethod 之前,Webforms 4.5 模型绑定将绑定到 student 对象。请注意,Enrollments 为空。

enter image description here

创建IValueProvider provider并将其传递给TryUpdateMethod后,Enrollments的计数为3(等于表单中提交的数据)。

enter image description here

更多详情:

enter image description here


关于绑定到 edit 的集合的发现:

  1. 在您的问题中,您无法将数据绑定为 MVC 的原因是 UpdateMethod 中的自动模型绑定仅适用于 (1) ASP 服务器控件; (2) 具有简单类型的属性,如 stringint...,而不是复杂类型,如 collection 或类对象。

  2. 对于这些复杂类型,您可以按照自己的意愿使用 MVC 方法。但是,我认为这种方法可能需要以下条件:

    • 它只适用于 HTML 元素(没有 runat=server),因为需要 name 属性(例如 <input><select>)。你明白我的意思吗? Webform Server Controls 有自己的 name 属性设计,旨在适应它自己的模型绑定,MVC 不接受这种绑定。这就是您无法设置 name 属性的原因,如果您尝试使用 Jquery/JavaScript/Code-Behind 更改它,Webform Server 控件的模型绑定将不起作用。
    • 此方法仅使用“MVC 样式”name 进行绑定。所以它不会绑定 ASP 服务器控件样式 name(比如 $Maincontent$... 的东西)。
    • Webform Server Controls 方法和 MVC 方法的绑定不能合二为一。我不得不像我的例子一样一个接一个地做。
  3. 我研究期间的其他不太相关的事情:

  • BindItemEvalContainer.ItemIndexRepeater)仅适用于像 Textbox 这样的网络表单控件,或带有 {{ 1}}。两者都会更改 runat=server 属性以适应 Webform 模型绑定样式,因此您不能将这些用于 MVC 方法。

  • 在@JonathanWalter 示例中,计数器 name 不能放置在 i 内。它必须放在 <%# %> 内。因此,我尝试了很多次,但无法将 <%=%> 构造为 TextBindItem.Enrollments[i].Id。所以很遗憾,我对此的希望落空了。

  • 如果我将集合绑定在 TextBoxonClickasp:button 事件中,MVC 方法也有效。只需确保将 <button> 放在 HTML 元素中,而不是服务器控件中,并将其命名为 MVC 样式。

这是我对这个问题的发现。我花了很多时间在这上面,所以我想把这个贴在这里,以防有人想尝试这种 MVC 方法。

感谢您阅读这篇长篇文章 =))。

答案 3 :(得分:-1)

[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 :(得分:-1)

您应该在内部数据列表中指定编辑项模板,因为项目模板中的属性可能不会在自动构造并传递给Update方法的模型中返回。 我没有时间尝试,但这应该有用......

<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 :(得分:-2)

在查询字符串中传递extras值,在update方法中添加参数值。使用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
      });
    }
}