每次使用新的guid索引时,如何在集合属性上获取验证消息?

时间:2016-01-22 14:42:49

标签: c# asp.net-mvc asp.net-mvc-4

在这个示例ASP.Net MVC 4程序中,我有一个用户填写有关赛马的详细信息。比赛有一个名字,以及所涉及的马匹列表。每匹马都有一个名字和年龄。

表单使用ajax和javascript来允许此人动态添加和删除马输入字段,然后在按下提交按钮时立即提交。

为了让这个过程变得简单,我使用html helper制作的Matt Lunn

public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string htmlFieldName = null) where TModel : class
{
    var items = expression.Compile()(html.ViewData.Model);
    var sb = new StringBuilder();

    if (String.IsNullOrEmpty(htmlFieldName))
    {
        var prefix = html.ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix;

        htmlFieldName = (prefix.Length > 0 ? (prefix + ".") : String.Empty) + ExpressionHelper.GetExpressionText(expression);
    }

    foreach (var item in items)
    {
        var dummy = new { Item = item };
        var guid = Guid.NewGuid().ToString();

        var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
        var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, expression.Parameters);

        sb.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldName, guid));
        sb.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
    }

    return new MvcHtmlString(sb.ToString());
}

虽然我不了解所有细节(请阅读博客文章),但我知道它将索引值更改为guids而不是顺序整数。这允许我删除列表中间的项目而无需重新计算索引。

以下是我的MCVE的其余代码

HomeController.cs

public class HomeController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        var model = new Race();

        //start with one already filled in
        model.HorsesInRace.Add(new Horse() { Name = "Scooby", Age = 10 });

        return View(model);
    }

    [HttpPost]
    public ActionResult Index(Race postedModel)
    {
        if (ModelState.IsValid)
            //model is valid, redirect to another page
            return RedirectToAction("ViewHorseListing");
        else
            //model is not valid, show the page again with validation errors
            return View(postedModel);
    }

    [HttpGet]
    public ActionResult AjaxMakeHorseEntry()
    {
        //new blank horse for ajax call
        var model = new List<Horse>() { new Horse() };
        return PartialView(model);
    }
}

Models.cs

public class Race
{
    public Race() { HorsesInRace = new List<Horse>(); }

    [Display(Name = "Race Name"), Required]
    public string RaceName { get; set; }

    [Display(Name = "Horses In Race")]
    public List<Horse> HorsesInRace { get; set; }
}

public class Horse
{
    [Display(Name = "Horse's Name"), Required]
    public string Name { get; set; }

    [Display(Name = "Horse's Age"), Required]
    public int Age { get; set; }
}

Index.cshtml

@model CollectionAjaxPosting.Models.Race
<h1>Race Details</h1>
@using (Html.BeginForm())
{
    @Html.ValidationSummary()
    <hr />
    <div>
        @Html.DisplayNameFor(x => x.RaceName)
        @Html.EditorFor(x => x.RaceName)
        @Html.ValidationMessageFor(x => x.RaceName)
    </div>
    <hr />
    <div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace)</div>
    <button id="btn-add-horse" type="button">Add New Horse</button>
    <input type="submit" value="Enter Horses" />
}

<script type="text/javascript">
    $(document).ready(function () {

        //add button logic
        $('#btn-add-horse').click(function () {
            $.ajax({
                url: '@Url.Action("AjaxMakeHorseEntry")',
                cache: false,
                method: 'GET',
                success: function (html) {
                    $('#horse-listing').append(html);
                }
            })
        });

        //delete-horse buttons
        $('#horse-listing').on('click', 'button.delete-horse', function () {
            var horseEntryToRemove = $(this).closest('div.horse');
            horseEntryToRemove.prev('input[type=hidden]').remove();
            horseEntryToRemove.remove();
        });

    });
</script>

查看/共享/ EditorTemplates / Horse.cshtml

@model CollectionAjaxPosting.Models.Horse

<div class="horse">
    <div>
        @Html.DisplayNameFor(x => x.Name)
        @Html.EditorFor(x => x.Name)
        @Html.ValidationMessageFor(x => x.Name)
    </div>
    <div>
        @Html.DisplayNameFor(x => x.Age)
        @Html.EditorFor(x => x.Age)
        @Html.ValidationMessageFor(x => x.Age)
    </div>
    <button type="button" class="delete-horse">Remove Horse</button>
    <hr />
</div>

查看/主页/ AjaxMakeHorseEntry.cshtml

@model IEnumerable<CollectionAjaxPosting.Models.Horse>

