在没有索引的ASP.NET MVC中绑定数组? [英] Binding arrays in ASP.NET MVC without index?

查看:57
本文介绍了在没有索引的ASP.NET MVC中绑定数组?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个如下所示的HTML:

 < input type =textname =data [] value =George/> 
< input type =textname =data []value =John/>
< input type =textname =data []value =Paul/>
< input type =textname =data []value =Ringo/>

在PHP中,我可以像这样接收这个数组:

  $ array = $ _POST ['name']; 
// $ array [0] ==George

在ASP.NET MVC模型联编程序约定迫使我将索引放入HTML中,因此控制器可以接收该数组。

 <! - ASP.NET MVC版本的HTML  - > 
< input type =textname =data [0]value =George/>
< input type =textname =data [1]value =John/>
< input type =textname =data [2]value =Paul/>
< input type =textname =data [3]value =Ringo/>

// C#Controller
public ActionResult SomeAction(string [] data)
{
//做东西
}

如果我发送第一个HTML,Action中的数据将是 null

好吧,我觉得这很糟糕。



如果我使用客户端代码删除或添加项目到数组,我必须编写代码重新索引HTML数组。 b

有没有办法扩展ASP.NET MVC ModelBinder来绑定没有索引的数组或解决这个问题的解决方法?

编辑



在尝试您的答案后,我得出结论:我发布的示例对我的目的没有用处。我的真实情况是这样的:

视图



 < table> 
@for(var i = 0; i< Model.Sections.Count; ++ i)
{
< tr>
< td>< a href =#class =edit-section>< span class =glyphicon glyphicon-question-sign>< / span>< / a> < / TD>
< td> @ Html.TextBoxFor(m => Model.Sections [i] .SectionOrder,new {@class =form-control})< / td>
< td> @ Html.TextBoxFor(m => Model.Sections [i] .Title,new {@class =form-control})< / td>
< td> @ Html.TextBoxFor(m => Model.Sections [i] .SubTitle,new {@class =form-control})< / td>
< td>
@ Html.HiddenFor(m => Model.Sections [i] .Id)
< a href =#class =delete-section>< span class =glyphicon glyphicon-删除>< /跨度>< / A>
< / td>
< / tr>
}
< / table>



动作



  public ActionResult SaveSections(ICollection< SectionModel> sections)
{
// DO STUFF
}

我试过手工制作HTML输入,例如:

  @for (var i = 0; i< Model.Sections.Count; ++ i)
{
< tr>
< td>< a href =#class =edit-section>< span class =glyphicon glyphicon-question-sign>< / span>< / a> < / TD>
< td> @ Html.TextBox(Sections.SectionOrder,Model.Sections [i] .SectionOrder,new {@class =form-control})< / td>
< td> @ Html.TextBox(Sections.Title,Model.Sections [i] .Title,new {@class =form-control})< / td>
< td> @ Html.TextBox(Sections.SubTitle,Model.Sections [i] .SubTitle,new {@class =form-control})< / td>
< td>
@ Html.Hidden(Sections.SubTitle,Model.Sections [i] .Id)
< a href =#class =delete-section>< span class = glyphicon glyphicon-remove>< / span>< / a>
< / td>
< / tr>
}

但它没有效果......

解决方案

您不需要明确索引平面数据。如果您认为您有

 < input type ='text'name ='data'value ='George'/> gt ; 
< input type ='text'name ='data'value ='John'/>
< input type ='text'name ='data'value ='Paul'/>
< input type ='text'name ='data'value ='Ringo'/>

然后在您的控制器中,您可以使用

<$

//数据现在应该是一个由4个元素组成的字符串数组$ b $ return RedirectToAction(Index );
}

要理解活页夹,基本上可以反向使用。当您提交表单时,假设它发布到您的Create方法中,模型联编程序会检查方法参数。它会看到你有一个字符串数组作为参数,它被称为 data 。它喜欢字符串,因为表单数据作为字符串提交。除了查看 data 键的元素的表单集合外,不需要做任何真正的工作。所有匹配的项目都会添加到数组中并分配给您的参数。



这适用于因为参数与表单元素相同的名称 。如果名称不匹配,那么您将得到空值,因为没有找到该名称的任何关键字。



如果您使用强视图(具有显式模型的视图),则你可以使用MVC帮助器为你生成这些帮助文件,并且输入元素将被分配专有名称以映射回你的对象。

例如,如果你有一个模型:

  public class BandMembers 
{
public string [] data {get;设置;}
}

