47 votes

Déclinaisons en cascade dans une vue Razor MVC 3

Je suis intéressé par la façon de mettre en œuvre des listes déroulantes en cascade pour les adresses dans une vue Razor. Mon entité Site possède une propriété SuburbId. Suburb a un CityId, et City un ProvinceId. J'aimerais afficher des listes déroulantes pour toutes les propriétés Suburb, City et Province dans la vue du site. Par exemple, la liste déroulante Suburb affichera initialement "First select a City" (sélectionnez d'abord une ville) et la liste déroulante City, "First select a province" (sélectionnez d'abord une province). Lorsque vous sélectionnez une province, les villes de cette province sont remplies, etc.

Comment puis-je y parvenir ? Par où dois-je commencer ?

87voto

Darin Dimitrov Points 528142

Illustrons cela par un exemple. Comme toujours, commencez par un modèle :

public class MyViewModel
{
    public string SelectedProvinceId { get; set; }
    public string SelectedCityId { get; set; }
    public string SelectedSuburbId { get; set; }
    public IEnumerable<Province> Provinces { get; set; }
}

public class Province
{
    public string Id { get; set; }
    public string Name { get; set; }
}

Ensuite, un contrôleur :

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);
    }
}

Et enfin une vue :

@model SomeNs.Models.MyViewModel

@{
    ViewBag.Title = "Home Page";
}

<script type="text/javascript" src="/scripts/jquery-1.4.4.js"></script>
<script type="text/javascript">
    $(function () {
        $('#SelectedProvinceId').change(function () {
            var selectedProvinceId = $(this).val();
            $.getJSON('@Url.Action("Cities")', { provinceId: selectedProvinceId }, function (cities) {
                var citiesSelect = $('#SelectedCityId');
                citiesSelect.empty();
                $.each(cities, function (index, city) {
                    citiesSelect.append(
                        $('<option/>')
                            .attr('value', city.Id)
                            .text(city.Name)
                    );
                });
            });
        });

        $('#SelectedCityId').change(function () {
            var selectedCityId = $(this).val();
            $.getJSON('@Url.Action("Suburbs")', { cityId: selectedCityId }, function (suburbs) {
                var suburbsSelect = $('#SelectedSuburbId');
                suburbsSelect.empty();
                $.each(suburbs, function (index, suburb) {
                    suburbsSelect.append(
                        $('<option/>')
                            .attr('value', suburb.Id)
                            .text(suburb.Name)
                    );
                });
            });
        });
    });
</script>

<div>
    Province: 
    @Html.DropDownListFor(x => x.SelectedProvinceId, new SelectList(Model.Provinces, "Id", "Name"))
</div>
<div>
    City: 
    @Html.DropDownListFor(x => x.SelectedCityId, Enumerable.Empty<SelectListItem>())
</div>
<div>
    Suburb: 
    @Html.DropDownListFor(x => x.SelectedSuburbId, Enumerable.Empty<SelectListItem>())
</div>

En guise d'amélioration, le code javascript pourrait être raccourci en écrivant un plugin jquery pour éviter de dupliquer certaines parties.


UPDATE :

Et en parlant d'un plugin, vous pourriez avoir quelque chose entre les lignes :

(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(
                            $('<option/>')
                                .attr('value', item.Id)
                                .text(item.Name)
                        );
                    });
                });
            });
        });
    };
})(jQuery);

Et ensuite, il suffit de le câbler :

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

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

5voto

Kris Points 51

Merci Darin pour votre piste vers la solution. Cela m'a beaucoup aidé à arriver au but. Mais comme 'xxviktor' l'a mentionné, j'ai eu une erreur de référence circulaire. Pour m'en débarrasser, j'ai procédé de cette façon.

    public string GetCounties(int countryID)
    {
        List<County> objCounties = new List<County>();
        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);
    }

Et pour réaliser la cascade automatique, j'ai légèrement étendu l'extension jQuery de cette façon.

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

Et le JS actuel utilise ce paramètre comme ci-dessous (dans la requête JSON).

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

J'espère que cela aidera quelqu'un qui a un problème similaire.

1voto

xxviktor Points 11

Soyez conscient que cette solution ne fonctionne pas directement avec EF 4.0. Elle provoque l'erreur "A circular reference was detected while serializing...". Voici les solutions possibles http://blogs.telerik.com/atanaskorchev/posts/10-01-25/resolving_circular_references_when_binding_the_mvc_grid.aspx J'ai utilisé le deuxième.

0voto

Debendra Dash Points 1932
<script src="~/Scripts/jquery-1.10.2.min.js"></script>

