HowTo: Cascading Dropdowns mit ASP.NET MVC

image Wenn zwei oder mehrere Eingabefelder, wie z.B. Dropdowns, durch eine bestimmte Auswahl logisch miteinander verknüpfen will, braucht man einen kleinen Mechanismus. Ich habe das ganze mit Javascript, AJAX und ASP.NET MVC gelöst und stelle die recht simple Lösung vor.

 

Cascading? Ein Bild sagt mehr als tausend Worte

imageDie drei Selectboxen stehen in Beziehung zueinander. Erst wenn man die Automarke ausgewählt hat, kann man das Modell auswählen und erst am Ende die Farbe -> Logisch.

Zu meinem BeispieL

image Nicht ganz so hübsch aussehen, aber ähnliches Prinzip. Erst Landauswählen, dann Bundesland, dann Stadt und am Ende die Straße.

 

Diese Struktur habe ich so auch in meinem Models Ordner, wobei Straßen nur simple Strings sind und ich den deshalb keine eigene Klasse spendiert habe.

image 

Das LocationRepository erzeugt mir ein paar Dummydaten:

    public class LocationRepository
    {
        public static IList<Country> GetCountries()
        {
            List<Country> countries = new List<Country>();

            for (int i = 0; i < 5; i++)
            {
                Country country = new Country();
                country.Name = "Country " + i.ToString();
                countries.Add(country);
            }

            return countries;
        }

        public static IList<FederalStates> GetFederalStates(string countryName)
        {
            List<FederalStates> states = new List<FederalStates>();

            for (int i = 0; i < 5; i++)
            {
                FederalStates state = new FederalStates();
                state.Name = countryName + " - FederalState " + i.ToString();
                states.Add(state);
            }

            return states;
        }

        public static IList<City> GetCities(string federalStateName)
        {
            List<City> cities = new List<City>();

            for (int i = 0; i < 5; i++)
            {
                City city = new City();
                city.Name = federalStateName + " - City " + i.ToString();
                cities.Add(city);
            }

            return cities;
        }

        public static IList<string> GetStreets(string cityName)
        {
            List<string> streets = new List<string>();

            for (int i = 0; i < 5; i++)
            {
                string street = cityName + " - Street " + i.ToString();
                streets.Add(street);
            }

            return streets;
        }
    }

Prinzip ist immer: Ich geb den Namen des höheren Elementes rein und die kleinen Helper bauen mir die entsprechenden Elemente.

Das HomeViewModel

    public class HomeViewModel
    {
        public IList<SelectListItem> Countries { get; set; }
        public IList<SelectListItem> FederalStates { get; set; }
        public IList<SelectListItem> Cities { get; set; }
        public IList<SelectListItem> Streets { get; set; }

        public HomeViewModel()
        {
            this.Countries = new List<SelectListItem>();
            this.Countries.Add(new SelectListItem() { Text = "Please choose...", Value = ""});
            this.FederalStates = new List<SelectListItem>();
            this.FederalStates.Add(new SelectListItem() { Text = "Please choose...", Value = "" });
            this.Cities = new List<SelectListItem>();
            this.Cities.Add(new SelectListItem() { Text = "Please choose...", Value = "" });
            this.Streets = new List<SelectListItem>();
            this.Streets.Add(new SelectListItem() { Text = "Please choose...", Value = "" });
        }
    }

Hier speichern wir unsere 4 Listen und noch einen Default Value.

