Desplegable en cascada en MVC 3 Razor view

Estoy interesado en cómo implementar listas desplegables en cascada para las direcciones en una vista Razor. Mi entidad de sitio tiene una propiedad Suburbio. El suburbio tiene CityId y City tiene ProvinceId. Me gustaría mostrar los menús desplegables para todos los suburbios, ciudades y provincias en la vista del sitio, donde, por ejemplo, el menú desplegable del barrio mostrará inicialmente “Primero seleccione una ciudad”, y el menú desplegable Ciudad, “Primero seleccione una provincia”. Al seleccionar una provincia, las ciudades de la provincia están pobladas, etc.

¿Cómo puedo conseguir esto? ¿Dónde empiezo?

Vamos a ilustrar con un ejemplo. Como siempre comienza con un modelo:

public class MyViewModel { public string SelectedProvinceId { get; set; } public string SelectedCityId { get; set; } public string SelectedSuburbId { get; set; } public IEnumerable Provinces { get; set; } } public class Province { public string Id { get; set; } public string Name { get; set; } } 

Luego un controlador:

 public class HomeController : Controller { public ActionResult Index() { var model = new MyViewModel { // TODO: Fetch those from your repository Provinces = Enumerable.Range(1, 10).Select(x => new Province { Id = (x + 1).ToString(), Name = "Province " + x }) }; return View(model); } public ActionResult Suburbs(int cityId) { // TODO: Fetch the suburbs from your repository based on the cityId var suburbs = Enumerable.Range(1, 5).Select(x => new { Id = x, Name = "suburb " + x }); return Json(suburbs, JsonRequestBehavior.AllowGet); } public ActionResult Cities(int provinceId) { // TODO: Fetch the cities from your repository based on the provinceId var cities = Enumerable.Range(1, 5).Select(x => new { Id = x, Name = "city " + x }); return Json(cities, JsonRequestBehavior.AllowGet); } } 

Y finalmente una vista:

 @model SomeNs.Models.MyViewModel @{ ViewBag.Title = "Home Page"; }   
Province: @Html.DropDownListFor(x => x.SelectedProvinceId, new SelectList(Model.Provinces, "Id", "Name"))
City: @Html.DropDownListFor(x => x.SelectedCityId, Enumerable.Empty())
Suburb: @Html.DropDownListFor(x => x.SelectedSuburbId, Enumerable.Empty())

Como mejora, el código de javascript podría acortarse escribiendo un complemento de jquery para evitar la duplicación de algunas partes.


ACTUALIZAR:

Y hablando de un complemento, podrías tener algo entre líneas:

 (function ($) { $.fn.cascade = function (options) { var defaults = { }; var opts = $.extend(defaults, options); return this.each(function () { $(this).change(function () { var selectedValue = $(this).val(); var params = { }; params[opts.paramName] = selectedValue; $.getJSON(opts.url, params, function (items) { opts.childSelect.empty(); $.each(items, function (index, item) { opts.childSelect.append( $('') .attr('value', item.Id) .text(item.Name) ); }); }); }); }); }; })(jQuery); 

Y luego simplemente conectarlo:

 $(function () { $('#SelectedProvinceId').cascade({ url: '@Url.Action("Cities")', paramName: 'provinceId', childSelect: $('#SelectedCityId') }); $('#SelectedCityId').cascade({ url: '@Url.Action("Suburbs")', paramName: 'cityId', childSelect: $('#SelectedSuburbId') }); }); 

Gracias Darin por su iniciativa. Me ayudó enormemente llegar al punto. Pero como se mencionó ‘xxviktor’, obtuve la referencia circular. error. Para deshacerme de él, lo hice de esta manera.

  public string GetCounties(int countryID) { List objCounties = new List(); var objResp = _mastRepo.GetCounties(countryID, ref objCounties); var objRetC = from c in objCounties select new SelectListItem { Text = c.Name, Value = c.ID.ToString() }; return new JavaScriptSerializer().Serialize(objRetC); } 

Y para lograr la cascada automática, extendí ligeramente la extensión jQuery de esta manera.

  $('#ddlCountry').cascade({ url: '@Url.Action("GetCounties")', paramName: 'countryID', childSelect: $('#ddlState'), childCascade: true }); 

Y el JS real está utilizando este parámetro como a continuación (dentro de la solicitud JSON).

  // trigger child change if (opts.childCascade) { opts.childSelect.change(); } 

Espero que esto ayude a alguien con un problema similar.

tenga en cuenta que esta solución no funciona directamente con EF 4.0. Causa el error “Se ha detectado una referencia circular al serializar …”. Aquí hay posibles soluciones http://blogs.telerik.com/atanaskorchev/posts/10-01-25/resolving_circular_references_when_binding_the_mvc_grid.aspx , he usado el segundo.

Para implementar listas desplegables en cascada que admitan la validación y el enlace incorporados de MVC, deberá hacer algo un poco diferente de lo que se hace en las otras respuestas aquí.

Si su modelo tiene validación, esto lo apoyará. Un extracto de un modelo con validación:

 [Required] [DisplayFormat(ConvertEmptyStringToNull = false)] public Guid cityId { get; set; } 

En su controlador necesita agregar un método get, para que su vista pueda obtener los datos relevantes más adelante:

 [AcceptVerbs(HttpVerbs.Get)] public JsonResult GetData(Guid id) { var cityList = (from s in db.City where s.stateId == id select new { cityId = s.cityId, name = s.name }); //simply grabbing all of the cities that are in the selected state return Json(cityList.ToList(), JsonRequestBehavior.AllowGet); } 

Ahora, a la Vista que mencioné anteriormente:

En su opinión, tiene dos menús desplegables similares a esto:

 
@Html.LabelFor(model => model.stateId, "State")
@Html.DropDownList("stateId", String.Empty) @Html.ValidationMessageFor(model => model.stateId)
@Html.LabelFor(model => model.cityId, "City")
@**@ @Html.DropDownList("cityId", String.Empty) @Html.ValidationMessageFor(model => model.cityId)

El contenido del menú desplegable está vinculado por el controlador y se completa automáticamente. Nota: en mi experiencia al eliminar este enlace y confiar en el script java para rellenar los menús desplegables, pierdes la validación. Además, la forma en que estamos vinculados aquí juega bien con la validación, por lo que no hay ninguna razón para cambiarla.

Ahora en nuestro plugin jQuery:

 (function ($) { $.fn.cascade = function (secondaryDropDown, actionUrl, stringValueToCompare) { primaryDropDown = this; //This doesn't necessarily need to be global globalOptions = new Array(); //This doesn't necessarily need to be global for (var i = 0; i < secondaryDropDown.options.length; i++) { globalOptions.push(secondaryDropDown.options[i]); } $(primaryDropDown).change(function () { if ($(primaryDropDown).val() != "") { $(secondaryDropDown).prop('disabled', false); //Enable the second dropdown if we have an acceptable value $.ajax({ url: actionUrl, type: 'GET', cache: false, data: { id: $(primaryDropDown).val() }, success: function (result) { $(secondaryDropDown).empty() //Empty the dropdown so we can re-populate it var dynamicData = new Array(); for (count = 0; count < result.length; count++) { dynamicData.push(result[count][stringValueToCompare]); } //allow the empty option so the second dropdown will not look odd when empty dynamicData.push(globalOptions[0].value); for (var i = 0; i < dynamicData.length; i++) { for (var j = 0; j < globalOptions.length; j++) { if (dynamicData[i] == globalOptions[j].value) { $(secondaryDropDown).append(globalOptions[j]); break; } } } }, dataType: 'json', error: function () { console.log("Error retrieving cascading dropdown data from " + actionUrl); } }); } else { $(secondaryDropDown).prop('disabled', true); } secondaryDropDown.selectedindex = 0; //this prevents a previous selection from sticking }); $(primaryDropDown).change(); }; } (jQuery)); 

Puede copiar el jQuery anterior que creé, en tags en su vista, o en un archivo de script separado si lo desea (nota que actualicé esto para hacerlo cruzar navegador, sin embargo, el escenario en que ya estaba usando, ya no es necesario, debería funcionar, sin embargo).

En esas mismas tags de script, (no en un archivo separado) puede llamar al complemento utilizando el siguiente javascript:

 $(document).ready(function () { var primaryDropDown = document.getElementById('stateId'); var secondaryDropdown = document.getElementById('cityId'); var actionUrl = '@Url.Action("GetData")' $(primaryDropDown).cascade(secondaryDropdown, actionUrl); }); 

Recuerde agregar la parte $(document).ready , la página debe estar completamente cargada antes de intentar hacer la cascada de desplegable.

   
 
@Html.DropDownList("country", ViewBag.country as List, "CountryName", new { style = "width: 200px;" })
@Html.DropDownList("State", ViewBag.country as List)