<script type="text/javascript">
    $(document).ready(function () {
        //Dropdownlist Selectedchange event
        $("#country").change(function () {
            $("#State").empty();
            $.ajax({
                type: 'POST',
                url: '@Url.Action("State")', // we are calling json method

                dataType: 'json',

                data: { id: $("#country").val() },
                // here we are get value of selected country and passing same value

                success: function (states) {
                    // states contains the JSON formatted list
                    // of states passed from the controller

                    $.each(states, function (i, state) {
                        $("#State").append('<option value="' + state.Value + '">' +
                             state.Text + '</option>');
                        // here we are adding option for States

                    });
                },
            error: function (ex) {
                alert('Failed to retrieve states.' + ex);
            }
        });
        return false;
    })
    });
</script>

<div>
    @Html.DropDownList("country", ViewBag.country as List<SelectListItem>, "CountryName", new { style = "width: 200px;" })
</div>
<div>

</div>
<div>
    @Html.DropDownList("State", ViewBag.country as List<SelectListItem>)

</div>

A partir du contrôleur, j'obtiens les valeurs suivantes

 public async Task<ActionResult> Country()
    {

        Country co = new Country();
        List<SelectListItem> li = new List<SelectListItem>();
        li.Add(new SelectListItem { Text = "Select", Value = "0" });
        li.Add(new SelectListItem { Text = "India", Value = "1" });
        li.Add(new SelectListItem { Text = "Nepal", Value = "2" });
        li.Add(new SelectListItem { Text = "USA", Value = "3" });
        li.Add(new SelectListItem { Text = "Kenya", Value = "4" }); ;
        ViewBag.country= li;
        return View();
    }
    public JsonResult  state(string id)
    {
        List<SelectListItem> states = new List<SelectListItem>();
        states.Add(new SelectListItem { Text = "--Select State--", Value = "0" });
        switch (id)
        {
            case "1":

                states.Add(new SelectListItem { Text = "MP", Value = "1" });
                states.Add(new SelectListItem { Text = "UP", Value = "2" });
                break;
            case "3":

                states.Add(new SelectListItem { Text = "USA1", Value = "3" });
                states.Add(new SelectListItem { Text = "USA2", Value = "4" });
                break;
        }

        return Json(new SelectList(states, "Value", "Text", JsonRequestBehavior.AllowGet));
    }

0voto

dweremeichik Points 31

Pour mettre en œuvre des listes déroulantes en cascade qui prennent en charge la validation et la liaison intégrées de MVC, vous devrez faire quelque chose d'un peu différent de ce qui est fait dans les autres réponses ici.

Si votre modèle comporte une validation, celle-ci sera prise en charge. Un extrait d'un modèle avec validation :

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

Dans votre contrôleur, vous devez ajouter une méthode get, afin que votre vue puisse obtenir les données pertinentes par la suite :

[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);  
}

Venons-en maintenant à la vue que j'ai mentionnée plus tôt :

Dans votre vue, vous avez deux listes déroulantes semblables à celle-ci :

<div class="editor-label">
    @Html.LabelFor(model => model.stateId, "State")
</div>
<div class="editor-field">
    @Html.DropDownList("stateId", String.Empty)
    @Html.ValidationMessageFor(model => model.stateId)
</div>

<div class="editor-label">
    @Html.LabelFor(model => model.cityId, "City")
</div>
<div class="editor-field">
    @*<select id="cityId"></select>*@
    @Html.DropDownList("cityId", String.Empty)
    @Html.ValidationMessageFor(model => model.cityId)
</div>

Le contenu des listes déroulantes est lié par le contrôleur et est automatiquement alimenté. Remarque : d'après mon expérience, supprimer cette liaison et s'appuyer sur le java script pour remplir les listes déroulantes vous fait perdre la validation. En outre, la façon dont nous lions ici joue bien avec la validation, il n'y a donc aucune raison de la changer.

Passons maintenant à notre 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));

Vous pouvez copier le jQuery ci-dessus que j'ai créé, dans <script>...</script> dans votre vue, ou dans un fichier script séparé si vous le souhaitez (notez que j'ai mis à jour ceci pour le rendre multi-navigateur, cependant le scénario que j'utilisais n'est plus nécessaire, il devrait fonctionner cependant).

Dans ces mêmes balises script, (pas dans un fichier séparé) vous pouvez appeler le plugin en utilisant le javascript suivant :

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

N'oubliez pas d'ajouter le $(document).ready la page doit être entièrement chargée avant que vous n'essayiez de faire en sorte que les listes déroulantes soient en cascade.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X