Der Controller

    [HandleError]
    public class HomeController : Controller
    {
        private HomeViewModel GetHomeViewModel(string country, string federalState, string city, string street)
        {
            HomeViewModel model = new HomeViewModel();

            IList<Country> countries = LocationRepository.GetCountries();
            foreach (Country countryItem in countries)
            {
                SelectListItem item = new SelectListItem();
                item.Text = countryItem.Name;
                item.Value = countryItem.Name;
                model.Countries.Add(item);
            }

            if(string.Empty != country)
            {
                IList<FederalStates> fedStates = LocationRepository.GetFederalStates(country);
                foreach (FederalStates fedItem in fedStates)
                {
                    SelectListItem item = new SelectListItem();
                    item.Text = fedItem.Name;
                    item.Value = fedItem.Name;
                    model.FederalStates.Add(item);
                }
            }

            if(string.Empty != federalState)
            {
                IList<City> cities = LocationRepository.GetCities(federalState);
                foreach (City cityItem in cities)
                {
                    SelectListItem item = new SelectListItem();
                    item.Text = cityItem.Name;
                    item.Value = cityItem.Name;
                    model.Cities.Add(item);
                }
            }

            if(string.Empty != city)
            {
                IList<string> streets = LocationRepository.GetStreets(city);
                foreach (string streetItem in streets)
                {
                    SelectListItem item = new SelectListItem();
                    item.Text = streetItem;
                    item.Value = streetItem;
                    model.Streets.Add(item);
                }
            }

            return model;
        }

        public ActionResult Index()
        {
            HomeViewModel model = this.GetHomeViewModel(string.Empty, string.Empty,string.Empty,string.Empty);
            return View(model);
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Index(string country, string federalState, string city, string street)
        {
            ViewData.ModelState.AddModelError("Message", "Uppps...!");
            HomeViewModel model = this.GetHomeViewModel(country, federalState, city, street);
            return View(model);
        }

        public JsonResult GetFederalStatesJson(string country)
        {
            IList<FederalStates> fedStates = LocationRepository.GetFederalStates(country);
            return Json(fedStates);
        }

        public JsonResult GetCitiesJson(string federalState)
        {
            IList<City> cities = LocationRepository.GetCities(federalState);
            return Json(cities);
        }

        public JsonResult GetStreetsJson(string cityName)
        {
            IList<string> streets = LocationRepository.GetStreets(cityName);
            return Json(streets);
        }
    }

Die 3 Json Actions sprechen mit dem Repository und geben mir die gewünschten Elemente wieder. Dann gibt es noch zwei Index-Actions. Einmal für POST und einmal für einen GET Aufruf.

  • Bei GET wird eine leere Liste zurückgegeben. Wir holen uns das Viewmodel und geben nur die erste Ebene, in meinem Fall die Länder, zurück.
  • Bei POST könnte ja während der Bearbeitung ein Fehler auftreten. Daher schreibe ich dort eine Fehlermeldung in den ModelState. Jetzt übergebe ich alle ausgewählten Daten meiner kleinen “GetHomeViewModel” Methode und lade mir die Daten die ich für den Anzeigen des Views brauche. Vorteil: Die Auswahl die der Nutzer getroffen hat geht nicht verloren.

Das war es eigentlich auch schon im Controller. Natürlich könnte man das alles noch schöner oder generischer bauen, das soll aber auch nur zur Inspiration dienen ;)

Jetzt der View

Das Formular, wobei unser View mit dem HomeViewModel typsiert ist:

    <% using(Html.BeginForm()) { %>
    <div>
        <fieldset>
            <legend>Country/FederalState/City/Street Selection</legend>
            <p>
                <%=Html.ValidationMessage("Message") %>
            </p>
            <p>
                <lable>Country</lable>
                <%=Html.DropDownList("Country",Model.Countries, new { id = "CountrySelection", onchange="changeCountry()" }) %>
            </p>
            <p>
                <lable>Federal States</lable>
                <%=Html.DropDownList("FederalState", Model.FederalStates, new { id = "FedStateSelection", onchange = "changeFederalState()" })%>
            </p>
            <p>
                <lable>City</lable>
                <%=Html.DropDownList("City", Model.Cities, new { id = "CitySelection", onchange = "changeCity()" })%>
            </p>
            <p>
                <lable>Street</lable>
                <%=Html.DropDownList("Street", Model.Streets, new { id = "StreetSelection" })%>
            </p>
            <button type="submit">Submit!</button>
        </fieldset>
    </div>
    <%} %>

Wir registrieren bei jeden Dropdown, bis auf die letzte Ebene, JavascriptEventhandler und geben unser ViewModel als Value jeweils mit rien.

Das Javascript

<script type="text/javascript">

        function resetSelection(element) {
            element.empty();
            element.append("<option value=''>Please choose...</option>");
        }

        function changeCountry() {
            resetSelection($("#FedStateSelection"));
            resetSelection($("#CitySelection"));
            resetSelection($("#StreetSelection"));

            if ($("#CountrySelection").val() != "") {
                var url = "<%=Url.Action("GetFederalStatesJson") %>?country=" + $("#CountrySelection").val();
                $.getJSON(url, null, function(data) {
                    $.each(data, function(index, optionData) {
                        $("#FedStateSelection").append("<option value='"
                         + optionData.Name
                         + "'>" + optionData.Name
                         + "</option>");
                    });
                });
            }
        }

        function changeFederalState() {
            resetSelection($("#CitySelection"));
            resetSelection($("#StreetSelection"));

            if ($("#FedStateSelection").val() != "") {
                var url = "<%=Url.Action("GetCitiesJson") %>?federalState=" + $("#FedStateSelection").val();
                $.getJSON(url, null, function(data) {
                    $.each(data, function(index, optionData) {
                        $("#CitySelection").append("<option value='"
                         + optionData.Name
                         + "'>" + optionData.Name
                         + "</option>");
                    });
                });
            }

        }

        function changeCity() {
            resetSelection($("#StreetSelection"));

            if ($("#CitySelection").val() != "") {
                var url = "<%=Url.Action("GetStreetsJson") %>?cityName=" + $("#CitySelection").val();
                $.getJSON(url, null, function(data) {
                    $.each(data, function(index, optionData) {
                        $("#StreetSelection").append("<option value='"
                         + optionData
                         + "'>" + optionData
                         + "</option>");
                    });
                });
            }
        }

    </script>

