MVC-modellbindung Dynamische Liste von Listen

Habe ich eine dynamische Liste der dynamischen Listen, die <input />s, die Gebucht werden müssen, um eine MVC-controller/action und gebunden, als ein typisiertes Objekt. Der Knackpunkt, mein problem ist, ich kann nicht herausfinden, wie man manuell wählen aus beliebigen Gepostet Formular Werte in meinem benutzerdefinierte binder Modell. Details dazu finden Sie unten.

Habe ich eine Liste von US-Bundesstaaten, die jeweils über eine Liste von Städten. Beide Staaten und Städte können dynamisch Hinzugefügt, gelöscht und neu bestellt. So etwas wie:

public class ConfigureStatesModel
{
    public List<State> States { get; set; }
}

public class State
{
    public string Name { get; set; }
    public List<City> Cities { get; set; }
}

public class City
{
    public string Name { get; set; }
    public int Population { get; set; }
}

GET:

public ActionResult Index()
{
    var csm = new ConfigureStatesModel(); //... populate model ...
    return View("~/Views/ConfigureStates.cshtml", csm);
}

Den ConfigureStates.cshtml:

@model Models.ConfigureStatesModel
@foreach (var state in Model.States)
{
    <input name="stateName" type="text" value="@state.Name" />
    foreach (var city in state.Cities)
    {
        <input name="cityName" type="text" value="@city.Name" />
        <input name="cityPopulation" type="text" value="@city.Population" />
    }
}

(Es gibt mehr markup und javascript, aber ich lasse es aus, für die Kürze/die Einfachheit.)

Alle form-Eingaben werden dann Veröffentlicht, um server, die als so (analysiert von Chrome-Dev-Tools):

stateName: California
cityName: Sacramento
cityPopulation: 1000000
cityName: San Francisco
cityPopulation: 2000000
stateName: Florida
cityName: Miami
cityPopulation: 3000000
cityName: Orlando
cityPopulation: 4000000

Mich bannen zu müssen die Formular-Werte, ideal gebunden List<State> (oder, was dasselbe ist, als ConfigureStatesModel), etwa so:

[HttpPost]
public ActionResult Save(List<State> states)
{
    //do some stuff
}

Eines benutzerdefinierten Modells binder scheint, wie das richtige Werkzeug für den job. Aber ich weiß nicht, wie zu wissen, welche Stadt Namen und Stadt Bevölkerung gehören dem Staat Namen. Das heißt, ich kann sehen, dass alle die form von Schlüsseln und Werten Gepostet, aber ich sehe nicht ein Weg, zu wissen, dass Ihre Beziehung:

public class StatesBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        //California, Florida
        List<string> stateNames = controllerContext.HttpContext.Request.Form.GetValues("stateName").ToList();

        //Sacramento, San Francisco, Miami, Orlando
        List<string> cityNames = controllerContext.HttpContext.Request.Form.GetValues("cityName").ToList();

        //1000000, 2000000, 3000000, 4000000
        List<int> cityPopulations = controllerContext.HttpContext.Request.Form.GetValues("cityPopulation")
            .Select(p => int.Parse(p)).ToList();

        //... build List<State> ...
    }
}

Wenn ich könnte, wissen nur die, um alle Werte kam in Bezug auf alle anderen form-Werte, das wäre genug. Der einzige Weg, ich sehe dies zu tun, ist ein Blick auf die raw-request-stream, als so:

Request.InputStream.Seek(0, SeekOrigin.Begin);
string urlEncodedFormData = new StreamReader(Request.InputStream).ReadToEnd();

aber ich will nicht zu spaßen mit manuell analysieren,.

Beachten Sie auch, dass die Reihenfolge der Liste von Staaten und die Reihenfolge der Listen von Städten in jedem Staat, egal, wie ich anhalten, das Konzept der display-Bestellung für Sie. Damit würde die bewahrt werden müssen aus dem Formular Werte aus.

Habe ich versucht, die Variationen der dynamischen Liste verbindlich wie diese und diese. Aber es fühlt sich falsch an junking die html und das hinzufügen einer Menge von (fehleranfälligen) javascript nur, um die Bindung zu arbeiten. Die Formular-Werte sind schon da; es sollte nur eine Frage der Erfassung auf dem server.

  • Kannst du die Klasse, die Sie übergeben möchten, MVC-Aktion ? Gemäß meinem Verständnis, die Sie übergeben möchten mehrere Mitgliedstaaten , für die Benutzung for statt foreach.
  • Befreien Sie sich von Ihrem benutzerdefinierten ModelBinder, das wird nie funktionieren. Sie benötigen, um die Ansicht zu generieren richtig. Siehe diese Antwort für einige Optionen
  • Hinweis für verschachtelte Sammlungen, die Sie brauchen zu use das plugin statt BeginCollectionItem
  • Oder, wenn Sie wollen, es zu tun alle client-Seite – siehe diese DotNetFiddle
InformationsquelleAutor BrianS | 2018-03-30



