ASP.NET MVC–Cascading Dropdown Lists Tutorial–Part 4: Cascading using FORM Hijaxing


Part 4 – Cascading using FORM Hijaxing 

   

First of all let’s see what we have so far:

  • In Part 2 we used the Html.BeginForm helper to cascade the dropdown lists. We click a Select button, the page is refreshed entirely and we have the Countries dropdown list filled with data (or the Cities table).
  • In Part 3 we used the Ajax.BeginForm helper to cascade the dropdown lists by updating only a portion of the page. We also have a fallback mechanism in case JavaScript is disabled, in which case the behavior is exactly as in Part 2. We still need to click the Select buttons.

In this part we will combine the previous parts (updating only a portion of the page, fallback in case JavaScript is disabled and using Html.BeginForm) and we will remove the need to click a Select button to cascade (this will happen when we select an option from the dropdown list) by  using a technique named Hijaxing (see the References section).

We will start by copying the DropDownAjaxPostController and renaming it to DropDownHijaxPostController. Everything stays the same (except for the name of the constructor and the path for the views):

public partial class DropDownHijaxPostController : Controller
{
    private readonly IContinentRepository _continentRepository;
    // If you are using Dependency Injection, you can delete the following constructor
    public DropDownHijaxPostController( ) : this( new ContinentRepository( ) ) { }
    public DropDownHijaxPostController( IContinentRepository continentRepository )
    {
        this._continentRepository = continentRepository;
    }
    public virtual ActionResult Index( )
    {
        Atlas atlas = new Atlas( );
        atlas.Continents = this._continentRepository.All;
        return View( atlas );
    }
    [HttpPost]
    public virtual ActionResult SelectContinent( int? selectedContinentId )
    {
        var countries = selectedContinentId.HasValue
            ? this._continentRepository.Find( selectedContinentId.Value ).Countries
            : null;
        Atlas atlas = new Atlas
        {
            SelectedContinentId = selectedContinentId ,
            Continents = this._continentRepository.All ,
            Countries = countries
        };
        if ( Request.IsAjaxRequest( ) )
        {
            return PartialView( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Countries , atlas );
        }
        else
        {
            return View( MVC.CascadingDropDownLists.DropDownHijaxPost.Views.Index , atlas );
        }
    }
    [HttpPost]
    public virtual ActionResult SelectCountry( int? selectedContinentId , int? selectedCountryId )
    {
        var selectedContinent = selectedContinentId.HasValue
            ? this._continentRepository.Find( selectedContinentId.Value )
            : null;
        var countries = ( selectedContinent != null )
            ? selectedContinent.Countries
            : null;
        var cities = ( countries != null && selectedCountryId.HasValue )
            ? countries.Where( c => c.Id == selectedCountryId.Value ).SingleOrDefault( ).Cities
            : null;
        Atlas atlas = new Atlas
        {
            SelectedContinentId = selectedContinentId ,
            SelectedCountryId = selectedCountryId ,
            Continents = this._continentRepository.All ,
            Countries = countries ,
            Cities = cities
        };
        if ( Request.IsAjaxRequest( ) )
        {
            return PartialView( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Cities , atlas );
        }
        else
        {
            return View( MVC.CascadingDropDownLists.DropDownHijaxPost.Views.Index , atlas );
        }
    }
}

Index.cshtml is the same as in Ajax.BeginForm method (again the path for the view changes) 

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@{
    ViewBag.Title = "Index";
}
@Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Continents )
<div id="countries">
    @Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Countries )
</div>
<div id="cities">
    @Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Cities )
</div>
@DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss:fff")

There will be more code in the Index.cshtml view but we’ll get to it later.

_Continents.cshtml:

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@using ( Html.BeginForm( MVC.CascadingDropDownLists.DropDownHijaxPost.SelectContinent( ) ) )
{ 
     <fieldset>
        <legend>Continents</legend>
        
        @Html.DropDownListFor( 
            m => m.SelectedContinentId , 
            new SelectList( Model.Continents , "Id" , "Name" ) , 
            "[Please select a continent]" 
        )
        <input type="submit" value="Select" />
    </fieldset>
}

This partial view is identical to the one in Part 2 (Html.BeginForm) except the action and controller where the form will post.

_Countries.cshtml:
@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@if ( Model.Countries != null && Model.Countries.Count( ) > 0 )
{
    using ( Html.BeginForm( MVC.CascadingDropDownLists.DropDownHijaxPost.SelectCountry( ) ) )
    { 
        <fieldset>
            <legend>Countries</legend>
                @Html.HiddenFor( m => m.SelectedContinentId )
                @Html.DropDownListFor( 
                    m => m.SelectedCountryId , 
                    new SelectList( Model.Countries , "Id" , "Name" ) , 
                    "[Please select a country]" 
                )
                <input type="submit" value="Select" />
        </fieldset>
    }
}
else
{
    <fieldset>
        <legend>Countries</legend>
        No information available
    </fieldset>
}

