ASP.NET MVC–Cascading Dropdown Lists Tutorial–Part 4: Cascading using FORM Hijaxing
- 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 6 – Creating a jQuery Cascade Select Plugin
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 ); } } }
@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