top button
Flag Notify
    Connect to us
      Site Registration

Site Registration

Model Binding To List Of Objects In ASP.NET MVC

+3 votes
555 views

Showing a single record for editing is quite common and the default model binding of ASP.NET MVC takes care of mapping the form fields to the model properties. However, sometimes you need to edit multiple records. For example, you may want to display an editable grid to the end user filled with existing data. The user can edit the values from multiple rows and hit Save in an attempt to save the data. In this case multiple model objects are being submitted to the action method. The single record editing works on the assumption that form field names from the view match the corresponding model property names. However, when multiple model objects are submitted this assumption is no longer valid. Luckily, by tweaking the form field names you can get this to work as expected. Let's see how.

Begin by creating a new ASP.NET MVC Application. Then right click on the Models folder and add an ADO.NET entity framework data model to it. Configure the model to use Customers table of the Northwind database. The following figure shows this model:

image

Then add HomeController in the Controllers folder. Modify the default Index() action method as shown below:

public ActionResult Index()
{
  NorthwindEntities db=new NorthwindEntities();
  var query = from c in db.Customers
              where c.Country=="UK"
              orderby c.CustomerID
              select c;
  return View(query.ToList());
}

The Index() action method simply selects all the customers from UK and passes them to the Index view as a List of Customer entities.

Then right click on the Index() action method and add Index view. The Index view is where you need to follow certain naming convention to get the desired results:

@model List<ModelBindingToListDemo.Models.Customer>
...
    <h1>List of Customers</h1>
    @using (Html.BeginForm("Index", "Home", FormMethod.Post))
    {
        <table border="1" cellpadding="6">
            @for (int i = 0; i < Model.Count;i++ )
            { 
                <tr>
                    <td>@Html.TextBox("customers[" + @i + "].CustomerID", 
                                      Model[i].CustomerID, 
                                      new { @readonly = "readonly" })</td>
                    <td>@Html.TextBox("customers[" + @i + "].CompanyName", 
                                      Model[i].CompanyName)</td>
                    <td>@Html.TextBox("customers[" + @i + "].ContactName", 
                                      Model[i].ContactName)</td>
                    <td>@Html.TextBox("customers[" + @i + "].Country", 
                                      Model[i].Country)</td>
                </tr>
            }
            <tr>
                <td colspan="4">
                    <input type="submit" value="Submit" />
                </td>
            </tr>
        </table>
    }
...

Notice the markup shown in the bold letters. The code is basically generating names for the textboxes matching the following convention:

customers[n].<Model_Property_Name>

Where n is an index starting from 0 and Model_Property_Name is the name of the properties such as CustomerID, CompanyName, ContactName and Country. For the sake of simplicity the above code uses only four properties form the Customer model class. So, all the textboxes having same index are considered as "one record". This naming convention is required to successfully bind data to the model as you will see later.

The following figures shows how the view looks like in the browser:

image

To see how the textbox names are being generated view the HTML source in the browser.

The above <form> submits the data to Index() method using post method. To handle this data write the second version of Index() action method as follows:

[HttpPost]
public ActionResult Index(List<Customer> customers)
{
  NorthwindEntities db=new NorthwindEntities();
  foreach (Customer cust in customers)
  {
    Customer existing = db.Customers.Find(cust.CustomerID);
    existing.CompanyName = cust.CompanyName;
    existing.ContactName = cust.ContactName;
    existing.Country = cust.Country;
  }
  db.SaveChanges();
  return View();
}

The overloaded Index() method takes a parameter - List of Customer entities. Recollect that this parameter name - customers - is what you used in the view markup earlier. Due the naming conventions followed the model binding framework of ASP.NET MVC transforms the form field values into a generic List of Customer objects. Once received you simply iterate through the List and modify the existing Customer with the new one. You can also put some logic to detect whether a record was really changed or not. Once all the rows are modified SaveChanges() is called to save the changes.

