【问题标题】:MVC Model Binding Dynamic List of ListsMVC模型绑定动态列表列表
【发布时间】:2018-03-31 05:52:39
【问题描述】:

我有一个动态列表的动态列表,其中有 <input />s 需要发布到 MVC 控制器/动作并绑定为类型化对象。我的问题的症结在于我无法弄清楚如何在我的custom model binder 中手动选择任意 POSTed 表单值。详情如下。

我有一个美国各州的列表,每个州都有一个城市列表。州和城市都可以动态添加、删除和重新排序。所以像:

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,但为了简洁/简单起见,我将其省略。)

然后所有表单输入都被 POST 到服务器,因此(由 Chrome 开发工具解析):

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

我需要捕获表单值,理想情况下绑定为List&lt;State&gt;(或等效为ConfigureStatesModel),如下所示:

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

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

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 感觉是错误的。表单值已经存在;应该只是在服务器上捕获它们。

【问题讨论】:

  • 你能分享你想传递给 MVC 动作的类吗?据我了解,您想传递多个状态,为此使用 for 而不是 foreach
  • 摆脱您永远无法使用的自定义 ModelBinder。您需要正确生成视图。一些选项请参考this answer
  • 注意嵌套集合,你需要使用this plugin而不是BeginCollectionItem
  • 或者如果你想在客户端做这一切 - 参考this DotNetFiddle

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


【解决方案1】:

我认为构建一个实际代表哪些城市属于哪个州的表单的唯一明显方法是要求您使用强类型帮助器。

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

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

最后一点可能需要一些调整,因为它未经测试,但它与我之前做过的类似。每当添加/编辑/删除/移动视图上的项目时,您都会调用它。

【讨论】:

  • 您只需要为集合索引器添加一个隐藏输入,它允许绑定非零、非连续的集合项 - 例如&lt;input name="States.Index" value="@outer" /&gt; 用于外部集合
【解决方案2】:

我想出了自己的解决方案。这有点像黑客,但我觉得它比替代品更好。其他解决方案和建议都涉及更改标记和添加 javascript 以同步添加的标记——我特别说过我不想在 OP 中这样做。如果说&lt;input /&gt;s 已经按照您想要的方式在 DOM 中排序,我觉得向 &lt;input /&gt; 名称添加索引是多余的。添加 javascript 只是要维护的另一件事,并且通过网络发送了不必要的位。

无论如何 .. 我的解决方案包括遍历原始请求正文。之前我没有意识到这基本上只是一个 url 编码的查询字符串,经过简单的 url 解码后很容易使用:

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) 在服务器上的请求流中以相同的顺序接收命令。据我了解,就我而言,这些都是有效的假设。

再一次,这感觉像是一个 hack,而且看起来不是很 MVC-y。但它对我有用。如果这恰好可以帮助其他人,那就太酷了。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-19
    • 1970-01-01
    • 1970-01-01
    • 2011-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多