从View中将项添加到List中并传递给MVC5中的Controller

Lon*_*yen 7 c# asp.net-mvc jquery razor

我有这样的表格:https://gyazo.com/289a1ac6b7ecd212fe79eec7c0634574

JS代码:

$(document).ready(function() {
    $("#add-more").click(function() {
        selectedColor = $("#select-color option:selected").val();
        if (selectedColor == '')
            return;
        var color = ' <
            div class = "form-group" >
            <
            label class = "col-md-2 control-label" > Color: < /label> <
            div class = "col-md-5" > < label class = "control-label" > ' + selectedColor + ' < /label></div >
            <
            /div>
        ';
        var sizeAndQuantity = ' <
            div class = "form-group" >
            <
            label class = "col-md-2 control-label" > Size and Quantity: < /label> <
            div class = "col-md-2" > < label class = "control-label" > S < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > M < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > L < /label><input type="text" class="form-control"></div >
            <
            div class = "col-md-2" > < label class = "control-label" > XL < /label><input type="text" class="form-control"></div >
            <
            /div>
        ';
        html = color + sizeAndQuantity
        $("#appendTarget").append(html)
    });
});
Run Code Online (Sandbox Code Playgroud)

旧代码:

模型:

namespace ProjectSem3.Areas.Admin.Models
{
    public class ProductViewModel
    {
        public ProductGeneral ProductGeneral { get; set; }
        public List<SizeColorQuantityViewModel> SizeColorQuantities { get; set; }
    }
    public class ProductGeneral
    {
        public string Product { get; set; }
        public string Description { get; set; }
        public string ShortDescription { get; set; }
        public List<ProductCategory> Categories { get; set; }
        public string SKU { get; set; }
        public float Price { get; set; }
        public float PromotionPrice { get; set; }
        public bool Status { get; set; }
    }

    public class SizeColorQuantityViewModel
    {
        public string ColorId { get; set; }
        public List<SizeAndQuantity> SizeAndQuantities { get; set; }
    }
    public class SizeAndQuantity
    {
        public string SizeId { get; set; }
        public int Quantity { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

控制器:

public class ProductController : Controller
    {
        // GET: Admin/Product
        public ActionResult Create()
        {
            var colors = new List<string>() { "Red", "Blue" };
            var sizes = new List<string>() { "S", "M", "L", "XL" };
            var categories = new ProductDao().LoadProductCategory();

            var productGeneral = new ProductGeneral()
            {
                Categories = categories
            };
            var model = new ProductViewModel
            {
                ProductGeneral = productGeneral,
                SizeColorQuantities = new List<SizeColorQuantityViewModel>()
            };


            foreach (var color in colors)
            {
                var child = new SizeColorQuantityViewModel
                {
                    ColorId = color,
                    SizeAndQuantities = new List<SizeAndQuantity>()
                };
                model.SizeColorQuantities.Add(child);
                foreach (var size in sizes)
                {
                    child.SizeAndQuantities.Add(new SizeAndQuantity()
                    {
                        SizeId = size 
                    });
                }
            }
            return View(model);
        }

        // POST: Admin/Product
        [HttpPost]
        public ActionResult Create(ProductViewModel model)
        {
            return View();
        }
    }
Run Code Online (Sandbox Code Playgroud)

视图:

@for (var i = 0; i < Model.SizeColorQuantities.Count; i++)
{
<div class="form-group">
   <label class="col-md-2 control-label">Color:</label>
   <div class="col-md-2">
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].ColorId, new { @class = "form-control", @readonly = "readonly" })
   </div>
</div>
<div class="form-group">
   <label class="col-md-2 control-label">Size and Quantity:</label>
   @for (var j = 0; j < Model.SizeColorQuantities[i].SizeAndQuantities.Count; j++)
   {
   <div class="col-md-2">
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].SizeId, new
      {
      @class = "form-control",
      @style = "margin-bottom: 15px",
      @readonly = "readonly"
      })
      @Html.TextBoxFor(m => m.SizeColorQuantities[i].SizeAndQuantities[j].Quantity, new { @class = "form-control" })
   </div>
   }
</div>
}
Run Code Online (Sandbox Code Playgroud)

我选择一种颜色并单击添加,它会在列表中添加更多项目.我是ASP.NET MVC的新手.我只知道Razor如何传递价值形式的价值

我也在这里询问了同样的事情并得到了实实在在的解释.但是,它是从控制器传递的静态值,然后用于绑定到剃刀.但现在,它并非一成不变.

你能告诉我如何将razor项目绑定到列表中以将其发布到控制器吗?如果你给我一些建议,我将非常感激.

谢谢你的帮助.(弓)