As mentioned earlier the naming convention requires that the index start at 0 and then sequentially increment for each record. If you try changing the start index to say 10, the model binding will fail to bind the data. What if you don't want to start the index from 0? For example, imagine a case where you are removing some row using client side script. In such cases the there might be "gaps" in between various index values. To overcome this situation you can follow an alternate naming convention:

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
  <table border="1" cellpadding="6">
  @for (int i = 0; i < Model.Count;i++ )
  { 
    <tr>
      <td>
      @Html.Hidden("customers.Index", (@i + 10))
      @Html.TextBox("customers[" + (@i + 10) + "].CustomerID", 
                    Model[i].CustomerID, new { @readonly = "readonly" })
      </td>
      <td>@Html.TextBox("customers[" + (@i + 10) + "].CompanyName", 
                        Model[i].CompanyName)</td>
      <td>@Html.TextBox("customers[" + (@i + 10) + "].ContactName", 
                        Model[i].ContactName)</td>
      <td>@Html.TextBox("customers[" + (@i + 10) + "].Country", 
                        Model[i].Country)</td>
    </tr>
  }
<tr>
...
}

Notice the above markup carefully. Each table row now has a hidden form field. The name of the hidden form field is customers.Index and its value is set to some arbitrary index (i + 10 in this case). Then all the textboxes are assigned names of the form:

 customers[<arbitrary_index>].<model_property_name>

In this case all the textboxes having same index as specified by the hidden field are considered as "one record". In this case the index need not be a number. It can be a string also. Again, recollect that "customers" in the above markup is the name of the parameter of the Index() method.

That's it! Run the application and test if it works as expected.

posted Oct 4, 2016 by Shivaranjini

  Promote This Article
Facebook Share Button Twitter Share Button LinkedIn Share Button


Related Articles

ASP.NET model binding is quite powerful and flexible. It caters to most of the scenarios without much configuration from developers. However, at times you may need to intervene in order to achieve the desired model binding effect. One such situation is when you use multiple instance of a partial view on a view. This article shows one possible approach to deal with such situations.

Suppose that you have a web page as shown below:

image

As shown in the above figure the web page captures OrderID, CustomerID, ShippingAddress and BillingAddress from the end user. This information is stored in a model class - Order - that looks like this:

public class Order
{
    public int OrderID { get; set; }
    public int CustomerID { get; set; }

    public Address ShippingAddress { get; set; }
    public Address BillingAddress { get; set; }
}

public class Address
{
    public string Street1{get;set;}
    public string Street2{get;set;}
    public string Country{get;set;}
    public string PostalCode{get;set;}
}

The Order class consists of four public properties namely OrderID, CustomerID, ShippingAddress and BillingAddress. Notice that OrderID and CustomerID are integer properties whereas ShippingAddress and BillingAddress properties are of type Address. The Address class is also shown and consists of four string properties - Street1, Street2, Country and PostalCode.

Now let's assume that the whole page is rendered using two ASP.NET MVC Partial Pages. The OrderID and CustomerID is captured using _BasicDetails.cshtml as shown below:

@model Demo.Models.Order

<table>
    <tr>
        <td>@Html.LabelFor(m=>m.OrderID)</td>
        <td>@Html.TextBoxFor(m=>m.OrderID)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.CustomerID)</td>
        <td>@Html.TextBoxFor(m=>m.CustomerID)</td>
    </tr>
</table>

Note that _BasicDetails partial page has its model set to Order class. The partial page then uses LabelFor() and TextBoxFor() helpers to display a label and textbox for the OrderID and CustomerID model properties respectively.

The address information is captured using _Address.cshtml as shown below:

@model Demo.Models.Address

<table>
    <tr>
        <td>@Html.LabelFor(m=>m.Street1)</td>
        <td>@Html.TextBoxFor(m=>m.Street1)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.Street2)</td>
        <td>@Html.TextBoxFor(m=>m.Street2)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.Country)</td>
        <td>@Html.TextBoxFor(m=>m.Country)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.PostalCode)</td>
        <td>@Html.TextBoxFor(m=>m.PostalCode)</td>
    </tr>
</table>

The _Address partial page has Address class as its model and uses LabelFor() and TextBoxFor() helpers to display model properties.

The Index view that makes use of _BasicDetails and _Address partial pages to form the complete page is shown below:

@model Demo.Models.Order

