MVC模型绑定动态列表列表

时间:2018-03-30 02:13:06

标签: c# asp.net-mvc model-binding custom-model-binder

我有一个动态列表的动态列表,其中<input />需要POST到MVC控制器/动作并绑定为类型对象。我的问题的关键是我无法弄清楚如何在我的custom model binder中手动挑选任意POST表单值。详情如下。

我有一份美国国家名单,每个国家都有一份城市名单。可以动态添加,删除和重新排序州和城市。如下所示:

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

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" />
    }
}

(有更多的标记和javascript,但为了简洁/简洁,我将其留下来。)

然后将所有表单输入发布到服务器,如此(由Chrome开发工具解析):

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

我需要捕获表单值,理想情况下绑定为List<State>(或等效地,作为ConfigureStatesModel),如下所示:

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

自定义模型绑定器似乎是适合该作业的工具。但我不知道如何知道哪个城市名称和城市人口属于哪个州名。也就是说,我可以看到POST的所有表单键和值,但我看不到知道它们之间关系的方法:

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> ...
    }
}

如果我只知道所有值与所有其他表单值相关的顺序,那就足够了。我看到这样做的唯一方法是查看原始请求流,如下所示:

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

但我不想弄乱手动解析它。

另请注意,状态列表的顺序和每个州的城市列表的顺序很重要,因为我坚持显示顺序的概念。所以这也需要从表单值中保留。

我尝试过动态列表绑定的变体,例如thisthis。但是感觉错误的是将html加起来并添加了很多(容易出错的)javascript,只是为了让绑定工作。表格值已经存在;它应该只是在服务器上捕获它们。

2 个答案:

答案 0 :(得分:0)

我看到建立一个实际上代表哪个城市属于哪个州的表单的唯一明显方式是要求你使用强类型助手。

所以,我会使用类似的东西:

@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>
}

这将创建具有默认模型绑定器可以处理的表单名称的输入。

需要一些额外工作的部分是处理重新订购。假设你已经使用了jQuery,我会使用这样的东西:

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

这最后一点可能需要一些调整,因为它未经测试,但它类似于我以前做过的事情。只要添加/编辑/删除/移动视图中的项目,您就会调用此方法。

答案 1 :(得分:0)

我提出了自己的解决方案。这有点像黑客,但我觉得它比其他选择更好。另一个解决方案和建议都涉及改变标记和添加javascript来同步添加的标记 - 我特别说我不想在OP中做。如果已经按照您希望的方式在DOM中对所述<input />进行了排序,我觉得为<input />名称添加索引是多余的。添加javascript只需要维护一件事,并通过网络发送不必要的比特。

无论如何..我的解决方案涉及循环遍历原始请求正文。我之前没有意识到这基本上只是一个url编码的查询字符串,并且在简单的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;
    }
}

这假定(1)html元素在DOM上正确排序,(2)在POST请求体中以相同的顺序设置,(3)在服务器上的请求流中接收订购。据我所知,在我看来,这些都是有效的假设。

同样,这感觉就像一个黑客,并且似乎不是MVC-y。但它对我有用。如果这可以帮助其他人,那就很酷。