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

时间:2016-08-24 17:26:48

标签: c# jquery asp.net-mvc 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)
    });
});

旧代码:

型号:

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

控制器:

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

查看:

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

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

我在here也问过同样的问题并得到了解释。但是,它是从控制器传递的静态值,然后用于绑定到剃刀。 但现在,它并非一成不变。

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

感谢您的帮助。 (弓)

1 个答案:

答案 0 :(得分:18)

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

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

我将在下面引用它:

我将考虑的方面是:

  1. 动态添加,删除和重新排序项目 集合
  2. 验证影响
  3. 代码可重用性和重构含义我将假设您已经熟悉ASP.NET MVC和基本的JavaScript概念。
  4. 源代码 All source code is available on 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; }
    }
    

    public class Movie
    {
        [Required]
        public string Title { get; set; }
        public int Rating { get; set; }
    }
    

    让我们开始吧!

    编辑视图 让我们首先为我们的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>
    }
    

    他正在为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)
    …
    }
    

    生成类似的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>
    

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

    <强> 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="" />
    

    注意我们如何为每个集合项引入“.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>
    

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

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

    注意两件事 - 首先,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;
        }
    }
    

    这个助手做了两件事:

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

      标题

    这解决了使用Html字段模板的问题,并且基本上重用了ASP.NET设施,而不是手工编写html,但它引导我进入我们需要解决的第二个怪癖。

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

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

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

    string itemIndex = Guid.New().ToString();
    

    使用:

    string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
    

    这里是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();
    }
    

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

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

    public ActionResult MovieEntryRow()
    {
        return PartialView("MovieEntryEditor");
    }
    

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

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

    完成;

    结论 虽然不能立即用标准的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/