_Cities.cshtml:

The _Cities.cshtml partial view is the same as in Part 2.

If we run it now it will behave exactly as in Part 2 but this is not what we wanted so let’s start adding some code:

First we need to reference the jQuery script and thanks to T4MVC we have intellisense and a cleaner src value:

    <script src="@Links.Scripts.jquery_1_6_1_min_js" type="text/javascript"></script>

Next we define a document ready function at the bottom of the Index view:

<script type="text/javascript">
    $(document).ready(function () {
        //Code here
    });
</script>

Because we will post the form when the selected value in the dropdown list changes we don’t need the Select buttons but we cannot remove them either. We need the submit buttons in case JavaScript is disabled so we will hide them using jQuery (if JavaScript is disabled there will be no JavaScript code executed so they will remain visible):

//Hide the submit buttons. If JavaScript is disabled we'll use the normal functionality
$('input[type=submit]').hide();

Now we need a way to submit Submit the first form to the SelectContinent action. For this we will catch the change event for the Continents dropdown lists, issue a form submit of its parent form, then catch the submit event of the form and perform an $..ajax() call instead:

//Continent select
$('#SelectedContinentId').change(function () {
    $(this).parents('form').submit();
    return false;
});
//Continent form submit
$("form[action$='SelectContinent']").submit(function () {
    $.ajax({
        url: $(this).attr('action'),
        type: 'post',
        data: $(this).serialize(),
        success: function (result) {
            $('#countries').html(result);
            $('input[type=submit]').hide();
        }
    });
    return false;
});

In case of success we replace the contents of the countries div with the result and hide the select button. We do something similar for the Countries dropdown list:

//Country select
$('#SelectedCountryId').live('change', function () {
    $(this).parents("form").submit();
    return false;
});
//Country form submit
$("form[action$='SelectCountry']").live('submit', function () {
    $.ajax({
        url: $(this).attr('action'),
        type: 'post',
        data: $(this).serialize(),
        success: function (result) {
            $('#cities').html(result);
        }
    });
    return false;
});

Notice that instead of using the change and submit functions directly we used the live function. The jQuery live() function allows us to bind to the events of controls that will be available in the future. At first there is no Countries dropdown list and using the change function will do nothing.

The final Index.cshtml view:

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@{
    ViewBag.Title = "Index";
}
@Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Continents )
<div id="countries">
    @Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Countries )
</div>
<div id="cities">
    @Html.Partial( MVC.CascadingDropDownLists.DropDownHijaxPost.Views._Cities )
</div>
@DateTime.Now.ToString("dd/MM/yyyy HH:mm:ss:fff")
<script type="text/javascript">
    $(document).ready(function () {
        //Hide the submit buttons. If javascript is disabled we'll use the normal functionality
        $('input[type=submit]').hide();
        //Continent select
        $('#SelectedContinentId').change(function () {
            $(this).parents('form').submit();
            return false;

        });
        //Continent form submit
        $("form[action$='SelectContinent']").submit(function () {
            $.ajax({
                url: $(this).attr('action'),
                type: 'post',
                data: $(this).serialize(),
                success: function (result) {
                    $('#countries').html(result);
                    $('input[type=submit]').hide();
                }
            });
            return false;
        });
        //Country select
        $('#SelectedCountryId').live('change', function () {
            $(this).parents("form").submit();
            return false;
        });
        //Country form submit
        $("form[action$='SelectCountry']").live('submit', function () {
            $.ajax({
                url: $(this).attr('action'),
                type: 'post',
                data: $(this).serialize(),
                success: function (result) {
                    $('#cities').html(result);
                }
            });
            return false;
        });
    });
</script>

See it in action

Cascading Dropdown Lists - Hijaxing

Try it with JavaScript enabled and disabled.

Theoretically speaking we’re done. We perform the dropdown lists cascade using the change event and we also have a fallback mechanism in case JavaScript is disabled. So why go further? Well if you have a requirement that says your web application should be fully functional even if JavaScript is disabled then stop here because in the next part we will trade this functionality for some speed and less code (although you can add some fallback functionality to the next method with some little effort).

References

Hijaxing | .live() - jQuery API | $.ajax() - jQuery API

Download

 

2 Comments

  • What if I need this to be part of a bigger form ?
    Form tag cannot be inside another form tag.
    Usually when someone need a cascading drop-down, it is inside of a bigger form.. how can I achieve this ?

  • @Dani, sorry for taking to much to answer. Yes this is not the solution in a bigger form. You can achieve the same thing using only one form. My suggestion is to use the jQuery version instead of Html.Ajax

Comments have been disabled for this content.