...
@using(Html.BeginForm("ProcessForm","Home",FormMethod.Post))
{
  <h3>Basic Details</h3>
  @Html.Partial("_BasicDetails")

  <h3>Shipping Address</h3>
  @Html.Partial("_Address",Model.ShippingAddress)
        
  <h3>Billing Address</h3>
  @Html.Partial("_Address",Model.BillingAddress)
        
  <input type="submit" value="Submit" />
}
</body>
</html>

The Index view renders the _BasicDetails partial page using Partial() helper. Since the model for Index view is Order class, the same is available to the _BasicDetails partial page. Then two instances of _Address partial page are rendered on the page to capture ShippingAddress and BillingAddress respectively. Recollect that _Address has Address class as its model. So, Model.ShippingAddress and Model.BillingAddress are passed to the Partial() helper.

The above form submits to ProcessForm action method that looks like this:

public ActionResult ProcessForm(Order ord)
{
    //do something with Order object here
    return View("Index");
}

And the Index() action method looks like this:

public ActionResult Index()
{
    Order ord = new Order();
    ord.BillingAddress = new Address();
    ord.ShippingAddress = new Address();
    return View(ord);
}

Both of these methods are quite straightforward and need no explanation.

Now comes the important and tricky part. If you run the application at this stage, you will get the following HTML markup in the browser (unwanted markup has been removed for the sake of clarity):

<form action="/Home/ProcessForm" method="post">        
<h3>Basic Details</h3>
<table>
    <tr>
        <td><label for="OrderID">OrderID</label></td>
        <td><input id="OrderID" name="OrderID" type="text" /></td>
    </tr>
    <tr>
        <td><label for="CustomerID">CustomerID</label></td>
        <td><input id="CustomerID" name="CustomerID" type="text" /></td>
    </tr>
</table>
<h3>Shipping Address</h3>
<table>
    <tr>
        <td><label for="Street1">Street1</label></td>
        <td><input id="Street1" name="Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Street2">Street2</label></td>
        <td><input id="Street2" name="Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Country">Country</label></td>
        <td><input id="Country" name="Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="PostalCode">PostalCode</label></td>
        <td><input id="PostalCode" name="PostalCode" type="text" value="" /></td>
    </tr>
</table>
<h3>Billing Address</h3>
<table>
    <tr>
        <td><label for="Street1">Street1</label></td>
        <td><input id="Street1" name="Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Street2">Street2</label></td>
        <td><input id="Street2" name="Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Country">Country</label></td>
        <td><input id="Country" name="Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="PostalCode">PostalCode</label></td>
        <td><input id="PostalCode" name="PostalCode" type="text" value="" /></td>
    </tr>
</table>
<input type="submit" value="Submit" />
</form>

Notice the markup in bold letters. Can you see HTML elements with duplicate id and name attributes? That's because you are rendering two instance of the _Address partial page. The model binding framework requires that the HTML fields follow this naming convention for the model binding to work as expected:

<input id="ShippingAddress_Street1" 
       name="ShippingAddress.Street1" type="text" value="" />
<input id="BillingAddress_Street1" 
       name="BillingAddress.Street1" type="text" value="" />

As you can see from the above markup the id and name attributes must fully quality the model property being bound. In the absence of such a naming pattern the Order instance won't be bound as expected as confirmed by the following figure:

image

As shown above the ShippingAddress and BillingAddress properties are null whereas OrderID and CustomerID are captured successfully.

The above problem can be solved by using a variation of the Partial() helper while rendering the _Address partial page. The following code shows how this is done:

<h3>Basic Details</h3>
@Html.Partial("_BasicDetails")

<h3>Shipping Address</h3>
@Html.Partial("_Address", 
  new ViewDataDictionary() 
  { 
    TemplateInfo = new TemplateInfo() 
      { HtmlFieldPrefix = "ShippingAddress" } })

<h3>Billing Address</h3>
@Html.Partial("_Address", 
  new ViewDataDictionary() 
    { TemplateInfo = new TemplateInfo() 
      { HtmlFieldPrefix = "BillingAddress" } })