在您的视图中,您将其指定为模型并使用适当的HTML帮助程序,您的操作方法可以如下所示:

  public ActionResult创建(BandMembers乐队)
{
// band现在有一个名为'data'的属性,包含4个元素
返回RedirectToAction(Index);
}

这应该会产生一个名为 band ,它有一个包含4个值的名称属性。这是因为模型联编程序看到一个名为band的参数,它与表单集合中的任何已知键不匹配,意识到它是一个复杂的对象(不是字符串,int,string [],int []等),并检查其参数成员。它看到这个对象有一个名为data的字符串数组,并且这个名称的表单集合中有键。它收集这些值,将它们赋值给data属性并将该对象赋值给band参数。



现在你理解视图模型了!



*如果您在控制器中使用了BandMembers类,但将其称为 data ,您将得到一个空值。这是因为模型联编程序使用键 data 找到表单集合中的项目,但无法弄清楚如何将它们从字符串转换为BandMembers对象。



编辑



关于以更深层次的示例进行编辑,以下是我所得到的



首先,我的模型只是让我们在同一页面上。

  public class FormData 
我创建了一个FormData对象,其中包含一个List of Section作为对象集合。 {
public列表< Section>部分{get;组; }
$ b $ public FormData()
{
}
}

和我的Section.cs类:

  public class Section 
{
public bool IsDeleted {get;组; }
public bool IsNew {get;组; }
public int Id {get;组; }
public int SectionOrder {get;组; }
public string Title {get;组; }
public string SubTitle {get;组; }

public Section()
{
}
}

在您的部分上使用EditorTemplate可以轻松渲染出为您生成的索引。我自己嘲笑了这个项目,并验证了这一点。不幸的是,正如你所看到的,一旦你删除一个项目,你的索引将失灵。那么你如何解决这个问题?当然,你可以去阅读索引并重写它们,或者 - 不要删除它们!我在模拟项目中所做的是在Section中添加一个名为IsDeleted的新属性,并将其作为隐藏元素呈现。在删除点击的JavaScript处理程序中,我隐藏该行并将该行的IsDeleted输入的隐藏输入更新为'true'。当我提交表单时,我现在会有一个完整的集合以及一个方便的标志,让我知道需要从模型中删除哪些行。



我创建了一个测试视图绑定到一个名为FormData的模型,该模型包含一个List。

  @model MVCEditorTemplateDemo.Models.FormData 
@使用(Html.BeginForm())
{
< table id =section-container>
@ Html.EditorFor(m => m.Sections)
< / table>
$ b @ Ajax.ActionLink(Add Section,GetNewSection,Home,new AjaxOptions(){HttpMethod =POST,InsertionMode = InsertionMode.InsertAfter,UpdateTargetId =section-container })
< input type =submitvalue =Submit/>
}

是的,那EditorFor正在收集!但它如何知道如何处理它呢?我在Views / Home中创建了一个文件夹(可以在共享中,如果你想在控制器中使用它)称为EditorTemplates,我在其中放置了一个名为Section.cshtml的部分视图。该名称很重要 - 它应该匹配它将呈现的对象的名称。由于我的模型包含名为Section的对象,因此我的EditorTemplate也应该称为Section。



下面是它的样子(EditorTemplates \ Section.cshtml):

  @model MVCEditorTemplateDemo.Models.Section 
< tr>
< td>< a href =#class =edit-section>< span class =glyphicon glyphicon-question-sign>< / span>< / a> < / TD>
< td> @ Html.TextBoxFor(m => Model.SectionOrder,new {@class =form-control})< / td>
< td> @ Html.TextBoxFor(m => Model.Title,new {@class =form-control})< / td>
< td> @ Html.TextBoxFor(m => Model.SubTitle,new {@class =form-control})< / td>
< td>
@ Html.HiddenFor(m => Model.Id)
@ Html.HiddenFor(m => Model.IsNew)
@ Html.HiddenFor(m => Model.IsDeleted )
< / td>
< / tr>

为了符合您的要求,我尽量保持与您尽可能接近的水平。当您计划动态添加或远程元素时,我通常不会使用表格。有些浏览器表现得不太好,更不用说我的设计师对复杂表引起的渲染仇恨。



好的,现在你有了你需要的东西ASP.NET MVC自动呈现您的项目并自动生成索引。所以让我们来看看如何删除该行。



回到我的Test视图,我添加了一个脚本部分,如下所示:

  @section脚本
{
< script type =text / javascript>
$(function(){
$(table)。on(click,.delete-section,function(){
$(this).closest(' tr')。hide();
$(this).prev('input')。val('true');
});
});
< / script>
}

