Multiple object bindings on MVC view
Multiple object bindings in MVC are the common requirement when working with more advanced UI. An example of that can be creating order form with multiple order items being added dynamically by the user.
In such a scenario we can create binding for our “Order” model as normal and create partial view with the structure of our “OrderItem” class separately. By doing this, the user will be able to dynamically preload order items using ajax request, appending created html to div container.
When submitting the form, order items will be automatically bound to the nested property list, provided the html control ids and names will follow the convention: name of the list property, index id and property name; id=’OrderItems_ID__Quantity’
For example:
<input type="number" id='OrderItems_@(Model.OrderItemId)__Quantity' value="@Model.Quantity" name='OrderItems[@Model.OrderItemId].Quantity'/>
Lets create our order and order items classes first
public class Order { public int OrderId { get; set; } [Required] [Display(Name = "Order Person")] public string OrderPerson { get; set; } [Required] [Display(Name = "Delivery Address")] public string DeliveryAddress { get; set; } public string OrderComments { get; set; } public DateTime OrderDate { get; set; } public double TotalNet { get; set; } public double TotalVAT { get; set; } public double TotalAmount { get; set; } [Required] public List<OrderItem> OrderItems { get; set; } } public class OrderItem { public int OrderItemId { get; set; } [Required] public string ItemName { get; set; } [Required] public int Quantity { get; set; } [Required] public double? UnitPrice { get; set; } public double VATRate { get; set; } public double TotalValue { get; set; } }
Our Order form will look the standard way, plus extra element for dynamically added elements:
<div class="form-group"> <b>@Html.ActionLink("Add item", "LoadBlankItemRow", null, new { id = "addOrderItem", @class = "btn btn-warning" })</b> <div style="padding-top: 10px;"> <p class="itemColumn">Quantity</p> <p class="itemColumn">Name</p> <p class="itemColumn">Unit Price</p> <p class="itemColumn">VAT Rate</p> <p class="itemColumn">Net Value</p> <p class="itemColumn"></p> </div> <div id="editorRows"> @foreach (var itemModel in Model.OrderItems) { @Html.Partial("_OrderItem", itemModel) } </div> </div>
When user clicks the Add item link, the controller action is being invoked returning html structure reflecting the list item model.
[HttpGet] public virtual PartialViewResult LoadBlankItemRow(int id) { var orderItem = new OrderItem { OrderItemId = id, Quantity = 1 }; return PartialView("_OrderItem", orderItem); }
We will use jquery to get the current numbers of items already inserted and define the current index. This will be used to create appropriate control names required for nested model bindings:
$("#addOrderItem").click(function (e) { var itemIndex = $("#editorRows input.iHidden").length; $.get("@Url.Action("LoadBlankItemRow", "Order")/" + itemIndex, function (data) { $("#editorRows").append(data); }); return false; });
Each dynamically inserted row will have JavaScript logic to remove the current row and recalculate total values:
$("a.deleteRow").click(function () { $(this).parents("#itemRow").remove(); calculateTotals(); return false; }); $('#OrderItems_@(Model.OrderItemId)__Quantity').blur(function () { updateTotalValue(@Model.OrderItemId); }); $('#OrderItems_@(Model.OrderItemId)__UnitPrice').blur(function () { updateTotalValue(@Model.OrderItemId); }); $('#OrderItems_@(Model.OrderItemId)__VATRate').change(function () { updateTotalValue(@Model.OrderItemId); });
Finally, scripts included on parent model form will calculate and update controls located outside the partial view, providing Total amount for the entire order.
function updateTotalValue(id) { var qnt = $('#OrderItems_' + id + '__Quantity').val(); var uprc = $('#OrderItems_' + id + '__UnitPrice').val(); var vat = parseFloat($('#OrderItems_' + id + '__VATRate').val()); if (vat == 0) { val = (qnt * uprc); } else { val = (qnt * uprc) * (1 + vat / 100); } if (qnt == 0 || uprc == 0) { val = 0; } $('#OrderItems_' + id + '__TotalValue').val(val.toFixed(2)); calculateTotals(); } function calculateTotals() { var totalNet = 0.0; var totalVat = 0.0; $("#editorRows").children().each(function (index) { var qnt = $('#OrderItems_' + index + '__Quantity').val(); var uprc = $('#OrderItems_' + index + '__UnitPrice').val(); var vat = parseFloat($('#OrderItems_' + index + '__VATRate').val()); if (qnt != null && qnt != 'NaN') { totalNet += qnt * uprc; if (vat != 0) { totalVat += parseFloat((qnt * uprc) * (vat / 100)); } } }); $('#TotalNet').val(totalNet.toFixed(2)); $('#TotalVAT').val(totalVat.toFixed(2)); $('#TotalAmount').val((totalNet + totalVat).toFixed(2)); }
I have included working project below. Enjoy!