@Html.EditorForMany(x => x, "HorsesInRace")

数据流适用于此代码。一个人可以在页面上尽可能多地创建和删除马条目,并且在提交表单时,所有输入的值都将被赋予操作方法。

但是,如果用户未输入有关马条目的[Required]信息,则ModelState.IsValid将显示错误,但会再次显示该表单,但不会显示验证消息为马属性。验证错误确实显示在ValidationSummary列表中。

例如,如果Race Name与一个Horse's Name一起留空,则会显示前者的验证消息。后者将使用“field-validation-valid”类进行验证<span>

enter image description here

我非常确定这是因为EditorForMany方法在每次创建页面时为每个属性创建新的guid,因此验证消息无法与正确的字段匹配。

我该怎么做才能解决这个问题?我是否需要放弃guid索引创建,还是可以对EditorForMany方法进行更改以允许验证消息正确传递?

1 个答案:

答案 0 :(得分:7)

  

我非常确定这是因为EditorForMany方法在每次创建页面时为每个属性创建新的guid,因此验证消息无法与正确的字段匹配。

是的;这正是这里发生的事情。

要解决此问题,我们需要修改EditorForMany(),以便重新使用项目的GUID,而不是生成新项目。反过来,这意味着我们需要跟踪GUID分配给哪个项目,以便可以重复使用。

前者可以通过对EditorForMany()的内部修改来完成。后者要求我们:

  1. 向我们的模型添加属性,其中可以存储分配的GUID
  2. EditorForMany()添加一个参数,告诉帮助者哪个属性包含要重复使用的GUID(如果有)。
  3. 这使EditorForMany助手看起来像这样;

    public static class HtmlHelperExtensions
    {
        public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, string htmlFieldName = null) where TModel : class
        {
            htmlFieldName = htmlFieldName ?? ExpressionHelper.GetExpressionText(propertyExpression);
    
            var items = propertyExpression.Compile()(html.ViewData.Model);
            var htmlBuilder = new StringBuilder();
            var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
            Func<TValue, string> indexResolver = null;
    
            if (indexResolverExpression == null)
            {
                indexResolver = x => null;
            }
            else
            {
                indexResolver = indexResolverExpression.Compile();
            }
    
            foreach (var item in items)
            {
                var dummy = new { Item = item };
                var guid = indexResolver(item);
                var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
                var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);
    
                if (String.IsNullOrEmpty(guid))
                {
                    guid = Guid.NewGuid().ToString();
                }
                else
                {
                    guid = html.AttributeEncode(guid);
                }
    
                htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid));
    
                if (indexResolverExpression != null)
                {
                    htmlBuilder.Append(String.Format(@"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression)));
                }
    
                htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
            }
    
            return new MvcHtmlString(htmlBuilder.ToString());
        }
    }
    

    然后我们还需要更改模型,添加存储GUID的属性;

    public class Race
    {
        public Race() { HorsesInRace = new List<Horse>(); }
    
        [Display(Name = "Race Name"), Required]
        public string RaceName { get; set; }
    
        [Display(Name = "Horses In Race")]
        public List<Horse> HorsesInRace { get; set; }
    }
    
    public class Horse
    {
        [Display(Name = "Horse's Name"), Required]
        public string Name { get; set; }
    
        [Display(Name = "Horse's Age"), Required]
        public int Age { get; set; }
    
        // Note the addition of Index here.
        public string Index { get; set; }
    }    
    

    ...最后,更改EditorForMany()的使用情况以使用新签名;

    Index.cshtml ;

    <div id="horse-listing">@Html.EditorForMany(x => x.HorsesInRace, x => x.Index)</div>
    

    AjaxMakeHorseEntry.cshtml ;

    @Html.EditorForMany(x => x, x => x.Index, "HorsesInRace")
    

    ...然后应该显示验证消息。

    另外,我建议不要将htmlFieldName参数用于EditorForMany,而是将控制器操作更改为; 功能

    [HttpGet]
    public ActionResult AjaxMakeHorseEntry()
    {
        var model = new Race();
    
        model.HorsesInRace.Add(new Horse());
        return PartialView(model);
    }
    

    ...然后你的 AjaxMakeHorseEntry.cshtml 视图就是这样;

    @model Models.Race
    
    @Html.EditorForMany(x => x.HorsesInRace, x => x.Index)
    

    否则,在嵌套name的使用时,生成的EditorForMany()属性会中断。

    因为这个原因,我要更新博客帖子以使用上面版本的EditorForMany(),而不是接受htmlFieldName参数。