zoukankan      html  css  js  c++  java
  • MVC批量更新,可验证并解决集合元素不连续控制器接收不完全的问题

    在"MVC批量添加,增加一条记录的同时添加N条集合属性所对应的个体"中,有2个问题待解决:

    1、由jquery动态生成了表单元素,但不能实施验证。
    2、一旦集合元素不连续,控制器就无法接收到完整的数据。

    批量添加或更新,控制器能接收的方式,大致有2种:
    Category.Name
    Category.Products[0].Name
    Category.Products[3].Name  
    Category.Products[6].Name 

    <input type="hidden" name="CgdModel.Ubis.Index" value="0" />
    <input type="text" name="CgdModel.Ubis[0].Fdt"...
    <input type="text" name="CgdModel.Ubis[0].Memo...

    <input type="hidden" name="CgdModel.Ubis.Index" value="1" />
    <input type="text" name="CgdModel.Ubis[1].Fdt"...
    <input type="text" name="CgdModel.Ubis[1].Memo...

     

    本篇使用第二种方式,并解决上文提到的2个问题。

    批量更新的界面为:
    1

     

    □ Model

    一个用户,可以有任意多喜欢看的电影。

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

       
    □ HomeController

    显示更新界面,接收更新数据,并相应视图添加按钮,通过部分视图添加新行。

        public class HomeController : Controller
        {
            private static User _currentUser;
     
            private static User CurrentUser
            {
                get
                {
                    if (_currentUser == null)
                    {
                        _currentUser = GetFakeUser();
                    }
                    return _currentUser;
                }
                set { _currentUser = value; }
            }
     
            private static User GetFakeUser()
            {
                return new User()
                {
                    Id = 1,
                    Name = "darren",
                    FavouriteMovies = new List<Movie>()
                    {
                        new Movie(){Title = "movie1"},
                        new Movie(){Title = "movie2"},
                        new Movie(){Title = "movie3"}
                    }
                };
            }
     
            public ActionResult EditUser()
            {
                return View(CurrentUser);
            }
     
            [HttpPost]
            public ActionResult EditUser(User user)
            {
                if (!ModelState.IsValid)
                {
                    return View(user);
                }
                CurrentUser = user;
                return View(CurrentUser);
            }
     
            //响应视图添加按钮,通过部分视图生成一行
            public ActionResult MovieEntryRow()
            {
                return PartialView("MovieEntryEditor");
            }     
     

     

    □ Home/EditUser.cshtml
    其中,每组集合元素,即用户喜欢的电影通过部分视图MovieEntryEditor.cshtml渲染出来。

    @using VariableCollection.Models
    @model VariableCollection.Models.User
     
    @{
        ViewBag.Title = "EditUser";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
     
    @using (Html.BeginForm())
    {
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>用户及喜欢电影</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>我最喜欢的电影</legend>
            @if (Model.FavouriteMovies == null || Model.FavouriteMovies.Count == 0)
            {
                <p>没有喜欢的电影~~</p>
            }
            else
            {
                <ul id="movieEditor" style="list-style-type: none">
                    @foreach (Movie movie in Model.FavouriteMovies)
                    {
                        Html.RenderPartial("MovieEntryEditor", movie);
                    }
                </ul>
                <a id="addAnother" href="#">添加行</a>
            }
        </fieldset>
        
        <p>
            <input type="submit" value="提交"/>
        </p>
    }
     
    @section scripts
    {
        <script type="text/javascript">
            $(function() {
                $('#movieEditor').sortable();
     
                $("#addAnother").click(function () {
                    $.get('/Home/MovieEntryRow', function (template) {
                        $("#movieEditor").append(template);
                    });
                });
            });
        </script>
    }
     

     

    □ Layout.cshtml中把jquery ui和验证相关js引进来

        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")
        @Scripts.Render("~/bundles/jqueryval")

     

    □ 写一个帮助方法,目的是生成如下格式:

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="6d85a95b-1dee-4175-bfae-73fad6a3763b" />
    <input class="text-box single-line" name="FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b].Title" type="text" value="Movie 1" />
    ...

     

    帮助方法除了要生成目标格式,还要考虑:
    ● 为了保证隐藏域value值的唯一性,每次渲染部分视图MovieEntryEditor.cshtml,让这里的value有一个唯一的GUID字符串。
    ● 还需要替换ViewData.TemplateInfo的HtmlFieldPrefix属性值为FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b]。

    using System;
    using System.Collections.Generic;
    using System.Web;
    using System.Web.Mvc;
     
    namespace VariableCollection.Extension
    {
        public static class CollectionEditingHtmlExtensions
        {
            /// <summary>
            /// 目标是生成如下格式
            ///<input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="6d85a95b-1dee-4175-bfae-73fad6a3763b" />
            ///<label>Title</label>
            ///<input class="text-box single-line" name="FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b].Title" type="text" value="Movie 1" />
            ///<span class="field-validation-valid"></span>
            /// </summary>
            /// <typeparam name="TModel"></typeparam>
            /// <param name="html"></param>
            /// <param name="collectionName">集合属性的名称</param>
            /// <returns></returns>
            public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName)
            {
                string itemIndex = Guid.NewGuid().ToString();
     
                //比如,FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b]
                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) }, //name="FavouriteMovies.Index"
                    { "value", itemIndex },//value="6d85a95b-1dee-4175-bfae-73fad6a3763b"
                    { "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;
                 }
             }
        }
    }
     

     

    由于BeginCollectionItem()方法返回的类型是实现了IDisposable接口的CollectionItemNamePrefixScope类,所以,我们在部分视图MovieEntryEditor.cshtml中可以使用using语句。

    @using VariableCollection.Extension
    @model VariableCollection.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(); ">删除行</a>
        }
    </li>


    运行,如果不符合验证要求,会报错,似乎看上去不错:
    2

    但,如果把_Layout.cshtml中有关客户端异步验证的js引用去掉,即把@Scripts.Render("~/bundles/jqueryval")注释掉,再次运行,居然服务端不再报错:
    3

    为什么?这是由User的ModelState状态不一致引起的。先来看下ModelState类:

    public class ModelState
    {
        public ModelErrorCollection Errors{get;}
        public ValueProviderResult Value{get;set;}
    }

    可见,ModelState不仅保存这有关Model的一切错误信息,还保存着有ValueProvider提供的表单数据。

    在提交表单之前,界面的input大概是这样的:

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="111" />
    <input class="text-box single-line" name="FavouriteMovies[111].Title" type="text" value="Movie 1" />
    <input class="text-box single-line" name="FavouriteMovies[111].Rating" type="text" value="Rating 1" />

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="222" />
    <input class="text-box single-line" name="FavouriteMovies[222].Title" type="text" value="Movie 2" />
    <input class="text-box single-line" name="FavouriteMovies[222].Rating" type="text" value="Rating 2" />

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="333" />
    <input class="text-box single-line" name="FavouriteMovies[333].Title" type="text" value="Movie 3" />
    <input class="text-box single-line" name="FavouriteMovies[333].Rating" type="text" value="Rating 3" />

     

    当提交失败,回到原先视图界面,这时候,所有的集合元素都需要通过MovieEntryEditor.cshtml来渲染,界面的input大概变成这样:

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="777" />
    <input class="text-box single-line" name="FavouriteMovies[777].Title" type="text" value="Movie 1" />
    <input class="text-box single-line" name="FavouriteMovies[777].Rating" type="text" value="Rating 1" />

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="888" />
    <input class="text-box single-line" name="FavouriteMovies[888].Title" type="text" value="Movie 2" />
    <input class="text-box single-line" name="FavouriteMovies[888].Rating" type="text" value="Rating 2" />

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="999" />
    <input class="text-box single-line" name="FavouriteMovies[999].Title" type="text" value="Movie 3" />
    <input class="text-box single-line" name="FavouriteMovies[999].Rating" type="text" value="Rating 3" />

     

    提交前后,ModelState的状态是不一致的,导致服务端验证失败。

    为了保证有关User的ModelState的状态一致,大体上应该这样做:
    ● 对于通过部分视图MovieEntryEditor.cshtml渲染出来的新的表单元素,我们希望BeginCollectionItem()方法为我们生成新的GUID字符串。
    ● 对于验证不通过,重新由部分视图MovieEntryEditor.cshtml渲染的表单元素,我们希望还是用原先的GUID字符串,以保证ModelState状态一致。

    □ 对帮助类CollectionEditingHtmlExtensions进行改良

    把原先生成字符串的代码:

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

     

    改成:

    string collectionIndexFieldName = String.Format("{0}.Index", collectionName);//FavouriteMovies.Index
    string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);

     

    其中,GetCollectionItemIndex()需要根据某种条件排判断:

    1、如果渲染新的表单元素,就产生新的GUID字符串
    2、如果不是渲染新的表单元素,就使用原先的GUID字符串

    改良后的完整代码如下:

    using System;
    using System.Collections.Generic;
    using System.Web;
    using System.Web.Mvc;
     
    namespace VariableCollection.Extension
    {
        public static class CollectionEditingHtmlExtensions
        {
            public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName)
            {
                string collectionIndexFieldName = String.Format("{0}.Index", collectionName);//FavouriteMovies.Index
                string itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
     
                //比如,FavouriteMovies[6d85a95b-1dee-4175-bfae-73fad6a3763b]
                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) }, //name="FavouriteMovies.Index"
                    { "value", itemIndex },//value="6d85a95b-1dee-4175-bfae-73fad6a3763b"
                    { "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;
                 }
             }
     
            /// <summary>
            /// 以FavouriteMovies.Index为键,把Guid字符串存放在上下文中
            /// 如果是添加进入部分视图,就直接生成一个Guid字符串
            /// 如果是更新,为了保持和ModelState的一致,就遍历原先的Guid
            /// </summary>
            /// <param name="collectionIndexFieldName">FavouriteMovies.Index</param>
            /// <returns>返回Guid字符串</returns>
            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];//HttpContext.Current.Request[FavouriteMovies.Index]
                    if (!string.IsNullOrWhiteSpace(previousIndicesValues))
                    {
                        foreach (string index in previousIndicesValues.Split(','))
                        {
                            previousIndices.Enqueue(index);
                        }
                    }
                }
                return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString();
            }
        }
    }
     

     

    在GetCollectionItemIndex()方法中,首先把隐藏域的name属性值,比如这里的FavouriteMovies.Index作为key,把队列Queue<string>作为value,以键值对的形式保存在上下文的HttpContext.Current.Items中。然后,通过HttpContext.Current.Request[FavouriteMovies.Index]获取所有隐藏域的value值集合赋值给previousIndicesValues变量,并依次存放到队列Queue<string>中。最后,根据previousIndicesValues是否有集合元素,判断MovieEntryEditor.cshtml视图到底是渲染新的表单元素还是原先的表单元素。

    如果previousIndicesValues包含集合元素,就说明MovieEntryEditor.cshtml视图需要还原原先的表单元素,每次从队列Queue<string>中"挤出"第一个GUID字符串,直到没有。这样保证了User对应的ModelState状态的一致性。

    如果previousIndicesValues中没有集合元素,就说明MovieEntryEditor.cshtml视图需要渲染新的表单元素,就生成一个新的GUID字符串。

    把刚才注释掉的客户端异步验证js引用,再注释回来。

    验证不通过:
    4

    验证通过:
    5

    □ 最后

    通过本篇的方法:

    <input type="hidden" name="FavouriteMovies.Index" value="m"/>
    <input name="FavouriteMovies[m].Title" type="text" value="" />
    <input name="FavouriteMovies[m].Rating" type="text" value="" />

     

    <input type="hidden" name="FavouriteMovies.Index" value="n"/>
    <input name="FavouriteMovies[n].Title" type="text" value="" />
    <input name="FavouriteMovies[n].Rating" type="text" value="" />
    ......

    即使集合元素不是连续的,控制器也能接收到所有的集合元素,实现批量添加或更新。

    通过部分视图渲染每组集合元素,保证了客户端和服务端的验证功能。

    □ 参考资料
    ※  Editing Variable Length Reorderable Collections in ASP.NET MVC – Part 1: ASP.NET MVC Views

  • 相关阅读:
    datagrid表格宽度设置成100%,宽度却不是100%,反而很窄
    Spring MVC 和Struts2对比
    在不知道用户名密码的情况下登录oracle数据库
    IE浏览器的兼容性问题
    在spring mvc 中读取配置文件(porperties)的信息
    godaddy设置方法 控制面板
    easyui datagrid 动态改变大小
    15 个最佳 jQuery 翻书效果插件
    windows7 如何关闭开机启动讲述人
    存储过程代码审核
  • 原文地址:https://www.cnblogs.com/darrenji/p/3716436.html
Copyright © 2011-2022 走看看