Lon*_*yen 24

你可以参考这篇文章.它对我来说很完美.

http://ivanz.com/2011/06/16/editing-variable-length-reorderable-collections-in-asp-net-mvc-part-1/

我将在下面引用它:

我将考虑的方面是:

  1. 向集合中动态添加,删除和重新排序项目
  2. 验证含义
  3. 代码可重用性和重构含义我将假设您已经熟悉ASP.NET MVC和基本JavaScript概念.

源代码 所有源代码都可以在GitHub上找到

示例我要构建的是一个小样本,我们的用户拥有最喜欢的电影列表.它看起来大致如下图所示,可以添加新喜欢的电影,删除喜欢的电影,还可以使用拖动处理程序上下重新排序.

在第1部分中,我将通过坚持ASP.NET MVC提供给我们的工具(如视图,部分视图,编辑器模板,模型绑定,模型验证等)来实现集合编辑.

域模型 域模型基本上是:

public class User
{
    public int? Id { get; set; }
    [Required]
    public string Name { get; set; }
    public IList<Movie> FavouriteMovies { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

public class Movie
{
    [Required]
    public string Title { get; set; }
    public int Rating { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

让我们开始吧!

编辑视图 让我们首先为我们的Person创建第一遍编辑视图,使其看起来像上图中的那个:

@model CollectionEditing.Models.User
@{ ViewBag.Title = "Edit My Account"; }

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>My Details</legend>

        @Html.HiddenFor(model => model.Id)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>
    </fieldset>

    <fieldset>
        <legend>My Favourite Movies</legend>

        @if (Model.FavouriteMovies == null || Model.FavouriteMovies.Count == 0) {
            <p>None.</p>
        } else {
            <ul id="movieEditor" style="list-style-type: none">
                @for (int i=0; i < Model.FavouriteMovies.Count; i++) {
                    <li style="padding-bottom:15px">
                        <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

                        @Html.LabelFor(model => model.FavouriteMovies[i].Title)
                        @Html.EditorFor(model => model.FavouriteMovies[i].Title)
                        @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)

                        @Html.LabelFor(model => model.FavouriteMovies[i].Rating)
                        @Html.EditorFor(model => model.FavouriteMovies[i].Rating)
                        @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Rating)

                        <a href="#" onclick="$(this).parent().remove();">Delete</a>
                    </li>
                }
            </ul>
            <a href="#">Add another</a>
        }

        <script type="text/javascript">
            $(function () {
                $("#movieEditor").sortable();
            });
        </script>
    </fieldset>

    <p>
        <input type="submit" value="Save" />
        <a href="/">Cancel</a>
    </p>
}
Run Code Online (Sandbox Code Playgroud)

他认为正在为Person.FavouriteMovies中的每部电影创建一个编辑控件列表.我正在使用jQuery选择器和dom函数在用户单击"删除"时删除电影,还使用jQuery UI Sortable来使HTML列表中的项目上下拖放.

完成后我们立即面临第一个问题:我们还没有实现"添加另一个".在我们这样做之前,让我们考虑一下ASP.NET MVC模型对集合的绑定是如何工作的.

ASP.NET MVC集合模型绑定模式ASP.NET MVC中的模型绑定集合有两种模式.你刚看到的第一个:

@for (int i=0; i < Model.FavouriteMovies.Count; i++) {
    @Html.LabelFor(model => model.FavouriteMovies[i].Title)
    @Html.EditorFor(model => model.FavouriteMovies[i].Title)
    @Html.ValidationMessageFor(model => model.FavouriteMovies[i].Title)
…
}
Run Code Online (Sandbox Code Playgroud)

它生成类似的HTML:

<label for="FavouriteMovies_0__Title">Title</label>
<input id="FavouriteMovies_0__Title" name="FavouriteMovies[0].Title" type="text" value="" />
<span class="field-validation-error">The Title field is required.</span>
Run Code Online (Sandbox Code Playgroud)

这对于显示集合和编辑静态长度集合非常有用,但是当我们想要编辑可变长度集合时会出现问题,因为:

1.指数必须是连续的(0,1,2,3,......).如果他们不是ASP.NET MVC停在第一个差距.例如,如果在模型绑定完成后您有项目0,1,3,4,则最终只会收集两个项目--1和2而不是4个项目.2.如果要重新排序HTML中的列表,MVC将在进行模型绑定时应用索引顺序而不是字段顺序.

这基本上意味着添加/删除/重新排序方案不适用于此.这并非不可能,但它将是一个大的混乱跟踪添加/删除/重新排序操作并重新索引所有字段属性.

现在,有人可能会说 - "嘿,你为什么不只是实现一个非连续的集合模型绑定器?".

是的,您可以编写非顺序集合模型绑定器的代码.但是,您将面临两个主要问题.第一个是IValueProvider没有公开迭代BindingContext中所有值的方法,你可以通过对模型绑定器进行硬编码来访问当前的HttpRequest Form值集合(这意味着如果有人决定通过Json提交表单)或查询参数您的模型绑定器将无法工作)或我已经看到一个更疯狂的解决方法,从CollectionName [0]逐个检查*BindingContext到CollectionName [Int32.MaxValue](这是20亿次迭代!).

