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

  • Carriage Return / Neue Zeile in Textareas

    Eine kleine Aufgabe: Jede neue Textzeile (Carriage Return/Wenn man Enter drückt ) in einer Textarea soll ein Element in einer Auflistung sein – wie mach ich das jetzt am einfachsten? Eigentlich ein grundlegendes Element im Web und der Nutzer macht bewusst Absätze – daher wäre es nur gerecht, wenn man das auch entsprechend würdigt. Kleine ...

  • image.png
    Doom, Quake, Wolfenstein & co. Source Code auf GitHub

    id Software, die Macher von Doom, Quake, Wolfenstein & co., stellen regelmäßig ihre älteren Spieltitle als Open Source zur Verfügung. Das Ganze runterzuladen fand ich bisher immer recht mühselig, allerdings gibt es seit kurzer Zeit die Sourcen auch auf GitHub. Darunter Spiele wie Doom 3, Quake 3, Wolfenstein für iOS. Wer also schon immer mal ...

  • image.png
    Twitter Bootstrap 2.0 released & “Release Präsentation”

    Wie bereits vom Twitter Bootstrap Team angekündigt wurde offiziel die Version 2.0 des UI Toolskits “Twitter Bootstrap” veröffentlich. Zudem wurden die Slides, welche bei der Release Party gezeigt wurden auch veröffentlicht: Downloads finden sich auf der Twitter Bootstrap Seite auf GitHub. Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf ...

  • image.png
    Javascript zu Dart Translator

    Dart, Google Javascript Alternative, wurde vor ein paar Monaten vorgestellt und die Webentwickler Szene ist noch etwas gespalten, ob Dart nun überflüssig ist oder einfach nur cool und längst überfällig ist. Um die Sprache näher zu erläutern hat Google die grundlegenden Javascript Basics nach Dart übersetzt. Das Ergebnis ist der “Translator”. Der Name mag momentan ...

  • Twitter Bootstrap 2.0–“Beta”

    Twitter Bootstrap, ein UI-Toolkit für Web-Applikationen von Twitter, erscheint (wie bereits berichtet) demnächst in der Version 2.0. Der offizielle Release ist am 31. Januar, allerdings beginnt jetzt laut Mark Otto (einer der Hauptentwickler von Twitter Bootstrap) die intensive Test-Phase. Das heisst, das es nun offiziel auch die 2.0 Dokumentation online gibt. Im Vergleich zur aktuellen ...

Support us!

Facebook