Auch dies könnte sicherlich nocht etwas schöner gestaltet werden, funktioniert aber erstmal und war ein Prototyp. Bei jedem changeXXX löschen wir die Daten der darunterliegenden Felder, da diese ja in Beziehung stehen und laden die Daten des direkten Kindelementes.

Auch hier habe ich für die URLs die URL Helper genommen. Näheres dazu in diesem Blogpost.

Im Fehlerfall

Nun hat der User seine Auswahl getroffen und drückt auf “Submit”. Wir übertragen die ausgewählten Daten und laden diese im Backend neu und zeigen wieder den View an. Damit bleiben seine gewählten Daten erhalten und der User freut sich:

image

*Submit* *Irgendwas läuft schief*

image

:)

Ich würde es jetzt nicht unbedingt als “Control” bezeichnen, da es noch zu viel “Detailimplementierung” benötigt, aber wer es braucht: Es funktioniert recht zuverlässig :)

[ Download Democode ]


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

One Response

  1. Hallo,

    danke für HOTO :-)
    Ich habe ein Probelm. wenn ich in mein Drobdownlist eine ein Item auswähle, wird die Jvascript-Funktion aufgerufen. Die Javascript-Metode ruft auch die GetJsonData-Methode in meinem Controller auf. Bis hier get alles ganz gut. Aber wenn die GetJsonData-Methode im Controller fertig ist, wird die $.getJson() methode nicht mehr aufgerufen. Also das Programm kehrt nicht mehr zurück zur JavaScript Methode.

    Wo ist das Problemm? Bin beim verzweifeln.

    Viele Grüße.

    Kaveh

    Reply

Comment on this post

Letzte Posts

  • image.png
    Chocolatey–apt-get für Windows

    Durch Zufall bin ich auf das Tool “Chocolatey” gestoßen. Wer die Website sich anschaut, wird evtl. eine Verwandschaft mit NuGet ausmachen. Was macht Chocolatey? Chocolatey ist ein “Maschine Package Manager”, das bedeutet, dass man für seine Maschine einfach Tools runterladen und Updaten kann – direkt über die Konsole. Was ist der Unterschied zu NuGet? NuGet ...

  • image.png
    SASS, LESS & Coffeescript in Visual Studio mit der Web Workbench

    CSS und Javascript sind die “kleinste” Schnittmenge von allen Browsern für die Erstellung von Web-Applikationen. Leider geht dabei etwas komfort verloren, daher lieben alle Webentwickler jQuery! SASS und LESS sind zwei Varianten, wie man “schöner” CSS schreiben kann und Coffeescript versucht Javascript Entwicklung zu vereinfachen. Aber immer der Reihe nach… Was ist SASS? SASS steht ...

  • image.png
    Code-Inside Sample nun auf GitHub: Google Code zu GitHub Migration

    Seit einiger Zeit habe ich Beispielcode auf Google Code bereitgestellt. Einfach nur noch weg von Google Code O-Ton damals war: Ich hatte mich für Google Code entschieden, weil ich hoffe dass früher oder später die Google Code Suche nutzbar ist und es dadurch wenigstens ein kleiner Mehrwert entsteht. Allerdings wirft es momentan noch ein Fehler. ...

  • image.png
    Windows-8-Hackathon @Night in Leipzig

    Hacken (=Entwickeln, nichts böswilliges!), Grillen und mitten in der Nacht fachsimpeln? Dann ist vielleicht der Windows-8-Hackathon was für dich. Der Hackathon wird vom 15. Juni (ab 19:00) bis zum 16. Juni (bis in die frühen Morgenstunden) in Leipzig stattfinden.  Mit dabei sind auch Darius Parys und Tom Wendel von der Microsoft Deutschland. Thematisch (wie der ...

  • image.png
    Einstieg in Redis on Windows & Redis mit .NET benutzen

    Redis gehört zu den NoSQL Datenbanken und ist dort in der Familie der Key-Value Stores zu finden. Redis wird oft mit “Blazing Fast” betitelt und laut dem Stackoverflow Thread soll es im Vergleich zu MongoDB zweimal (beim Schreiben) und sogar dreimal (beim Lesen) so schnell sein wie MongoDB – auch wenn der Vergleich etwas “hinkt” ...

Auf Amazon einkaufen & unterstützen

Facebook