第二个主要问题是,一旦您从非顺序索引和项目创建顺序集合,并且您有验证错误并且您重新呈现表单视图,您的ModelState将不再匹配数据.以前在索引X处的项目现在在删除之前的另一个项目之后的索引X-1处,但是ModelState验证消息和状态仍然指向X,因为这是您提交的内容.

因此,即使是自定义模型绑定器也无济于事.

值得庆幸的是,还有第二种模式,它主要有助于我们想要实现的目标(即使我认为它不是为了解决这个问题而设计的):

<input type="hidden" name="FavouriteMovies.Index" value="indexA"/>
<input name="FavouriteMovies[indexA].Title" type="text" value="" />
<input name="FavouriteMovies[indexA].Rating" type="text" value="" />
<input type="hidden" name="FavouriteMovies.Index" value="indexB"/>
<input name="FavouriteMovies[indexB].Title" type="text" value="" />
<input name="FavouriteMovies[indexB].Rating" type="text" value="" />
Run Code Online (Sandbox Code Playgroud)

请注意我们如何为每个集合项引入".Index"隐藏字段.通过这样做,我们告诉ASP.NET MVC的模型绑定"嘿,不要寻找标准的数字集合索引,而是寻找我们已经指定的自定义索引值,并在你是时给我一个集合中的项目列表完成".这有什么用?

我们可以指定我们想要的任何索引值索引不必是顺序的,并且项目将按照提交时在HTML中的顺序放入集合中.巴姆!这解决了大多数问题,但不是我们所有的问题.

解决方案

首先,ASP.NET MVC没有HTML帮助程序来生成"[something] .Index"模式,这是主要问题,因为这意味着我们无法使用验证和自定义编辑器.我们可以通过使用一些ASP.NET模板fu来解决这个问题.我们要做的是将Movie编辑器移动到它自己的局部视图(MovieEntryEditor.cshtml):

@model CollectionEditing.Models.Movie

<li style="padding-bottom:15px">
    @using (Html.BeginCollectionItem("FavouriteMovies")) {
        <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

        @Html.LabelFor(model => model.Title)
        @Html.EditorFor(model => model.Title)
        @Html.ValidationMessageFor(model => model.Title)

        @Html.LabelFor(model => model.Rating)
        @Html.EditorFor(model => model.Rating)
        @Html.ValidationMessageFor(model => model.Rating)

        <a href="#" onclick="$(this).parent().remove();">Delete</a>
    }
</li>
Run Code Online (Sandbox Code Playgroud)

并更新我们的编辑视图以使用它:

<ul id="movieEditor" style="list-style-type: none">
    @foreach (Movie movie in Model.FavouriteMovies) {
        Html.RenderPartial("MovieEntryEditor", movie);
    }
</ul>
<p><a id="addAnother" href="#">Add another</a>
Run Code Online (Sandbox Code Playgroud)

注意两件事 - 首先,Movie部分编辑视图使用标准的Html助手,然后调用自定义的Html.BeginCollectionItem.*你甚至可能会问自己:等一下.这不起作用,因为局部视图将产生名称,如"Title"而不是"FavouriteMovies [xxx] .Title",所以让我向您展示*Html.BeginCollectionItem的源代码:

public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html,                                                       string collectionName)
{
    string itemIndex = Guid.NewGuid().ToString();
    string collectionItemName = String.Format("{0}[{1}]", collectionName, itemIndex);

    TagBuilder indexField = new TagBuilder("input");
    indexField.MergeAttributes(new Dictionary<string, string>() {
        { "name", String.Format("{0}.Index", collectionName) },
        { "value", itemIndex },
        { "type", "hidden" },
        { "autocomplete", "off" }
    });

    html.ViewContext.Writer.WriteLine(indexField.ToString(TagRenderMode.SelfClosing));
    return new CollectionItemNamePrefixScope(html.ViewData.TemplateInfo, collectionItemName);
}

private class CollectionItemNamePrefixScope : IDisposable
{
    private readonly TemplateInfo _templateInfo;
    private readonly string _previousPrefix;

