Client Blocks.

Requires MVCControlToolkit.Controls.x.x.x.js or the smaller MVCControlToolkit.Controls.Core.x.x.x.js, knockout.all.x.x.x.min.js and  MvcControlToolkit.Utils-x.x.x.min.js  This feature is based on the knockout library.

See here: http://knockoutjs.com/documentation/introduction.html.  

Requires the inclusion of the namespace: MVCControlsToolkit.Controls.Bindings

Attention: MvcControlToolkit.Utils-x.x.x.min.js must be placed AFTER knockout.all.x.x.x.min.js, since it checks if knockout.js features have been installed.

A whole View or a part of it (with all its children partial views,  and templates), can be marked as Client Block. When this is done the part of ViewModel associated to the Client Block is rendered in javascript, and it is bound to the Html of the Client Block according to the same name conventions used by the default model binder: modification done on the client side view model are immediately reflected on the bound Html nodes, and vice versa. When the page is submitted the client side view model is serialized and sent to the server that re-inserts it in its original place within the server side ViewModel.

A Client Block is defined by three information:

  1. The part of ViewModel that will be rendered on the client side as Client ViewModel
  2. The Html Nodes that will be part of the Client Block and that will be bound to the Client View Model
  3. The form that will be used to submit the Client ViewModel.

Each construct that defines a Client Block specifies explicitely the Client ViewModel either as a part of the actual page ViewModel, or as an isntance of any class. The second approach is followed when the page will be submitted to an action method that uses a ViewModel that is different from the ViewModel of the View containing the Client Block. 

The Html nodes included in the Client Block are specified through the id of an Html node, called the Root of the Client Block. The tree of all descendants of the the Root of the Client Block is called Client Block Tree. Not all nodes of the Client Block Tree are part of the Client Block: just the ones whose names were obtained through an expression referring to the Client ViewModel will be included in the client block. This expression may be either a lamda expression or a text expression of the type Property.SubProperty....,that targets a property contained in the chosen ViewModel. All nodes referring to the Client ViewModel MUST be descendant of the Root of the Client Block. This means that the Client Block Tree may have holes containing input fields that doesn't belong to the Client Block. For instance all input fields rendered through a call to  Action, or RenderAction performed within a Client Block Tree, do not belong to any ClientBlock of the View because they refer to another ViewModel, specifically the ViewModel of the action method that whose invoked. The picture below gives a graphical representation of a Client Block Tree:

Client Block Tree

A View may include several Client Blocks, but, though each Client Block Tree may have holes made of nodes that do not belong to any Client Block, Client Block Trees of diffferent Client Blocks MUST NOT overlap.

The form used to submit the Client ViewModel is the one the Client Block whose defined in, if the Client ViewModel was defined in a View. If the Client Block is a whole View and consequently the ClientBlock is defined through an Action Result the form used to submit the Client ViewModel is the first form defined in the View.

Both Validation and Metadata work in Client Blocks as in normal Views.

The same form may include several Client Blocks, if the associated Client ViewModels are parts of a bigger server side ViewModel. When the form is submitted all its Client ViewModels are included in their right places in the bigger server side ViewModel.

There are two way to define a Client Block:

  1. By calling the TemplateFor helper with the clientModel parameter set to true in a View. In this case the Client Block use the object specified as first argument as Client ViewModel, and it usually includes just the Html nodes defined in the template passed as second argument.
  2. By calling the ClientBlockView controller extension method in an action method to return a ClientBlockResult. In this case the Client ViewModel is the whole server side ViewModel, and the Client Block includes most of the View passed to the extension method.

Let analyze this two possibilities in detail.

 

The TemplateFor Helper.

The TemplateFor helper istantiates a template on a model passed as firts argument. However, in case the clientModel parameter is set to true both the model and the template are used to define a Client Block. The form associated to the Client Block is the one the TemplateFor extension method was called in.

There are two overloads of the TemplateFor extension method, the first one specifies the model with a Lambda expression:

 

        public static MvcHtmlString TemplateFor<VM, T>(
            this HtmlHelper<VM> htmlHelper,
            Expression<Func<VM, T>> expression,
            object template, 
            Type subClass=null,
            bool clientModel=false,
            string uniqueName=null,
            string externalContainerId=null,
            bool initialSave=false)

 

