Oct 28 2011

Low BandWidth Transfers with The Client Side Templates of the Mvc Controls Toolkit

Category: MVCFrancesco @ 23:07

Defining MVC Controls 2: Using the DataGrid

Defining MVC Controls 3: Datagrid, Sorting, and Master-Detail Views

Data Filtering, in the New Mvc 3 Version of the Mvc Controls Toolkit

Handling Big Amounts of Data with Client-Side Templates

Handling Big Amounts of Data with Client-Side Templates 2

This post is a tutorial on the new Client-Side templates, and on the ClientBlockRepeater of the Mvc Controls toolkit. I assume that everyone is already familiar with the Client Block feature of the Mvc Controls Toolkit.

Client-Side Templates allow a rich UI to be defined just once in a template, and then to use this template to render each of the items in a big collection contained in a Client-Side ViewModel. This way the rich interface is obtained with a low Internet BandWidth cost, since code needed to create the rich UI is sent to the client just once in a template.

Templates are defined as normal Mvc Controls Tool kit templates, by the developer, then they are rendered on the sever and transformed into client-side templates. The template is enriched with some extra code that enables both client and server side validation on the input fields that are created dynamically when the template is instantiated on each Javascript item on the client-side.Once on the client, they are instantiated with each of the data items contained into an array of the Client-Side ViewModel.

The insertion of the actual data into the template can be done statically by inserting them just once when the template is instantiated with the data item, or dynamically, in which case we create a permanent client-side binding between the data field of the data item and an html element of the template. In the case of dynamic byndings each data item acts as a ViewModel for the instance of the template that is associated to it. Dynamic instantiation of a data fields has the advantage that each time the data is changed, the html element it is bound to is automatically updated without requiring the re-instantiation of the whole template. Moreover, dynamic binding can be also two-way, in which case also modifications of the html element can be reflected back on the data field the html elements is bound. Two way bindings are necessary when we want to collect input from the user into the Client-side ViewModel.

As a conclusion dynamic bindings are more flexible, but they are also slower to render, expecially two way bindings, so if one would actually like to put on the screen thousands of items they should be planned carefully.

Suppose we want to show a list of products that the user can purchase by just inserting the quantity he would like to receive together with the desired delivery date in the same row displaying the product. This approach is quite rough and it is not efficient, however the main purpose of this tutorial is to show how client-side template works, so we will use it. In a further tutorilal we will show a better approach.

For simplicity we will just display the description of  the product. Thus our simple ViewModel description of each product is:

public class ProductView
    {
        public int Code { get; set; }
        public string Description { get; set; }
        [Range(0, 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);
            }
            
        }
    }

We put a range attribute on the Quantity, to allow a maximum purchase of 4 instances of the same product, than we added two read only properties just to define some constraints on the delivery date through the DateRangeAttribute as already explained in a previous tutorial.The Milestone attribute declares that the two Date bounds will not be rendered, and the ScriptIgnoreAttribute, avoid that they are rendered when the model is serialized in json.

Our full ViewModel is:

public class ToBuyViewModel
    {
        public List<ProductView> Products { get; set; }
        public int LastKey { get; set; }
    }

The LastKey property is needed to handle our paging technique: we put on the View an “AddNewPage” button, that when clicked add a new page of products to the ones already shown on the screen. Thus we have no page changes, but just the addition of new products…just a marketing technique to sell more products :)

Since we want to handle our products with cliemt-side templates, we define the whole View as a Client Block. Accordingly our Action method will return a ClientBlockViewResult by calling the ClientBlockView controller extension method:

return this.ClientBlockView(shopping, "productsClientView");

Where shopping is our ViewModel filled with data. We will use a ClientBlockRepeater to render our instantiated Client-Side templates as child of a tbody tag of a table:

@Html.ValidationSummary(false)
              <table >
              <thead>
                  <tr>
                  <td><strong>Product</strong></td>
                  <td><strong>Quantity</strong></td>
                  <td><strong>Delivery Date</strong></td>
                  </tr>
              </thead>
              
              @Html.ClientBlockRepeater(m => m.Products,
                   _S.H<ProductView>(
                        @<tr>
                            <td>
                            @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>
                     </tr>
                    ),
                    ExternalContainerType.tbody,
                    new { id = "ProductsContainer"})           
              </table>

As you can see the template passed to the repeater is a normal server side template. It is compiled and transformed into a client side template in a way that is completely transparent to the developer.

