In my previous post here I introduced the client-side templates of the Mvc Controls Toolkit, and I discussed how they support both the use of server controls and validation. I pointed out also that they allow the implementation of rich UI interfaces at a low BandWidth cost since the rich UI code is sent in templates and then replicated on the client-side on several data items by exploiting the capability of templates to be instantiated on on whole collection of items.
As already discussed, instantiation of the collections is done substantially, either by substituting “holes” in the template with the actual data contained in each item once for all when the template is transformed into html nodes, or by creating bindings between item properties and template html elements. Bindings grant that modifications of data items performed after the template has been instantiated propagate immediately to the bound html elements, and vice versa that input provided by the user into html elements are propagated immediately to the data items they are bound to. However, bindings have an higher cost in terms of computational resources, especially if the html elements involved in the binding are complex server controls, like for instance a DateTime input.
As a consequence, one should avoid using input fields bound to data items when dealing with big collections of thousands of items. This can be achieved, by allowing the user to select the collection items he would like to operate on, as a first step. Once the item has been selected a new template containing all desired input field is instantiated. Since the user will probably select just a few of all listed items, this way we avoid any performance issue. Moreover, if we group all selected items in a different area of the page we help the user to manage properly all items he decided to operate on without being forced to look for them among thousands of other items.
To show how all this can be implemented we modify adequately the example of my previous post. To download the full code of the example, please, go to the download area of the Mvc Controls Toolkit and download the file: RazorClientTemplating.
Now we need two ViewModel versions of the product class, the first one to be used for display only purposes, and the other one to be used once the user has selected a product and needs to specify the quantity, and delivery date:
public class ProductViewBase
{
public int Code { get; set; }
public string Description { get; set; }
}
public class ProductView : ProductViewBase
{
[Required, Range(1, 4), Display(Name="Quantity", Prompt="Insert Quantity")]
public int? Quantity { get; set; }
[DateRange(DynamicMinimum="MinDeliveryDate", DynamicMaximum="MaxDeliveryDate")]
public DateTime DeliveryDate { get; set; }
[MileStone, ScriptIgnore]
public DateTime MaxDeliveryDate
{
get
{
return DateTime.Today.AddMonths(2);
}
}
[MileStone, ScriptIgnore]
public DateTime MinDeliveryDate
{
get
{
return DateTime.Today.AddDays(2);
}
}
}
Since now the ProductView class is used only for the products that have been selected by the user, we modified the RangeAttribute to require a minimum Quantity of 1: if the user changes his mind and he doesn’t want the product anymore he can just delete it from the list of selected products
We need two Client-Side ViewModel. The first one for handling the list of all products with some kind of paging:
public class ToBuyViewModel
{
public List<ProductViewBase> Products { get; set; }
public int LastKey { get; set; }
public DateTime DefaultDeliveryDate { get; set; }
}
The LasteKey integer is used as in my previous post to enable the user to load more products pages in the View through Ajax, while DefaultDeliveryDate is the delivery date that is suggested to the user immediately after he selects a product.
The other model handles the selected products and is submitted to the Confirm action method that handles the purchase confirmation:
public class PurchaseConfirmationView
{
public List<ProductView> Products { get; set; }
public bool PurchaseConfirmed { get; set; }
public bool GoToList { get; set; }
}
It contains the list of all selected products and two booleans that encode some user choices. Once the user has selected all products the above Client-Side ViewModel is submitted to the Confirm action method that will handle the purchase confirmation process allowing the user either to cancel the purchase and returns to the main purchase page or to confirm the purchase by means of the PurchaseConfirmed and GoToList booleans. Below the Confirm action method:
[HttpPost, System.Web.Mvc.OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult Confirm(PurchaseConfirmationView purchase)
{
if (purchase.GoToList)
return RedirectToAction("Index");
if (ModelState.IsValid)
{
if (purchase.PurchaseConfirmed)
{
//insert here actual purchase locgics
return View("PurchaseConfirmed", purchase);
}
else
{
purchase.GoToList = false;
purchase.PurchaseConfirmed = false;
return this.ClientBlockView("PurchaseConfirmation", purchase, "confirmModel");
}
}
else
{
purchase.GoToList = false;
purchase.PurchaseConfirmed = false;
return this.ClientBlockView("PurchaseConfirmation", purchase, "confirmModel");
}
}
As you can see the Confirm Action returns a ClientBlockViewResult, thus defining the whole Confirmation View as a Client Block. Infact the Confirmation View is completely handled on the Client-Side, since it contains just a ClientBlockRepeater showing all products selected by the user.
The PurchaseConfirmationView ViewModel is rendered in the main view in a separate form whose submit target is the Confirm action method, and it it is filled with the new products selected by the user. To handle it we define a different Client Block by means of the Template extension method:
<div id="ProductsToBuy">
<h2>Products Chosen</h2>
<br /><br /><br />
@using (Html.BeginForm("Confirm", "Shopping"))
{
@Html.Template(new PurchaseConfirmationView { Products = new List<ProductView>() },
_S.H<PurchaseConfirmationView>(ToBuyInnerBlock),
true,
"confirmModel",
"ProductsToBuy")
}
</div>
The third parameter of the Template extension method is set to true, to declare we are defining a new Client Block, the first parameter is the object we would like to use as Client ViewModel, “confirmModel” is the name of the global javascript variable where to put the Client ViewModel, “ProductsToBuy” is the id of the div acting as Root of he Client Block. Finally, ToBuyInnerBlock is the name of the template containing the whole code of the Client Block; we defined it with a Razor Helper:
@helper ToBuyInnerBlock(HtmlHelper<PurchaseConfirmationView> Html)
{
@Html.ValidationSummary(false)
<div class="ToScroll">
<table >
<thead>
<tr>
<td><strong>Product</strong></td>
<td><strong>Quantity</strong></td>
<td><strong>Delivery Date</strong></td>
<td><strong>Delete</strong></td>
</tr>
</thead>
@Html.ClientBlockRepeater(m => m.Products,
_S.H<ProductView>(
@<tr>
<td>
@{var itemBindings = item.ItemClientViewModel();}
@item._D(m => m.Description)
</td>
<td>
@item.TypedTextBoxFor(m => m.Quantity)
@item.ValidationMessageFor(m => m.Quantity, "*")
</td>
<td>
@item.DateTimeFor(m => m.DeliveryDate, DateTime.Today,
false).Date()
</td>
<td><a href="#" data-bind="click: function() { confirmModel.Products.remove($data) }">Delete</a></td>
</tr>
),
ExternalContainerType.tbody,
new { id = "ProductsToBuyContainer" },
null,
"ProductsToBuyTemplate")
</table>
</div>
}
We added manually a Click binding to remove a product from the list of products to buy. It uses the remove method of the observableArray class to remove the product from the Products observable array. The argument of the remove method is $data that is substituted with the actual data item that is associated to the template when the template is instantiated. The anonymous object passed as third argument of the repeater just add an Html id attribute to the tbody tag that will be created as container of all instantiated templates, while the last argument of the repeater “ProductsToBuyTemplate” just gives a custom name to the Client Template that will be automatically created by the repeater. In our case this is a necessity, because the standard PrefixedId(m =>m.Products)+”_Template” name that is automatically created by the repeater would have collided with the analogous name of the template for the whole list of products to choose.
In fact the page is divided into two columns, the left column contains all products and the right column the products the user have chosen to buy. The two columns correspond to two different Client Blocks. The root of the Client Block containing the products to buy has been specified in the Template extension method to be the div with id “ProductsToBuy”. The root of the first Client Block that contains all product is specified in the Action method, that returns a ClientBlockViewResult, to be an Html node with id “ProductsToList”:
return this.ClientBlockView(shopping, "productsClientView", "ProductsToList");
Below the first Client Block:
<div id="ProductsToList">
<h2>Products List</h2>
<br /><br /><br />
@using (Html.BeginForm("Index", "Shopping"))
{
<div id="automaticLoad" class="ToScroll">
<div id="automaticLoadContent">
@Html.ValidationSummary(false)
<table >
<thead>
<tr>
<td><strong>Product</strong></td>
<td><strong>Price</strong></td>
<td><strong>Select Product</strong></td>
</tr>
</thead>
@Html.ClientBlockRepeater(m => m.Products,
_S.H<ProductViewBase>(
@<tr>
<td>
@item._D(m => m.Description)
</td>
<td>
@item._D(m => m.Price)
</td>
<td><a href="#" data-bind="click: function() { productsClientView.buyProduct($data) }">Buy</a> </td>
</tr>
),
ExternalContainerType.tbody,
new { id = "ProductsContainer" })
</table>
Also here we added manually a Click binding. In this case the click handler calls the javascript function that adds a new product to the list of products to buy:
<script language='javascript' type='text/javascript'>
productsClientView.buyProduct = function (product) {
confirmModel.Products.push(
{
Quantity: ko.observable(1),
Code: product.Code,
Description: product.Description,
Price: product.Price,
DeliveryDate: ko.observable(this.DefaultDeliveryDate())
}
);
};
</script>
Description and Code are not defined as observables since they will not be bound to any html element with a permanent binding but they will just fill predefined “holes” in the template.Quantity, instead, is defined as an observable and is given the initial value 1, since it will be bound to an input field. DeliveryDate, too, is defined as an observable since it will be bound to a DateTime input. Please notice, that, since it is copied from the DefaultDeliveryField of the first ViewModel, this date is already defined as an observable. However, we are forced to unwrap it from the original observable and place it into a new observable because if we use an unique observable for all dates all dates are forced to have the same value.
The divs named automaticLoad and automaticLoadContent before the repeater handles both the scrolling of the left column and the automating loading of a new page of products that is triggered automatically when the user move the scrollbar to the bottom:
<script language='javascript' type='text/javascript'>
$(document).ready(function () {
$('#automaticLoad').scroll(function () {
if ($('#automaticLoad').scrollTop() >= $('#automaticLoadContent').height() - $('#automaticLoad').height()) {
$.getJSON("@MvcHtmlString.Create(nextPageUrl)" + "&startFrom=" + productsClientView.LastKey(), function (data) {
if (data != null && data.Products != null) {
data = ko.mapping.fromJS(data);
for (var i = 0; i < data.Products().length; i++) {
productsClientView.Products.push(data.Products()[i]);
}
productsClientView.LastKey(data.LastKey());
}
});
}
});
});
</script>
That’s all! Download the full code from the download area of the Mvc Controls Toolkit and enjoy! The file is named: RazorClientTemplating
Francesco
Tags: MVC Controls Toolkit, Knockout, Client Side ViewModel, Templates, Bindings