ASP.NET MVC–Cascading Dropdown Lists Tutorial–Part 5.3: Cascading using jQuery.ajax() ($.ajax() and Knockout.js)
- Part 1 – Defining the problem and the context
- Part 2 – Cascading using normal FORM post (Html.BeginForm helper)
- Part 3 – Cascading using Microsoft AJAX (Ajax.BeginForm helper)
- Part 4 – Cascading using FORM Hijaxing
- Part 5 – Cascading using jQuery.ajax():
- Part 5.1 – $.ajax() and DOM Objects
- Part 5.2 – $.ajax() and jQuery Templates
- Part 5.3 – $.ajax() and Knockout.js
- Part 6 – Creating a jQuery Cascade Select Plugin
Part 5.3: Cascading using jQuery.ajax() ($.ajax() and Knockout.js)
For this part we will use Knockout.ks:
Knockout is a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model. Any time you have sections of UI that update dynamically (e.g., changing depending on the user’s actions or when an external data source changes), KO can help you implement it more simply and maintainably. (see the References section).
Before reading further it is better to read the Knockout.js documentation + demos (2-3 hours read) as the things can get really confusing.
We will reuse the controller from Part 5.1 and add a new action called KnockOutJs:
public virtual ViewResult KnockOutJs( ) { return View( MVC.CascadingDropDownLists.DropDownjQueryAjaxPost.Views.KnockoutJs ); }
And of course the KnockoutJs.cshtml:
<fieldset> <legend>Continents</legend> <select id="continents" data-bind=' options: continents, optionsValue : "Id", optionsText: "Name", optionsCaption: "[Please select a continent]", value: selectedContinent'> </select> </fieldset> <fieldset> <legend>Countries</legend> <div> <select id="countries" data-bind=' options: selectedContinent() ? countries : null, optionsValue : "Id", optionsText: "Name", optionsCaption: "[Please select a country]", value: selectedCountry, visible: (countries() && countries().length > 0)'> </select> <span data-bind=' template: {name: "noInfoTemplate"}, visible: !(countries() && countries().length > 0)'> </span> </div> </fieldset> <fieldset> <legend>Cities</legend> <div> <table data-bind='visible: (cities() && cities().length > 0 )'> <thead> <tr> <th> Name </th> <th> Population </th> </tr> </thead> <tbody data-bind='template: {name: "citiesTemplate", foreach: cities}'> </tbody> </table> <span data-bind=' template: {name: "noInfoTemplate"}, visible: !(cities() && cities().length > 0 )'> </span> </div> </fieldset> @DateTime.Now.ToString( "dd/MM/yyyy HH:mm:ss:fff" ) <script type="text/html" id="citiesTemplate"> <tr> <td>${Name}</td> <td align='right'>${Population}</td> </tr> </script> <script type="text/html" id="noInfoTemplate"> No Information Available </script>
Compared to Part 5.1 and 5.2 there is a lot of code but the real power of Knockout.js is when we have a very complex view. Knockout.js introduces an attribute called “data-bind” that binds the html tag to a JavaScript view model, for short.
For the continents dropdown list we have the following (in exact order of the properties):
-
Get the options from the model property called continents
-
The option value will be filled from the Id property of each Continent item
-
The option text (what we see) will be filled from the Name property of each Continent item
-
The first item presented to the use, the option caption, is set to “[Please select a continent]” (the value for this option will be undefined or null)
-
The value of the selected Continent will go the selectedContinent property from the view model
For the countries dropdown list we have the following (in exact order of the properties):
-
Get the options from the model property called countries but only if there is a continent selected
-
The option value will be filled from the Id property of each Country item
-
The option text (what we see) will be filled from the Name property of each Country item
-
The first item presented to the use, the option caption, is set to “[Please select a country]” (the value for this option will be undefined or null)
-
The value of the selected Country will go the selectedCountry property from the view model
-
The countries dropdown list will only be visible if the countries property is not null and not empty (visible expects a value that will evaluate to true/false, 1/0, null, undefined)
For the cities we have the following:
-
The cities table is only visible if the cities property is not null and not empty
-
The cities table body gets its data (the rows) from a template called citiesTemplate which is called for each City in the cities property. We could use the {{each}} template tag directly in the template but in this way if a City is added to the cities array it will be automatically displayed.
There are also 2 span tags visible if the countries and cities table are not visible. The span tags get their inner html form the noInfoTemplate.
Now let’s see how the view model looks. Here is the complete script used by this view:
<script type='text/javascript'> $(document).ready(function () { var atlas = function () { this.selectedContinent = ko.observable(); this.selectedCountry = ko.observable(); this.continents = ko.observableArray(); this.countries = ko.observableArray(); this.cities = ko.observableArray(); // Whenever the continent changes, reset the country selection this.selectedContinent.subscribe(function (continentId) { this.selectedCountry(undefined); this.countries(undefined); if (continentId != null) { $.ajax({ url: '@Url.Action( MVC.CascadingDropDownLists.DropDownjQueryAjaxPost.GetCountries( ) )', data: { continentId: continentId }, type: 'GET', success: function (data) { atlasViewModel.countries(data); } }); } } .bind(this)); this.selectedCountry.subscribe(function (countryId) { this.cities(undefined); if (countryId != null) { $.ajax({ url: '@Url.Action( MVC.CascadingDropDownLists.DropDownjQueryAjaxPost.GetCities( ) )', data: { countryId: countryId }, type: 'GET', success: function (data) { atlasViewModel.cities(data); } }); } } .bind(this)); }; var atlasViewModel = new atlas(); ko.applyBindings(atlasViewModel); //Load the continents $.ajax({ url: '@Url.Action( MVC.CascadingDropDownLists.DropDownjQueryAjaxPost.GetContinents( ) )', type: 'GET', success: function (data) { atlasViewModel.continents(data); } }); }); </script>
Another important piece of the Knockout.js library are the observable properties. When the observable properties change the tags with data-bind attribute will be automatically updated . We can also subscribe to the modification of those properties (as we did for the selectedContinent and selectedCountry properties). Knockout.js is activated by calling the applyBindings method with the view model passed as the argument.
The above code does the following:
-
When the document is ready we make an Ajax request for the continents. The continents property of the view model is updated and it triggers the UI update (if there are binding to that property).
-
When we select a continent the selectedContinent property is updated (through the data-bind properties)
-
Because we have a subscription for the changing of the selectedContinent property our function will be called.
-
The function will clear the selectedCountry and the cities array (again triggering the UI update)
-
If the selected continent is not null (in other words if we haven’t selected the [Please select a continent] option) we make an Ajax request for the countries based on the selected continent id and the countries property is updated with the data received from the server (triggering an UI update).
The function for the selectedCountry changed event is similar as the selectedContinent one.
See it in action
Cascading Dropdown Lists - Knockout.js
The code can be improved by adding 2 dependent observables called countriesVisible and citiesVisible that should be used instead of (countries() && countries().length > 0) statement. Dependent observables depend on the other properties (they are basically function that return a result based on the other properties). For those reading the documentation should be easy to implement.
References
Knockout.js – Documentation | Knockout.js - Samples