where:

  • expression specifies the Client ViewModel as a part of the current ViewModel
  • template is the template to be used
  • clientModel when set to true, specifies we are defining a Client Block.
  • uniqueName (used only with clientModel=true ) is the name of the javascript global variable where the extension method will put the Client Model.
  • externalContainerId (used only with clientModel=true ) is the id of the Root of the Client Block. The Root of the Client Block  can be also outside the template. Typically it is an Html node that includes the TemplateFor helper and that will contain the instantiated template. However it can be also another ancestor. This way we may add also input fileds outside the template in the Client Block, by specifying their names with adequate text expressions(this is useful just in rare cases).
  • The subClass parameter, if not null, must be a Type that is a subclass of the type of the Client ViewModel. When this parameter is specified, the Client ViewModel is casted to this subclass before being used(usefull to handle collections of heterogenous types).
  • If initialSave (used only with clientModel=true )  is set to true, the Client ViewModel is serialized and inserted into an hidden field as soon as the page is rendered. Normally one doesn't need this.

Another extension allows the use of a generic object as Client ViewModel:

 

public static MvcHtmlString Template<VM, T>(
           this HtmlHelper<VM> htmlHelper,
           T model,
           object template,
           bool clientModel = false,
           string uniqueName = null,
           string externalContainerId = null,
           string prefix="",
           bool initialSave = false)

 

This overload allows the preparation of a ViewModel destined to an action method that uses a ViewModel that is different from the one of the current View. 

If prefix is not empty it is used as a prefix of all model properties when the Client ViewModel is posted. It usefull to make the Client ViewModel matchs the target action ViewModel. For instance, if the Client ViewModel is an Address object, destined to an action method that accepts Customer classes as ViewModel, we can specify a "OfficeAddress" prefix, to make the Client ViewModel to be inserted into the OfficeAddress property of the target ViewModel. 

Below an example of use of the TemplateFor extension method:

 

<div id="ClientViewModelContainer">           
           <%: Html.TemplateFor(m => m.KeywordsModel, 
                               "ClientViewModelExample",
                                null, 
                                true, 
                                "keywordsHandling", 
                                "ClientViewModelContainer", 
                                true) %>         
</div>


 

KeywordsModel is the piece of the main View ViewModel to be used as Client ViewModel, "ClientViewModelExample" is the name of a partial view used as a template for Client ViewModel, keywordsHandling is the name of the javascript variable that will contain the Client ViewModel, and finally ClientViewModelContainer is the Root of the Client Block.

When TemplateFor is called within a ClientBlock, the ClientBlock is extended to the new template independently of the value of the clientModel parameter. More specifically, in this case the clientModel, the uniqueName, the externalContainerId, and initialSave parameters are completely ignored, and the already existing Client Block is extended in the new template.  This is the correct way to extend a Client Block in a Partial View or in general in any kind of template.

The ClienBlockView controller extension method.

The ClientBlockView controller extension return a  ClientBlockResult action result, that forces a whole View and a whole server side ViewModel to be used to define a Client Block:

 

public static ClientBlockResult ClientBlockView(
this System.Web.Mvc.Controller controller, 
string viewName, 
object model, 
string modelJsName, 
string blockId = null)

 

Where:

  • viewName is the name of a View.
  • modelJsName, is the name of the javascript variable that will contain the Client ViewModel.
  • model is the ViewModel
  • blockId is the id of the Root of the Client Block. If blockId is null the  the Root of the Client Block is the body tag of the Html page.

Often blockId= null, in which case the Client Block include the whole Html page. However, a blockId != null is usefull when one would like to use two Client ViewModels. The first one to receive the data from the server, and possibly to submit them to a first Action Method, and the second one to submit data to a different Action Method. In this case the second Client ViewModel is inserted in a second form with the TemplateFor extension method, or with some overload of the lower level extension methods ClientViewModel or SAClientViewModel. Often, the second ViewModel is not bound to any Html node and is filled manually from javascript.

As already pointed out the main Client ViewModel is automatically associated with the first form of the Html page.

Below an example of use of the ClientBlockView controller extension method:

 

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

Client Side handling of the ViewModel.

The Client ViewModel is inserted in a javascript global variable, specified either in the TemplateFor, or in the ClientBlockView constructs. Each property of the Client ViewModel is a knockout observable (collections are knockout observable arrays). This allows the automatic propagation of changes of the ViewModel to the bound Html nodes and viceversa. To access the value of an observable property, say name, we have to use the notation: model.name(), while to put a new value into an observable property we use the notation: model.name(newValue). In order to modify an observable array we can either get the original javascript array, manipulate it, and then re-assign it to the observale property, or we can use directly the manipulation function of observable arrays.

The values contained in the simple properties of the Client ViewModel are transfrormed from their original data type to strings before being transferred to the Html nodes the property is bound to. This transformation is done according to the actual globalization settings and to the dotnet formatting specification contained in the FormatAttribute of the sever side counterpart of the property, if any.

The model<->Html knockout bindings used by  Client Blocks, are not the original knockout bindings, but custom bindings that enhance the standard knockout bindings with strong typing, globalization, and values formatting capabilities.

