ASP.NET MVC–Cascading Dropdown Lists Tutorial–Part 2: Cascading using normal FORM post (Html.BeginForm helper)


Part 2 – Cascading using normal FORM post (Html.BeginForm helper)

This method uses the Html.BeginForm helper and no client side scripts. We will start by creating a controller called DropDownNormalPostController:

public class DropDownNormalPostController : Controller
{
}

Because we need to access the database we will need a repository variable (we only need the ContinentRepository because we have the navigation properties defined):

public partial class DropDownNormalPostController : Controller
{
    private readonly IContinentRepository _continentRepository;
    // If you are using Dependency Injection, you can delete the following constructor
    public DropDownNormalPostController( ) : this( new ContinentRepository( ) ) { }
    public DropDownNormalPostController( IContinentRepository continentRepository )
    {
        this._continentRepository = continentRepository;
    }
}

We will use the default Index action to show the view with the two dropdown lists (Continents and Countries) and a table to show the Cities:

public virtual ViewResult Index( )
{
    var atlas = new Atlas( );
    atlas.Continents = this._continentRepository.All;
    return View( atlas );
}

Index.cshtml:

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

As you can see we have a partial view for each of three types. Below are the partial views:

_Continents.cshtml:

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@*@using ( Html.BeginForm( "SelectContinent" , "DropDownNormalPost") )*@
@using ( Html.BeginForm( MVC.CascadingDropDownLists.DropDownNormalPost.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>
}

We have a Dropdown list inside a form and when the Select submit button is clicked the form is posted to the SelectContinent action (The commented @using shows how we will normally write a form if we don’t use T4MVC).

The SelectContinent action:

[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
    };
    return View( MVC.CascadingDropDownLists.DropDownNormalPost.Views.Index , atlas );
}

The SelectContient action has a nullable int parameter because we don’t have any required constraint on the SelectedContinentId property from the Atlas model. In other words when the user selects the first option ([Please select a continent]) everything will be reset.

Because we need to fill the second dropdown list with countries we will call de Find method from the ContinentRepository and get the Countries property. Next we construct an Atlas object with the data we have so far and pass it to the Index view.

_Countries.cshtml:

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
@if ( Model.Countries != null && Model.Countries.Count( ) > 0 )
{
    using ( Html.BeginForm( MVC.CascadingDropDownLists.DropDownNormalPost.SelectCountry( ) , FormMethod.Post ) )
    { 
        <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>
}

and the SelectCountry action:

[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
    };
    return View( MVC.CascadingDropDownLists.DropDownNormalPost.Views.Index , atlas );
}

The only differences here are that we have now two nullable int parameters (selectedContinentId and selectedCountryId) and a little more code (nothing too complicated). We could have only 1 parameter, selectedCountryId and add the CountryRepository besides the ContinentRepository thus simplifying the SelectCountry action a little (Feel free to modify it as an exercise).

_Cities.cshtml:

@model Mvc3.Extensions.Demo.Areas.CascadingDropDownLists.Models.Atlas
<fieldset>
    <legend>Cities</legend>
    @if ( Model.Cities != null && Model.Cities.Count( ) > 0 )
    {
        <table>
            <tr>
                <th>
                    Name
                </th>
                <th>
                    Population
                </th>
            </tr>
            @foreach ( var item in Model.Cities )
            {
                <tr>
                    <td>
                        @item.Name
                    </td>
                    <td align="right">
                        @item.Population.ToString( "0,0" )
                    </td>
                </tr>
            }
        </table>
    }
    else
    {
        @:<text>No information available</text>
    }
</fieldset>

The _Cities.cshtml partial view is a simple table.

See it in action

Cascading Dropdown Lists - Normal FORM post

Have you noticed anything ugly about this approach? There are three things:

  1. We need to click a button in order to apply the selection from the dropdown lists (we will fix this in the following posts)
  2. The date and time displayed at the bottom of the Index view changes every time we click a Select button, indicating that we do a full page refresh (we will fix this in the next post)
  3. When a Select button is clicked the URL changes from /CascadingDropDownLists/DropDownNormalPost to /CascadingDropDownLists/DropDownNormalPost/SelectContinent or /CascadingDropDownLists/DropDownNormalPost/SelectCountry. Also if we click refresh right after a button click  we get a message saying that the form will be submitted again (we fix this below)

Here is the modified controller that keeps the same URL and removes the message about form resubmission (using the Post/Redirect/Get method):

public partial class DropDownNormalPostPRGController : Controller
{
    private readonly IContinentRepository _continentRepository;
    // If you are using Dependency Injection, you can delete the following constructor
    public DropDownNormalPostPRGController( ) : this( new ContinentRepository( ) ) { }
    public DropDownNormalPostPRGController( IContinentRepository continentRepository )
    {
        this._continentRepository = continentRepository;
    }
    public virtual ViewResult Index( )
    {
        Atlas atlas = TempData[ "atlas" ] as Atlas;
        if ( atlas == null )
        {
            atlas = new Atlas( );
            atlas.Continents = this._continentRepository.All;
        }
        return View( MVC.CascadingDropDownLists.DropDownNormalPostPRG.Views.Index , 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
        };
        this.TempData[ "atlas" ] = atlas;
        return RedirectToAction( MVC.CascadingDropDownLists.DropDownNormalPostPRG.Index( ) );
    }
    [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
        };
        this.TempData[ "atlas" ] = atlas;
        return RedirectToAction( MVC.CascadingDropDownLists.DropDownNormalPostPRG.Index( ) );
    }
}

The actions are similar, the only difference is that now we use the TempData dictionary to keep the atlas object and redirect to the Index action instead of returning the Index view. In the Index action we look for the atlas object in the TempData dictionary or construct a new one before returning the Index view.

Cascading Dropdown Lists - Normal FORM post (PRG)

References

Download

Download code

 

2 Comments

  • HgJOi6 Im obliged for the blog post.Much thanks again.

  • Hi all,

    on controller on line


    atlas.Continents = this._continentRepository.All;

    I get error

    Error 21 Cannot implicitly convert type 'System.Linq.IQueryable' to 'System.Collections.Generic.IEnumerable'. An explicit conversion exists (are you missing a cast?) D:\fiat.hr\fiat.hr\Controllers\AdminConfigController.cs 32 30 fiat.hrWebUI

    Can please someone help?

Comments have been disabled for this content.