从View将项目添加到List并传递到MVC5中的Controller [英] Add item into List from View and pass to Controller in MVC5

查看:105
本文介绍了从View将项目添加到List并传递到MVC5中的Controller的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个这样的表格:

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的价值如何传递价值形式

我也在解决方案

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

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

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

让我们开始吧!

编辑视图 首先,为我们的人员创建一个初次编辑视图,使其看起来像上图所示:

@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 UI Sortable 时,我正在使用jQuery选择器和dom函数删除电影. a>使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,而不是四个项目. 2.如果要在HTML ASP.NET中重新排序列表,则在进行模型绑定时,MVC将应用索引顺序而不是字段顺序.

这基本上意味着添加/删除/重新排序方案已无济于事.这不是不可能,但是跟踪添加/删除/重新排序动作并为所有字段属性重新编制索引会造成很大的麻烦.

现在,有人可能会说–嘿,为什么不实施非顺序收集模型绑定器?"

是的,您可以编写非顺序收集模型绑定程序的代码.但是,您将面临两个主要问题.第一个原因是IValueProvider并未提供对BindingContext中的所有值进行迭代的方法,您可以通过对模型绑定程序进行硬编码来访问当前的HttpRequest Form值集合(这意味着如果有人决定通过Json提交表单)来解决*或查询模型绑定程序无法使用的参数),或者我看到了另一种疯狂的解决方法,它可以从CollectionName [0]到CollectionName [Int32.MaxValue]逐一检查* BindingContext(这是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的模型绑定嘿,不要寻找标准的数值集合索引,而是寻找我们指定的自定义Index值,当您处于完毕".这有什么帮助?

我们可以指定所需的任何索引值 索引不必是连续的,项目将按提交时在HTML中的顺序放入集合中. am!可以解决大多数问题,但不能解决所有问题.

解决方案

首先,ASP.NET MVC没有HTML助手来生成"[something] .Index"模式,这是一个主要问题,因为这意味着我们无法使用验证和自定义编辑器.我们可以通过利用一些ASP.NET模板fu来解决此问题.我们要做的是将电影"编辑器移至其自己的局部视图(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>

注意两件事–首先,电影部分编辑视图使用标准的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字段追加到输出 (请记住,使用.Index模式,索引可以是任何字符串)
  • 通过IDisposable限制帮助程序的执行范围,并设置 模板渲染上下文(html帮助器和显示/编辑器 模板)为"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/

I have a form like this:

JS Code:

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

Old Code:

Model:

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

Controller:

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

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

I choose a color and click Add, it'll add more item in to list. I'm newbie in ASP.NET MVC. I just know value from Razor how to pass value form

I also asked about same thing in here and received kind explain. But, it's static value which pass from controller and then is used to bind into razor. But now, it isn't static.

Could you tell me how to bind razor item in to list to post it to controller? I would be very grateful if you give me some suggest.

Thanks for your kind helping. (bow)

解决方案

You can refer this post. It works perfect for me.

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

I will quote it below:

The aspects I will consider are:

  1. Dynamically adding, removing and reordering items to/from the collection
  2. Validation implications
  3. Code Reusability and Refactoring implications I will assume that you are already familiar with ASP.NET MVC and basic JavaScript concepts.

Source Code All source code is available on GitHub

The Sample What I am going to build is a little sample where we have a user who has a list of favourite movies. It will look roughly like on the image below and will allow for adding new favourite movies, removing favourite movies and also reordering them up and down using the drag handler.

In Part 1 I look at implementing collection editing by sticking to facilities provided to us by ASP.NET MVC such as views, partial views, editor templates, model binding, model validation, etc.

Domain Model The domain model is basically:

public class User
{
    public int? Id { get; set; }
    [Required]
    public string Name { get; set; }
    public IList<Movie> FavouriteMovies { get; set; }
}

and

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

Let’s get cracking!

An Edit View Let’s start by creating a first-pass edit view for our Person to look like the one on the image above:

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

he view is creating a list of editing controls for each of the movies in Person.FavouriteMovies. I am using a jQuery selector and dom function to remove a movie when the user clicks "Delete" and also a jQuery UI Sortable to make the items from the HTML list drag and droppable up and down.

With this done we immediately face the first problem: We haven’t implemented the "Add another". Before we do that let’s consider how ASP.NET MVC model binding of collections works.

ASP.NET MVC Collection Model Binding Patterns There are two patterns for model binding collections in ASP.NET MVC. The first one you have just seen:

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

which generates similar 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>

This is really great for displaying collections and editing static length collections, but problematic when we want to edit variable length collections, because:

1. Indices have to be sequential (0, 1, 2, 3, …). If they aren’t ASP.NET MVC stops at the first gap. E.g. if you have item 0, 1, 3, 4 after the model binding has finished you will end up with a collection of two items only – 1 and 2 instead of four items. 2. If you were to reorder the list in the HTML ASP.NET MVC will apply the indices order not the fields order when doing model binding.

This basically means that add/remove/reorder scenarios are no go with this. It’s not impossible but it will be big big mess tracking add/remove/reorder actions and re-indexing all field attributes.

Now, someone might say – "Hey, why don’t you just implement a non-sequential collection model binder?" .

Yes, you can write the code for a non-sequential collection model binder. You will however face two major issues with that however. The first being that the IValueProvider doesn’t expose a way to iterate through all values in the BindingContext which you can workaround* by hardcoding the model binder to access the current HttpRequest Form values collection (meaning that if someone decides to submit the form via Json or query parameters your model binder won’t work) or I’ve seen one more insane workaround which checks the *BindingContext one by one from CollectionName[0] to CollectionName[Int32.MaxValue] (that’s 2 billion iterations!).

Second major issue is that once you create a sequential collection from the non-sequential indices and items and you have a validation error and you re-render the form view your ModelState will no longer match the data. An item that used to be at index X is now at index X-1 after another item before it was deleted, however the ModelState validation message and state still point to X, because this is what you submitted.

So, even a custom model binder won’t help.

Thankfully there is a second pattern, which mostly helps for what we want to achieve (even though I don’t think it was designed to solve exactly this):

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

Notice how we have introduced an ".Index" hidden field for each collection item. By doing that we tell ASP.NET MVC’s model binding "Hey, don’t look for a standard numeric collection index, but instead look for the custom Index value we have specified and just get me the list of items in a collection when you are done". How does this help?

We can specify any index value we want The index doesn’t have to be sequential and items will be put in the collection in the order they are in the HTML when submitted. Bam! That’s solves most, but not all of our problems.

The Solution

Firstly, ASP.NET MVC doesn’t have HTML helpers to generate the "[something].Index" pattern which is major problem since it means we can’t use validation and custom editors. We can fix that by utilizing some ASP.NET templating fu. What we are going to do is move the Movie editor to a its own partial view (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>

And update our Edit view to use it:

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

Notice two things – firstly the Movie partial edit view uses standard Html helpers and secondly there is a call to something custom called Html.BeginCollectionItem. *You might even ask yourself: Wait a second. This won’t work, because the partial view will produce names such as "Title" instead of "FavouriteMovies[xxx].Title", so let me show you the source code of *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;
    }
}