The bindings of the TypedTextBox with the Quantity, and the one of the  DateTimeInput with the DeliveryDate of each ProductView object in the Products collection of our Client-Side ViewModel are deined automatically by the Client Block engine of the Mvc Controls Toolkit, since the input fields are defined through expressions(the Client Block engine define automatically bindings according to the same name convention used by the default model binder). The Description is inserted into the template statically (that is once for all when the template is instantiated) with the _D helper.This helper renders its parameter enclosed in a span element and with the formatting defined in a FormatAttribute.

There is also an _F helper that displays its parameter with formatting but without the span element, and the _P helper that displays its parameter in a rough format without any formatting.

As you can see, thanks, to both the implicit binding definition of input fields performed by the Client Block engine and to The _D helper we have defined the Client-Side template in exactly the same way we would have defined a Server-Side template! This make the use of Client-Side template straigthforward and enhance the re-usability of templates.

Let’ handle paging now. We need just to ask a new page of products through an Ajax call to an action method: As a first step we build the Url string of the request. We pass a ViewHint= “json” parameter to our action method, just to say we want the answer in JSON. Since our action method has been decorated with the attribute:

[AcceptViewHint(JsonRequestBehavior.AllowGet)]

as explained here, the result of the action method is automatically transformed into a JsonResult.

At run time we add also the parameter that specifies the code of the product to start with. When data arrive we push it in our original Products collection…and miracle of the Client Templates, new templates are instantiated automatically for the new products! Finally we update, our LastKey property to be ready for the next page request.

@{
                    string nextPageUrl = Url.Action("Index", "Shopping", new { ViewHint = "json" });
                }

                <script language='javascript' type='text/javascript'>
                    $(document).ready(function () {
                        $('#Button1').bind('click', function () {
                            $.ajax({
                                url: "@MvcHtmlString.Create(nextPageUrl)" + "&startFrom=" + productsClientView.LastKey(),
                                dataType: "text",
                                success:
                                function (textData) {
                                    var data = $.parseJSON(textData);
                                    if (data != null && data.Products != null) {
                                        data = ko.mapping.fromJS(data);
                                        var unwrapped = productsClientView.Products();
                                        for (var i = 0; i < data.Products().length; i++) {
                                            unwrapped.push(data.Products()[i]);
                                        }
                                        productsClientView.Products(unwrapped);
                                        productsClientView.LastKey(data.LastKey());
                                    }
                                }
                            });
                        });
                    });

When the user clicks the “submit” button, in order to avoid sending a big amount of useless data to the server, we copy into another Client-Side ViewModel, that is located into another form, just the items with a Quantity greater than 0, and then submit this other form. This way, since we are submitting another form, we avoid sending all input fields that we have created to the server, and we avoid also sending our main Client-Side ViewModel that might have become very big after a lot of new page requests.

Below the other form with the secondary ViewModel:

@using (Html.BeginForm("Confirm", "Shopping"))
            {
                var confirmModel = Html.SAClientViewModel(
                    "confirmModel",
                    new PurchaseConfirmationView(),
                    initialSave: false,
                    applyBindings: false);
                    
            }

 

We rendered our secondary Client-Side ViewModel differently from our main Client-Side ViewModel since it is not part of the Server-Side ViewModel, but it is destined to an Action method that uses a different type of ViewModel. The SAClientViewModel helpers accepts directly an instance of an object and defines a Client ViewModel with it. Finally, below the javascriot function that fill the secondary Client-Side ViewModel:

<script language='javascript' type='text/javascript'>
        productsClientView.submitProductsChosen = function () {
            confirmModel.Products = new Array();
            for (var i = 0; i < this.Products().length; i++) {
                var quantity = this.Products()[i].Quantity();
                if (quantity != null && quantity != 0) {
                    confirmModel.Products.push(this.Products()[i]);
                }
            }
            if(confirmModel.Products.length>0)
                confirmModel.saveAndSubmit();
        };
    </script>

Now we need just to attach the javascript function that submits the form to the click event of some button. We can do this by defining manually a click binding for this button. We ask for a IBindingsBuilder interface to the helper of our View by calling the ClientBindings extension method, and use it to define our click binding:

@{
                    var clientModel = Html.ClientBindings();
                    var sumbitButtonBindings = clientModel.Click(m => m, "submitProductsChosen").Get();
                }
                <input type="button" value="Submit" data-bind='@sumbitButtonBindings'/>

 

That’s all! The full code is contained in the RazorClientTemplating file in the Mvc Controls Toolkit download page. Download it and enjoy!

                                                                    Francesco

Tags: , , , , ,

Comments