这很好。当用户点击删除按钮时,他们会立即得到用户界面反馈,表明该行已经消失,当提交表单时,我将收集整个收集的项目,并使用便利的属性告诉我需要从哪些项目中删除我的数据存储。最好的部分是我从来不必迭代我的集合,所有的索引都是自动生成的。



最后我回答你的问题。 b

但是我很好奇我需要做些什么来创造新的行。所以让我们回顾一下Test视图和Ajax.Action助手。您会立即注意到我正在指示浏览器执行POST请求。为什么?因为浏览器可以缓存GET请求以优化性能。通常你不会在乎,因为你通常会为每个请求返回相同的HTML,但由于我们需要包含特殊的命名,所以我们的HTML每次都会有所不同(将索引包含在我们的输入名称中)。其余的是自我解释。真正的诀窍是在服务器端 - 我们如何返回一个部分,以适当的索引添加一行到该表?



不幸的是,该框架虽然擅长查找正常情况下,检查EditorTemplates和DisplayTemplates文件夹似乎会降低。如果我们不使用模板,那么我们的Action就会比我们通常要更脏。


{
var section = new Section(){IsNew = true};
FakedData.Sections.Add(section);
ViewData.TemplateInfo.HtmlFieldPrefix = string.Format(Sections [{0}],FakedData.Sections.Count-1);
返回PartialView(〜/ Views / Home / EditorTemplates / Section.cshtml,section);
}

好的,那么我们看到了什么?首先,我创建了一个新的Section对象,因为我需要它为EditorTemplate进行渲染。我添加了第二个新房产IsNew,但我现在实际上没有做任何事情。我只是想方便地看到我的POST方法中添加和删除了什么。



我将这个新节添加到我的数据存储(FakedData)中。您可以用另一种方式跟踪新请求的数量 - 只要确保每次单击添加节链接时它都会增加。



现在就可以开始使用了。由于我们返回的是部分视图,因此它没有父模型的上下文。如果我们只返回传入的部分模板,它将不会知道它是较大集合的一部分,并且不会适当地生成名称。所以我们用HtmlFieldPrefix字段告诉它我们在哪里。我使用自己的数据存储区来跟踪正确的索引,但是可能来自任何地方,并且如果添加了IsNew属性,您可以在提交时将新(而不是删除的)记录添加到商店中。

就像我说过的,框架只是基于名称找到模板有点麻烦,所以我们必须给出它的路径才能正确返回。



现在我为Test视图添加了一个POST处理程序,并且我可靠地获取了删除,添加和删除的项目数总数。

  [HttpPost] 
public ActionResult Test(FormData form)
{
var sectionCount = form.Sections.Count();
var deletedCount = form.Sections.Count(i => i.IsDeleted);
var newItemCount = form.Sections.Count(i => i.IsNew);

form.Sections = form.Sections.Where(s =>!s.IsDeleted).ToList();
FakedData = form;
返回RedirectToAction(Test);
}

就是这样。我们有一个完整的端到端的渲染你的集合适当的索引,删除行,添加新的行,我们不需要破解模型联编程序,操纵名称或诉诸JavaScript重新编号我们提交的内容。



我非常希望获得关于此的反馈。如果我没有看到更好的答案,这可能是我在将来做这样的事情时总是使用的路线。


I have an HTML that looks like this:

<input type="text" name="data[]" value="George"/>
<input type="text" name="data[]" value="John"/>
<input type="text" name="data[]" value="Paul"/>
<input type="text" name="data[]" value="Ringo"/>

In PHP, I can receive this array like:

$array = $_POST['name'];
// $array[0] == "George"

In ASP.NET MVC the model binder conventions forces me to put indexes in the HTML, so the controller can receive the array.

<!-- HTML for the ASP.NET MVC Version -->
<input type="text" name="data[0]" value="George"/>
<input type="text" name="data[1]" value="John"/>
<input type="text" name="data[2]" value="Paul"/>
<input type="text" name="data[3]" value="Ringo"/>

// C# Controller
public ActionResult SomeAction(string[] data)
{
    // Do stuff
}

If I send the first HTML, data will be null in the Action.

Well, I think this sucks.

If I use client side code to remove or add items to the array, I have to write code to re-index the HTML array.

Is there a way to extend the ASP.NET MVC ModelBinder for binding arrays with no indexes or a workaround to deal with this?

EDIT

After trying your answers I arrive to the conclusion that the example I've posted is not useful to my purposes. My real situation is this:

View