This helper does two things:

  • Appends a hidden Index field to the output with a random GUID value (remember that using the .Index pattern an index can be any string)
  • Scopes the execution of the helper via an IDisposable and sets the template rendering context (html helperes and display/editor templates) to be "FavouriteMovies[GUID].", so we end up with HTML like this:

    Title

This solves the problem of using Html field templates and basically reusing ASP.NET facilities instead of having to write html by hand, but it leads me to the second quirk that we need to address.

Let me show you the second and final problem. Disable client side validation and delete the title of e.g. "Movie 2" and click submit. Validation will fail, because Title of a movie is a required field, but while we are shown the edit form again** there are no validation messages**:

Why is that? It’s the same problem I mentioned earlier in this post. Each time we render the view we assign different names to the fields, which do not match the ones submitted and leads to a *ModelState *inconsistency. We have to figure out how to persist the name and more specifically the Index across requests. We have two options:

Add a hidden CollectionIndex field and CollectionIndex property on the Movie object to persist the FavouriteMovies.Index. This however is intrusive and suboptimal. Instead of polluting the Movie object with an extra property be smart and in our helper Html.BeginCollectionItem reapply/reuse the submitted FavouriteMovies.Index form values. Let’s replace in Html.BeginCollectionItem this line:

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

with:

string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);

And here’ is the code for 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();
}

We get all submitted values for e.g. "FavouriteMovie.Index" put them in a queue, which we store for the duration of the request. Each time we render a collection item we dequeue its old index value and if none is available we generate a new one. That way we preserve the Index across requests and can have a consistent ModelState and see validation errors and messages:

All that is left is to implement the "Add another" button functionality and we can do that easily by appending a new row to the movie editor, which we can fetch using Ajax and use our existing MovieEntryEditor.cshtml partial view like that:

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

And then add the follwing "Add Another" click handler:

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

Done;

Conclusion While not immediately obvious editing variable length reorderable collections with standard ASP.NET MVC is possible and what I like about this approach is that:

We can keep using traditional ASP.NET html helpers, editor and display templates (Html.EditorFor, etc.) with in our collection editing We can make use of the ASP.NET MVC model validation client and server side What I don’t like that much however is:

That we have to use an AJAX request to append a new row to the editor. That we need to use the name of the collection in the movie editor partial view, but otherwise when doing the standalone AJAX get request the name context won’t be properly set for the partial template fields. I would love to hear your thoughts. The sample source code is available on my GitHub


Other way: http://blog.stevensanderson.com/2008/12/22/editing-a-variable-length-list-of-items-in-aspnet-mvc/

这篇关于从View将项目添加到List并传递到MVC5中的Controller的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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