ASP.NET MVC–Cascading Dropdown Lists Tutorial–Part 5.3: Cascading using jQuery.ajax() ($.ajax() and Knockout.js)


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

Download

Download code

 

8 Comments

  • I have used the same concept as described here. But I am not binding with the Id of the object. Due to this is set in every options. How do I set the value of the option attibutes??? Plz help.

  • @prateeksasanker.net you can remove the optionsCaption property. That is the one that generates the empty . It's purpose is to give the user a visual hint. In my sample everything is reset if you select that option but you can choose to do nothing by moving

    this.selectedCountry(undefined);
    this.countries(undefined);

    in an else brach.

  • @prateeksasanker.net: In my sample the value is set by using optionsValue : "Id". I see that you have optionsText: "AcName" but no optionsValue.

  • Yeah I tried using optionsValue: "Id" but if I do so then my cascaded drop downs are not being populated. Thats the problem. Can I set the options value id with any other method ?

  • @prateeksasanker.net: Do you have an Id property? Maybe it has another name. optionsValue: "[Property you want to use a value]". Try to see the javascript error (Ctrl+Shift+J in FireFox will open the error console)

  • thanks Radu Enuca. yeah I told wat you mentioned but no javascript error. Can you help me on copying the selected item of dropdownlist to a textbox ?

  • Nice sample! Worked well for me.
    The only part I did not get is why you used
    atlasViewModel.countries(data);
    and not
    this.countries(data);

    Using this.countries(data); worked for me. Is there some reason for using atlasViewModel.cities(data)?

  • @Malcolmn Frexner: I used separate function for the callbacks in the beginning

Comments have been disabled for this content.