<table>
@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBoxFor(m => Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.HiddenFor(m => Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}
</table>

Action

public ActionResult SaveSections(ICollection<SectionModel> sections)
{
    // DO STUFF
}

I've tried making the HTML inputs by hand, like:

@for (var i = 0; i < Model.Sections.Count; ++i)
{
    <tr>
        <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
        <td>@Html.TextBox("Sections.SectionOrder", Model.Sections[i].SectionOrder, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.Title", Model.Sections[i].Title, new { @class = "form-control" })</td>
        <td>@Html.TextBox("Sections.SubTitle", Model.Sections[i].SubTitle, new { @class = "form-control" })</td>
        <td>
            @Html.Hidden("Sections.SubTitle", Model.Sections[i].Id)
            <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
        </td>
    </tr>
}

But it didn't work...

解决方案

You don't need to explicitly index flat data. If in your view you have

<input type='text' name='data' value='George' />
<input type='text' name='data' value='John' />
<input type='text' name='data' value='Paul' />
<input type='text' name='data' value='Ringo' />

Then in your controller you can use

public ActionResult Create(string[] data)
{
    //data should now be a string array of 4 elements
    return RedirectToAction("Index");
}

To understand the binder, basically work backward. When you submit your form, assuming it is posting to your Create method, the model binder examines the method parameters. It will see you have an array of strings as a parameter and it's called data. It likes strings because form data is submitted as strings. It doesn't have to do any real work here except look in the form collection for elements with a key of data. All items that match are added to an array and assigned to your parameter.

This works because the parameter has the same name as the form elements. If the names didn't match, you'd get null because nothing was found with that key name.

If you use strong views (views with an explicit model) then you can use the MVC helpers to generate these for your and the input elements will be assigned the proper name to map back to your object.

For instance, if you had a model:

public class BandMembers
{
    public string[] data {get; set;}
}

And in your view you specified this as your model and used the appropriate HTML helpers, your action method could instead be like the following:

public ActionResult Create(BandMembers band)
{
    //band now has a property called 'data' with 4 elements
    return RedirectToAction("Index");
}

This should result in an instantiated object called band which has a property names with 4 values. This works because the model binder sees a parameter called band that doesn't match any known keys from the form collection, realizes it's a complex object (not a string, int, string[], int[], etc.) and examines its members. It sees this object has a string array called data and there are keys in the form collection with that name. It gathers up the values, assigns them to the data property and assigns that object to your band parameter.

Now you understand view models!

*Be warned if you'd used BandMembers class in your controller but called it data you'd get a null. This is because the model binder finds items in the form collection with the key data but can't figure out how to cast them from strings to a BandMembers object.

EDIT

With regard to your edit with a deeper example, here's what I've come up with.

First, my model just so we're on the same page. I created a FormData object that contains a List of Section to act as the collection of objects.

public class FormData
{
    public List<Section> Sections { get; set; }

    public FormData()
    {
    }
}

And my Section.cs class:

public class Section
{
    public bool IsDeleted { get; set; }
    public bool IsNew { get; set; }
    public int Id { get; set; }
    public int SectionOrder { get; set; }
    public string Title { get; set; }
    public string SubTitle { get; set; }

    public Section()
    {
    }
}

Using an EditorTemplate on your Section makes it easy to render out the content with indexes generated for you. I've mocked the project up on my own and verified this is working. Unfortunately as you've seen, once you remove an item, your indexes will be out of order. So how do you fix this? Sure you can go and read the indexes and rewrite them, OR - just don't remove them! What I've done in my mock project is add a new property on Section called IsDeleted and render it as a hidden element. In the JavaScript handler for the delete click, I hide the row and update the hidden input for that row's IsDeleted input to 'true'. When I submit the form, I'll now have a complete collection along with a handy flag that lets me know which rows I need to remove from my model.

I created a Test view bound to a model called FormData which contains a List.

@model MVCEditorTemplateDemo.Models.FormData
@using (Html.BeginForm())
{
    <table id="section-container">
        @Html.EditorFor(m => m.Sections)
    </table>

    @Ajax.ActionLink("Add Section", "GetNewSection", "Home", new AjaxOptions() { HttpMethod="POST", InsertionMode=InsertionMode.InsertAfter, UpdateTargetId="section-container" })
    <input type="submit" value="Submit" />
}

Yes, that EditorFor is taking the collection! But how does it know what to do with it? I created a folder in my Views/Home (can be in Shared if you want to use it across controllers) called EditorTemplates in which I place a partial view called Section.cshtml. The name is important - it should match the name of the object it will render. Since my model contains objects called Section, my EditorTemplate should also be called Section.

Here's what it looks like (EditorTemplates\Section.cshtml):

@model MVCEditorTemplateDemo.Models.Section
<tr>
    <td><a href="#" class="edit-section"><span class="glyphicon glyphicon-question-sign"></span></a></td>
    <td>@Html.TextBoxFor(m => Model.SectionOrder, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.Title, new { @class = "form-control" })</td>
    <td>@Html.TextBoxFor(m => Model.SubTitle, new { @class = "form-control" })</td>
    <td>
        @Html.HiddenFor(m => Model.Id)
        @Html.HiddenFor(m => Model.IsNew)
        @Html.HiddenFor(m => Model.IsDeleted)
        <a href="#" class="delete-section"><span class="glyphicon glyphicon-remove"></span></a>
    </td>
</tr>

I tried to keep it as close to what you had as I could in order to match your requirement. I wouldn't normally use tables otherwise when you plan to dynamically add or remote elements. Some browsers don't behave very well not to mention my designer has a burning hatred for the complication tables cause with rendering.

Alright, now you have what you need to let ASP.NET MVC automatically render your items and generate the indexes automatically. So let's see about deleting that row.

Back in my Test view, I've added a scripts section as follows:

@section scripts
{
    <script type="text/javascript">
        $(function () { 
            $("table").on("click", ".delete-section", function() {
                $(this).closest('tr').hide();
                $(this).prev('input').val('true');
            });
        });
    </script>
}

This works perfectly. When a user clicks the delete button, they get an immediate UI feedback that row is gone and when the form is submitted, I'll have the entire collection of items I rendered with a handy property letting me know which items I need to remove from my data store. The best part is I never had to iterate my collection and all my indexes were generated automatically.

And that ends my answer to your question.

But then I got curious what I'd need to do to make new rows. So let's take a look back at the Test view and that Ajax.Action helper. You'll notice right away that I'm instructing the browser to do a POST request. Why? Because browsers can cache GET requests to optimize performance. Normally you would't care as you'd typically be returning the same HTML for every request but since we need to include special naming, our HTML is actually different every time (to include the index in our input names). The rest is self explanatory. The real trick is on the server side - how do we return a partial to add a row to that table with the proper indexing?

Unfortunately the framework, while good at finding Views normally, seems to fall down at checking in the EditorTemplates and DisplayTemplates folders. Our Action is therefor a bit more dirty than we'd normally have if we weren't using templates.

public ActionResult GetNewSection()
{
    var section = new Section() { IsNew = true };
    FakedData.Sections.Add(section);
    ViewData.TemplateInfo.HtmlFieldPrefix = string.Format("Sections[{0}]", FakedData.Sections.Count-1);
    return PartialView("~/Views/Home/EditorTemplates/Section.cshtml", section);
}

Okay, so what are we seeing? First, I create a new Section object since I'm going to need it for the EditorTemplate to render. I added a second new property IsNew but I don't actually do anything with it at the moment. I just wanted a convenient way of seeing what's been added and deleted in my POST method coming up.

I add that new Section to my data store (FakedData). You could instead track the number of new requests in another way - just be sure it increments each time you click the Add Section link.

Now for the trick. Since we're returning a partial view it doesn't have the context of parent model. If we just return the template with only the section passed in, it won't know it's part of a larger collection and won't generate the names appropriately. So we tell it where we are with the HtmlFieldPrefix field. I use my data store to keep track of the proper index but again, this could come from anywhere and if you added the IsNew property, you'll be able to add the new (and not-deleted) records to your store on submit instead.

Like I said, the framework has a little trouble finding the template just based on the name so we have to give the path to it in order for it to return properly. Not a big deal overall but it is a bit annoying.

Now I added a POST handler for the Test view and I am reliably getting a count of items deleted, added and total count. Just remember that rows could be New and Deleted!

[HttpPost]
public ActionResult Test(FormData form)
{
    var sectionCount = form.Sections.Count();
    var deletedCount = form.Sections.Count(i => i.IsDeleted);
    var newItemCount = form.Sections.Count(i => i.IsNew);

    form.Sections = form.Sections.Where(s => !s.IsDeleted).ToList();
    FakedData = form;
    return RedirectToAction("Test");
}

And that's it. We have a complete end-to-end of rendering out your collection with proper indexes, "deleting" rows, adding new rows and we didn't have to hack the model binder, manipulate the names or resort to JavaScript hacks to re-number our elements on submit.

I'm eager for feedback on this. If I don't see a better answer, this might be the route I always use when doing stuff like this in the future.

这篇关于在没有索引的ASP.NET MVC中绑定数组?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