2 Replies
  1. 0

    Die offensichtlich nur so wie ich das sehe der Aufbau einer form, die tatsächlich repräsentieren die Städte gehören, der Staat würde erfordern, dass Sie die stark typisierte Helfer.

    So, ich würde verwenden etwas ähnliches wie:

    @model Models.ConfigureStatesModel
    
    @for (int outer = 0; outer < Model.States.Count; outer++)
    {
        <div class="states">
            @Html.TextBoxFor(m => m.States[outer].Name, new { @class="state" })
            for (int inner = 0; inner < Model.States[outer].Cities.Count; inner++)
            {
                <div class="cities">
                    @Html.TextBoxFor(m => m.States[outer].Cities[inner].Name)
                    @Html.TextBoxFor(m => m.States[outer].Cities[inner].Population)
                </div>
            }
        </div>
    }

    Dadurch wird der Eingaben mit Formular-Namen, die der default modelbinder verarbeiten kann.

    Den Teil, der erfordert einige zusätzliche Arbeit ist der Umgang mit der Nachbestellung. Ich würde so etwas wie dies, vorausgesetzt, Sie sind mit jQuery bereits:

    //Iterate through each state
    $('.states').each(function (i, el) {
        var state = $(this);
        var input = state.find('input.state');
    
        var nameState = input.attr('name');
        if (nameState != null) {
            input.attr('name', nameState.replace(new RegExp("States\\[.*\\]", 'gi'), '[' + i + ']'));
        }
    
        var idState = input.attr('id');
        if (idState != null) {
            input.attr('id', idState.replace(new RegExp("States_\\d+"), i));
        }
    
        //Iterate through the cities associated with each state
        state.find('.cities').each(function (index, elem) {
            var inputs = $(this).find('input');
    
            inputs.each(function(){
                var cityInput = (this);
    
                var nameCity = cityInput.attr('name');
                if (nameCity != null) {
                    cityInput.attr('name', nameCity.replace(new RegExp("Cities\\[.*\\]", 'gi'), '[' + index + ']'));
                }
    
                var idCity = cityInput.attr('id');
                if (idCity != null) {
                    cityInput.attr('id', idCity.replace(new RegExp("Cities_\\d+"), index));
                }
            });
        });
    });

    Dieser Letzte bit wahrscheinlich erfordert einige Anpassungen, wie es ist ungetestet, aber es ist ähnlich zu etwas, was ich zuvor getan habe. Würden Sie das nennen, wenn die Elemente, die auf Ihre anzeigen werden Hinzugefügt/bearbeitet/entfernt/verschoben werden.

    • Alles, was Sie brauchen, ist ein hidden-input für die Erfassung indexer ermöglicht die nicht-null nicht-konsekutive Sammlung Elemente gebunden zu sein – z.B. <input name="States.Index" value="@outer" /> für die äußere Sammlung
  2. 0

    Kam ich mit meiner eigenen Lösung. Es ist ein bisschen ein hack, aber ich glaube, es ist besser als die alternativen. Die andere Lösung und Vorschläge aller beteiligten verändern Sie die markup-Tags und das hinzufügen von javascript zu synchronisieren Hinzugefügt markup — was ich ausdrücklich gesagt habe ich will nicht in den OP. Ich fühle das hinzufügen von Indizes zu den <input /> Namen ist redundant, wenn gesagt <input />s sind bereits bestellt, in der DOM-wie Sie es wünschen. Und das hinzufügen von javascript ist nur eine weitere Sache, um zu halten und unnötige bits gesendet durch den Draht.

    Sowieso .. Meine Lösung beinhaltet, Durchlaufen die raw-request-body. Ich hatte nicht erkannt, dass vor, dass dies im Grunde nur eine url-codierte Abfragezeichenfolge, und es ist einfach, mit zu arbeiten nach einem einfachen url-decode:

    public class StatesBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);
            string urlEncodedFormData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
            var decodedFormKeyValuePairs = urlEncodedFormData
                .Split('&')
                .Select(s => s.Split('='))
                .Where(kv => kv.Length == 2 && !string.IsNullOrEmpty(kv[0]) && !string.IsNullOrEmpty(kv[1]))
                .Select(kv => new { key = HttpUtility.UrlDecode(kv[0]), value = HttpUtility.UrlDecode(kv[1]) });
            var states = new List<State>();
            foreach (var kv in decodedFormKeyValuePairs)
            {
                if (kv.key == "stateName")
                {
                    states.Add(new State { Name = kv.value, Cities = new List<City>() });
                }
                else if (kv.key == "cityName")
                {
                    states.Last().Cities.Add(new City { Name = kv.value });
                }
                else if (kv.key == "cityPopulation")
                {
                    states.Last().Cities.Last().Population = int.Parse(kv.value);
                }
                else
                {
                    //key-value form field that can be ignored
                }
            }
            return states;
        }
    }

    Dies setzt Voraus, dass (1) der html-Elemente sind bestellt, und der DOM korrekt, (2) sind in den POST-request-body in der gleichen Reihenfolge, und (3) sind die in der Anforderung empfangen stream auf dem server in der gleichen Reihenfolge. Für mein Verständnis und in meinem Fall, gelten diese Annahmen.

    Wieder, das fühlt sich an wie ein hack, und scheint nicht sehr MVC-y. Aber es funktioniert für mich. Wenn dies geschieht, um jemandem zu helfen gibt, cool.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.