The variation of Partial() helper used above uses ViewDataDictionary parameter to specify TemplateInfo. The HtmlFieldPrefix property of the TemplateInfo is set to ShippingAddress for the first instance and to the BillingAddress for the second instance.

If you run the application now, you will find the following markup in the browser:

<form action="/Home/ProcessForm" method="post">
<h3>Basic Details</h3>
<table>
    <tr>
        <td><label for="OrderID">OrderID</label></td>
        <td><input id="OrderID" name="OrderID" type="text" value="0" /></td>
    </tr>
    <tr>
        <td><label for="CustomerID">CustomerID</label></td>
        <td><input id="CustomerID" name="CustomerID" type="text" value="0" /></td>
    </tr>
</table>
<h3>Shipping Address</h3>
<table>
    <tr>
        <td><label for="ShippingAddress_Street1">Street1</label></td>
        <td><input id="ShippingAddress_Street1" 
                   name="ShippingAddress.Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_Street2">Street2</label></td>
        <td><input id="ShippingAddress_Street2" 
                   name="ShippingAddress.Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_Country">Country</label></td>
        <td><input id="ShippingAddress_Country" 
                   name="ShippingAddress.Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_PostalCode">PostalCode</label></td>
        <td><input id="ShippingAddress_PostalCode" 
                   name="ShippingAddress.PostalCode" type="text" value="" /></td>
    </tr>
</table>
<h3>Billing Address</h3>
<table>
    <tr>
        <td><label for="BillingAddress_Street1">Street1</label></td>
        <td><input id="BillingAddress_Street1" 
                   name="BillingAddress.Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_Street2">Street2</label></td>
        <td><input id="BillingAddress_Street2" 
                   name="BillingAddress.Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_Country">Country</label></td>
        <td><input id="BillingAddress_Country" 
                   name="BillingAddress.Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_PostalCode">PostalCode</label></td>
        <td><input id="BillingAddress_PostalCode" 
                   name="BillingAddress.PostalCode" type="text" value="" /></td>
    </tr>
</table>
<input type="submit" value="Submit" />
</form>

As expected the id and name attributes are now fully qualified and hence the model binding will happen as expected as shown below:

image

The model binding now correctly captures ShippingAddress as well as BillingAddress information.

READ MORE

In this section, you will learn about the Model in ASP.NET MVC framework.

Model represents domain specific data and business logic in MVC architecture. It maintains the data of the application. Model objects retrieve and store model state in the persistance store like a database.

Model class holds data in public properties. All the Model classes reside in the Model folder in MVC folder structure.

Let's see how to add model class in ASP.NET MVC.

Adding Model:

Open our first MVC project created in previous step in the Visual Studio. Right click on Model folder -> Add -> click on Class..

In the Add New Item dialog box, enter class name 'Student' and click Add.

Create Model Class

Create Model Class

This will add new Student class in model folder. Now, add Id, Name, Age properties as shown below.

Example: Model class

namespace MVC_BasicTutorials.Models
{
    public class Student
    {
        public int StudentId { get; set; }
        public string StudentName { get; set;  }
        public int Age { get; set;  }
    }
}


So in this way, you can create a model class which you can use in View. You will learn how to implement validations using model later.

Learn how to create a View in the next section.

READ MORE

ASP.NET model binding framework takes care of validating a model based on data annotation validations. This works well when a model is being bound with request data. However, at times you may need to create and fill a model programmatically. In such cases, although the model properties are decorated with data annotation validators, they won't validate the data because they are not invoked at all. Consider a situation wherein users are uploading CSV files from the client machine on to the server. Your application is supposed to read those files and assign the values from the file to the model properties. The model objects are then stored to some database. In this case data validations won't fire and your model will be held in an invalid state. Since model validations are not being invoked ModelState.IsValid will always return true and there won't be any easy way to detect and display these validation failures. Luckily, ASP.NET MVC allows you to validate a model object via code. This article shows how.

Let's assume that you have a UserInfo model class as shown below:

public class UserInfo
{
    [Required]
    [StringLength(100, MinimumLength = 10)]
    public string FirstName { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 10)]
    public string LastName { get; set; }

    [Required]
    public DateTime BirthDate { get; set; }
}