    public CollectionItemNamePrefixScope(TemplateInfo templateInfo, string collectionItemName)
    {
        this._templateInfo = templateInfo;

        _previousPrefix = templateInfo.HtmlFieldPrefix;
        templateInfo.HtmlFieldPrefix = collectionItemName;
    }

    public void Dispose()
    {
        _templateInfo.HtmlFieldPrefix = _previousPrefix;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个助手做了两件事:

  • 使用随机GUID值将隐藏的索引字段追加到输出中(请记住,使用.Index模式,索引可以是任何字符串)
  • 通过IDisposable实现助手的执行,并将模板渲染上下文(html helperes和display/editor templates)设置为"FavouriteMovies [GUID].",因此我们最终得到如下HTML:

    标题

这解决了使用Html字段模板和基本上重用ASP.NET设施而不必手动编写html的问题,但它引导我到了我们需要解决的第二个怪癖.

让我告诉你第二个也是最后一个问题.禁用客户端验证并删除例如"Movie 2"的标题,然后单击"提交".验证将失败,因为电影的标题是必填字段,但是当我们再次显示编辑表单时**没有验证消息**:

这是为什么?这是我在本文前面提到的同样问题.每次渲染视图时,我们都会为字段指定不同的名称,这些名称与提交的字段不匹配,并导致*ModelState*不一致.我们必须弄清楚如何在请求之间保留名称,更具体地说是索引.我们有两种选择:

在Movie对象上添加隐藏的CollectionIndex字段和CollectionIndex属性以保留FavouriteMovies.Index.然而,这是侵入性的和次优的.而不是使用额外的属性污染Movie对象是聪明的,并且在我们的帮助器中Html.BeginCollectionItem重新应用/重用提交的FavouriteMovies.Index表单值.让我们在Html.BeginCollectionItem中替换这一行:

string itemIndex = Guid.New().ToString();
Run Code Online (Sandbox Code Playgroud)

有:

string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
Run Code Online (Sandbox Code Playgroud)

这里是GetCollectionItemIndex的代码:

private static string GetCollectionItemIndex(string collectionIndexFieldName)
{
    Queue<string> previousIndices = (Queue<string>) HttpContext.Current.Items[collectionIndexFieldName];
    if (previousIndices == null) {
        HttpContext.Current.Items[collectionIndexFieldName] = previousIndices = new Queue<string>();

        string previousIndicesValues = HttpContext.Current.Request[collectionIndexFieldName];
        if (!String.IsNullOrWhiteSpace(previousIndicesValues)) {
            foreach (string index in previousIndicesValues.Split(','))
                previousIndices.Enqueue(index);
        }
    }

    return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
}
Run Code Online (Sandbox Code Playgroud)

我们得到所有提交的值,例如"FavouriteMovie.Index"将它们放入队列中,我们会在请求期间存储这些值.每次我们渲染一个集合项时,我们都会将其旧的索引值出列,如果没有,我们会生成一个新的索引值.这样我们就可以跨请求保留索引,并且可以拥有一致的ModelState并查看验证错误和消息:

剩下的就是实现"添加另一个"按钮功能,我们可以通过向电影编辑器添加一个新行来轻松实现,我们可以使用Ajax获取它并使用我们现有的MovieEntryEditor.cshtml局部视图:

public ActionResult MovieEntryRow()
{
    return PartialView("MovieEntryEditor");
}
Run Code Online (Sandbox Code Playgroud)

然后添加以下"添加另一个"点击处理程序:

$("#addAnother").click(function () {
    $.get('/User/MovieEntryRow', function (template) {
        $("#movieEditor").append(template);
    });
});
Run Code Online (Sandbox Code Playgroud)

完成;

结论虽然使用标准ASP.NET MVC编辑可变长度可重新排序的集合并不是很明显,但我喜欢这种方法的是:

我们可以在我们的集合编辑中继续使用传统的ASP.NET html助手,编辑器和显示模板(Html.EditorFor等)我们可以利用ASP.NET MVC模型验证客户端和服务器端我不喜欢什么然而,那是:

我们必须使用AJAX请求将新行追加到编辑器中.我们需要在电影编辑器局部视图中使用集合的名称,但是在执行独立的AJAX get请求时,将不会为部分模板字段正确设置名称上下文.我很想听听你的想法.我的GitHub上提供了示例源代码


其他方式:http://blog.stevensanderson.com/2008/12/22/editing-a-variable-length-list-of-items-in-aspnet-mvc/

  • github 链接给了我丢失的一段代码`string collectionIndexFieldName = String.Format("{0}.Index", collectionName);` 这是一个很好的答案,通过了每个解决方案的优缺点。感谢您在解释所有细微差别方面付出的额外努力。 (2认同)