All custom Client Blocks bindings update the model not only when the user change the value in the input control, but also whenever the changedByCode event is triggered by javascript code. This way, it is possible to update the model also when the input value is changed by some javascript code without being forced to trigger the change event that might confuse some listener that needs to react just to changes done by the user. Moreover, whenerver a model value is changed all input controls that are bound to it trigger the modelChanged event that may be used to notify the change to other javascript module that are not connected to the model.

On the contrary, the string values contained in the input fields are parsed, according to the actual globalization settings, and transformed in the adequate data type before being transferred to the Client ViewModel they are bound to.

Observable arrays can be bound to to the selected items, of a listbox, to the selected items of a DualSelect box, or to the whole list items of a dropdown or listbox.

An observable array can be bound also to  an arrays of checboxes. It is enough to give to all checkboxes the same name, that reference the property of the ViewModel containing the observable array. For instance: <input type='checkbox' name='@Html.PrefixedName("MyProperty")'/> or <input type='checkbox' name='@Html.PrefixedName(m => m.MyProperty)'/>, where the helper we are using is strongly typed with the Client ViewModel of the Client Block or with a subpart of it. 

When this is done the elements of the observable array will ALWAYS be the list of the values of all selected checkboxes, parsed to match the datata type of the array. The addition of a new element in the array will cause the checkbox with that value becomes checked, and so on.

In a completely analogous way a simple observable property can be bound to an array of radiobuttons.

However, Observable arrays can be bound also to client side templates, in such a way that insertion and deletion of element of the arrays are reflected into insertion and deletion of the Html nodes obtained by instantiating the template on an element of the array. This can be done by using a repeater: ClientBlockRepeater.

Adding Further Bindings manually

When we define a Client Block bindings between input fields and Client ViewModel are defined automatically, according to a name convention. However Client Blocks support all knockout bindinds, so further bindings can be added manually.

The procedure to add further bindings manually is as follows:

  1. Call the ClientBindings() extension method from the helper of the template, or View we are in, to get a IBindingsBuilder<T> interface: var=Html.ClientBindings();
  2. Build a string containing all bindings we would like to add to an Html element: var myBinding=bindings.CSS("hide", m=>m.DetailsRequired).Get(). The binding above makes an element visible or not according to the value of the DetailsRequired ViewModel property. Each binding return the original IBindingsBuilder<t> object, so that other  bindings can be added. When we finish defining bindings for the html element we call the Get() method to get the final string containing all bindings: Css(....).Style(...)....Get().
  3. Put the bindings string into the data-bind Html5 attribute of the Html element: <div data-bind='@myBinding'>. If the Html element is rendered with an helper method we can use the htmlAttributes dictionary of the helper.

We can also prevent the automatic bindings definition based on the name convention on an Html element by adding to it the Html5 attribute data-nobinding='true';

Handling Server Side Errors.

Since some of the Html that belongs to a Client Block is created dynamically on the Client Side, the sever is not able to add it the right styles to show properly validation errors that were discovered on the server. To recovery completely all server side errors it is enough to call the  function @Html.ClientBindings().HandleServerErrors() at the end of each Client Block, where Html is the html helper of the root of the Client Block, that is either the html helper of the template in the TemplateFor method that started the Client Block, or the View Html helper if the ClientBlock was started by a ClientBlockView controller extension method. 

Building the page Html on the client side with the client side control flow statements.

Predefined methods of Client ViewModels and definition of new methods.

All Client ViewModels are automatically empowered with some basic methods:

  • saveAndSubmit(): Validates the form associated to the Client ViewModel. In case of success serializes the Client ViewModel in a predefined hidden filed, and submit the form.
  • saveAndSubmitAlone(formId): Validates the form whose id is passed as argument. In case of success serializes the Client ViewModel in a predefined hidden filed, and submits the form associated to the ViewModel. Usefull, when the Viewmodel is associated to a form that is different from the main form that contains all input fields. This technique is used to post data to an action method that uses a ViewModel that is different from the one of the View the page come from: in this case data are copied to another Client ViewModel located into another form. Therefore the form to validate is different from the form to be submitted.
  • validateAndSave(): Validates the form associated to the Client ViewModel. In case of success serializes the Client ViewModel in a predefined hidden filed, without submitting the form. Return true if validation was successfull and false otherwise.
  • save(). Serialize the Client ViewModel in its predefined hidden field, so that it is ready to be submitted.

Other methods can be defined with the IBindingsBuilder<M> interface method: 

 

IBindingsBuilder<M> AddMethod(string name, string javaScriptCode);

 

 Bindings Reference

Client Block complete example

Last edited Mar 26, 2013 at 10:05 AM by frankabbruzzese, version 32

Comments

No comments yet.