The UserInfo class consists of three properties - FirstName, LastName and BirthDate. All the model properties are decorated with [Required] attribute. Additionally, FirstName and LastName properties are decorated with [StringLength] attribute. Both of these properties also have MinimumLength set to 10 just for the sake of testing.

Now consider a view (Index.cshtml) as shown below:

image

The Index view shown above consists of a file upload control and a button. You are supposed to select a CSV file that contains data suitable for UserInfo model. Upon selecting a CSV file and clicking the Upload button success or error message will be displayed in another view (UploadResult.cshtml). A sample CSV file would look like this:

Nancy,Davolio,1/2/1960

The HTML markup that makes Index.cshtml is shown below:

<h1>Select file to be uploaded:</h1>
<form action="/home/savedata" method="post" 
enctype="multipart/form-data">
    <input type="file" name="file1" />
    <input type="submit" value="Upload" />
</form>

The <form> posts the file to SaveData() action method. The SaveData() action does the job of reading the uploaded file, instantiating a model object, setting model properties and then displaying UploadResult view with a success or error message. For example, the following figure shows UploadResult view when the UserInfo model is in invalid state:

image

Notice that the UploadResult view displays validation errors through ValidationSummary() helper.

Now let's see the most important part of the application - SaveData() action method.

public ActionResult SaveData()
{
  if(Request.Files.Count>0)
  {
    HttpFileCollectionBase files = Request.Files;
    foreach(string key in Request.Files)
    {
     HttpPostedFileBase item = Request.Files[key];
     byte[] data = new byte[item.InputStream.Length];
     int byteCount = item.InputStream.Read(data, 0, data.Length);
     string strData = ASCIIEncoding.UTF8.GetString(data).Trim();
     string[] values = strData.Split(',');

     UserInfo info = new UserInfo();
     info.FirstName = values[0];
     info.LastName = values[1];
     info.BirthDate = DateTime.Parse(values[2]);

     if(TryValidateModel(info))
     {
       //save in database here
       ViewBag.Message = "User information is 
                     validated successfully!";
     }
     else
     {
       ViewBag.Message = "Error while validating user info. 
           Please review the errors below 
           and upload the file again!";
     }
     return View("UploadResult", info);
   }
 }
 return View("Index");
}

The SaveData() action iterates through the Request.Files collection and gets hold of the file being uploaded. In our example it is assumed that only one will be uploaded. You don't need to save this file on the server because your interest is in the CSV data in the file. That's why the code reads the content of InputStream of the file using Read() method. The Read() method reads the content in a byte array. This byte array is converted into a string using GetString() method of the ASCIIEncoding class. The string data is further split into a string array using Split() method.

Notice the code marked in bold letters. The code creates a UserInfo object and assigns FirstName, LastName and BirthDate properties from the string array created earlier. If you see the sample data shown earlier you will realize that FirstName and LastName doesn't meet the requirement of [StringLength] attribute. However, at this stage ModelState.IsValid will be true because data validations are not invoked yet.

image

Next, the code uses TryValidateModel() method that invokes the data validations. There are two variations of this method - ValidateModel() and TryValidateModel(). Both of them come from the Controller base class. The former method throws an exception if there are any validation errors whereas the later method silently returns false in case validations fail. If you check ModelState.IsValid after the call to TryValidateModel() you will see that it returns false because there are validation errors.

image

The SaveData() action sends UserInfo model object to the UploadResult view so that validation errors can be displayed. The UploadResult view contains this HTML markup:

@model StProcInCodeFirstDemo.Models.UserInfo
...
<body>
    @ViewBag.Message
    <br /><br />
    @Html.ValidationSummary()
    <br /><br />
    @Html.ActionLink("Upload again!","Index")
</body>
...

Notice that UploadResult view uses ValidateSummary() helper to display the validation error messages. As you might be aware ValidationSummary() or ValidationMessage() helpers work only if ModelState dictionary contains some entries. In our example these entries are added by TryValidateModel() method. Since ModelState dictionary contains some entries the ValidationSummary() iterates through them and displays the validation errors.

That's it! Try running the application a couple of times with invalid as well as valid data and see if everything works as expected.

READ MORE
...