18

I'm trying to update an old Web Forms application to use the new model binding features added in 4.5, similar to the MVC binding features.

I'm having trouble making an editable FormView that presents a single model that contains simple members plus a member that is a collection of other models. I need the user to be able to edit the simple properties of the parent object and the properties of the child collection.

The problem is that the child collection (ProductChoice.Extras) is always null after model binding when the code is trying to update the model.

Here are my models:

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

And my user control code behind:

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)
        {
        }
    }
}

My control markup:

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

I have tried making the Extras property a BindingList rather than a List but it didn't make any difference, the Extras collection isn't bound in the UpdateProduct method.

Abolfazl
  • 1,592
  • 11
  • 32
PaulD
  • 323
  • 2
  • 7
  • Can you not try just passing in the model ProductChoice. E.G: public void UpdateProduct(ProductChoice model) { } – izip Feb 13 '14 at 13:00
  • Good question @PaulD: I had thought that this FormView/Repeater approach would be a common one (my approach is identical to yours) - but I haven't been able to crack this one either. – Merenzo Feb 14 '14 at 04:17
  • I tried implementing a custom [`System.Web.ModelBinding.IValueProvider`](http://msdn.microsoft.com/en-us/library/system.web.modelbinding.ivalueprovider(v=vs.110).aspx), in order to use the [overload](http://msdn.microsoft.com/en-us/library/hh140347(v=vs.110).aspx) for `TryUpdateModel()`, but my `IValueProvider` implementation got stuck in an infinite recursive loop as it reflected its way through the object properties... – Merenzo Feb 21 '14 at 01:22
  • And [`FormValueProvider`](http://msdn.microsoft.com/en-us/library/system.web.modelbinding.formvalueprovider(v=vs.110).aspx) is sealed, which is a bummer. – Merenzo Feb 21 '14 at 01:27
  • Glad I'm not going mad - I couldn't find a single reference anywhere to what I'm trying to do. At least someone else has tried it too :-) – PaulD Mar 03 '14 at 01:12
  • I've tried various techniques, and I cannot believe there's not a relevant answer to this yet. – contactmatt Mar 24 '15 at 18:41
  • Your code seems right to me. However, would you mind trying the apporoach number two from Scott Guthrie's post on modelbinding, with TryUpdateModel after getting the original from the database: http://weblogs.asp.net/scottgu/web-forms-model-binding-part-3-updating-and-validation-asp-net-4-5-series – Marcel Aug 07 '15 at 12:15
  • @Marcel, thanks for the link. I'm not actively working on this project/problem any more so don't have the code around to test it out. – PaulD Aug 20 '15 at 00:43
  • The second method does not work either. Model binding child collections just doesn't seem to work with web forms. Talk about frustrating. I've sort of gotten around it by factoring child collections into ListView outside of FormView, and then manually calling ListView.Update(index) from inside the FormView update method, but it's a poor kludge. – naasking Oct 08 '16 at 13:20
  • I'm too afraid to post this as an answer. But don't you need to instantiate the list? `_productChoice = new Models.ProductChoice() { Quantity = 1, ProductId = 1, Extras = new List() };` – Worthy7 Dec 21 '17 at 02:16

6 Answers6

1

Digging into System.Web.ModelBinding reveals that the CollectionModelBinder expects that values passed in to the FormValueProvider would be in the same format as they would be for MVC, that is: MyCollection[i]

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

Unfortunately, your repeater's element names will not match that criteria.

While certainly unorthodox, you could still achieve this by writing non-server textboxes, and then giving them a name starting with your datalist naming container, followed by the index. And thanks to "Request.Unvalidated" (also introduced in 4.5), you have the ability to databind to this data even though it's not represented by server-side controls.

seraphym
  • 1,126
  • 1
  • 8
  • 21
0

Unfortunately I do not know exactly how this is done with Web Forms so I am unsure how to reproduce this with a repeater, but in MVC the model binder requires an index to reconstruct the list. If I would have to guess how this is done in web forms, it would be something similar to this:

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

Notice I replaced the repeater with a for loop that iterates through the collection with the index used to access each Extra. This is similar to how I am required to do what you want in ASP.NET MVC. The index is posted along with the rest of the web form when the form is submitted which allows the model binder to reconstruct the ordered list of objects.

I hope this is some amount of help and forgive me for any errors, as I do not have a web forms project to test this with at the moment.

Jonathan Walton
  • 449
  • 3
  • 18
0

I've found a workaround to bind collection property of object inside UpdateMethod:

My model:

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

Controls:

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

The code behind:


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
    
}

Result:

Before TryUpdateMethod, the Webforms 4.5 Model Binding will bind to student object. Notice that the Enrollments is empty.

enter image description here

After create a IValueProvider provider and pass it to TryUpdateMethod, Enrollments count is 3(equal to the data submitted in form).

enter image description here

More details:

enter image description here


What I have found about binding to collection for edit:

  1. In your question, the reason you cannot bind data as MVC is that the auto model binding in UpdateMethod only works with (1) ASP Server Control; and (2) properties having simple types like string or int..., not complex types like collection or class object.

  2. For these complex type, you can use MVC approach as you intend to. But, I think this approach may require following conditions:

    • It only works with HTML element (and without runat=server), because of the requirement for name attribute (of <input> or <select> for example). You get what I mean? The Webform Server Controls have its own design for name properties, which is designed to fit it own model binding, which not being accepted in MVC. That's why you cannot set name properties, and if you try to change it with Jquery/JavaScript/Code-Behind, the model binding for the Webform Server Controls will not work.
    • This approach only use "MVC style" name to bind. So it will not bind the ASP Server Control style name (like $Maincontent$... thing).
    • The binding for Webform Server Controls approach and MVC approach cant be merged in one. I had to do one after one like in my example.
  3. Other less related things during my research:

  • The BindItem or Eval or Container.ItemIndex (of Repeater) only works with Webform Control like Textbox, or HTML element with runat=server. Both will change the name attribute to fit with Webform model Binding style, so you cannot use these for MVC approach.

  • In @JonathanWalter example, the counter i cannot be placed inside <%# %>. It must be put inside <%=%>. Because of this, I try many times but cannot contruct the Text as BindItem.Enrollments[i].Id for TextBox. So my hope on this was failed, sadly.

  • The MVC approach also works if I bind the collection inside onClick event of asp:button or <button>. Just make sure that the collection is put inside HTML element, not Server Control, and naming it like MVC style.

So this is my finding about the question. I spend quite a few day on this, so I feel like to post this here in case someone might want to try this MVC approach.

Thanks for reading this long post =)).

B.Green
  • 67
  • 7
-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;}
}
varkhedi
  • 13
  • 1
  • 8
-1

You should specify an Edit Item Template in an inner datalist as the Properties in the Item Template may not be returned in the model that is constructed automatically and passed to the Update method. I've not had time to try it but this should work...

<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>
Enoch Olaoye
  • 144
  • 1
  • 6
-2

Pass the extras values in query string, add parameter values in the update method. Use data attributes on HTML 5 to store those 3 values in the Form View element

example

UpdateMethod="UpdateProduct/104/coke/2"

or

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

for the first approach you have to write routing rule in Route Config.

Inside the user control like as below

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
      });
    }
}
  • 3
    I don't see how this would work for an unknown-length collection, as per his question? Note his code comments describing the problem: `it should contain two ProductChoiceExtra objects`. – Merenzo Feb 21 '14 at 01:16
  • 1
    As @Merenzo says, I don't know how many extras are available for a product at compile time - there might be none or there might be 23. – PaulD Mar 03 '14 at 01:11
  • Minus 1. This answer makes no sense when binding a collection with a variable amount of values. – contactmatt Mar 24 '15 at 19:27