如何使用MVC HTML编辑器模板生成非顺序前缀集合索引?

时间:2012-06-29 18:47:47

标签: c# asp.net-mvc editorfor asp.net-mvc-templates

以下代码已被删除很多,但基本上我想要达到的目的如下:

我希望能够编辑问题及其包含的答案选择,同时能够从页面动态添加/删除问题/答案选择。理想情况下,我的项目的HtmlFieldPrefix将是非顺序的,但Html.EditorFor()使用顺序索引。

我有一个问题ViewModel,它包含一个IEnumerable的答案选择:

public class QuestionViewModel
{
    public int QuestionId { get; set; }
    public IEnumerable<AnswerChoiceViewModel> AnswerChoices { get; set; }
}

在我的问题部分视图(Question.ascx)中,我有:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.QuestionViewModel>" %>

<%=Html.HiddenFor(m => m.QuestionId)%>
<%=Html.EditorFor(m => m.AnswerChoices) %>

答案选择编辑器模板(AnswerChoiceViewModel.ascx):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.AnswerChoiceViewModel>" %>

<%=Html.HiddenFor(m => m.AnswerChoiceId)%>
<%=Html.TextBoxFor(m => m.Name)%>

当我渲染Question.ascx时,输出将如下所示:

<input type="hidden" id="QuestionId" value="1" />
<input type="hidden" id="Question.AnswerChoices[0].AnswerChoiceId" value="1" />
<input type="hidden" id="Question.AnswerChoices[0].Name" value="Answer Choice 1" />

<input type="hidden" id="QuestionId" value="2" />
<input type="hidden" id="Question.AnswerChoices[1].AnswerChoiceId" value="2" />
<input type="hidden" id="Question.AnswerChoices[1].Name" value="Answer Choice 2" />

我想知道的是我如何为自定义GUID索引提供编辑器,以便页面呈现如下:

<input type="hidden" id="QuestionId" value="1" />
<input type="hidden" id="Question.AnswerChoices[e1424d5e-5585-413c-a1b0-595f39747876].AnswerChoiceId" value="1" />
<input type="hidden" id="Question.AnswerChoices[e1424d5e-5585-413c-a1b0-595f39747876].Name" value="Answer Choice 1" />

<input type="hidden" id="QuestionId" value="2" />
<input type="hidden" id="Question.AnswerChoices[633db1c3-f1e6-470b-9c7f-c138f2d9fa71].AnswerChoiceId" value="2" />
<input type="hidden" id="Question.AnswerChoices[633db1c3-f1e6-470b-9c7f-c138f2d9fa71].Name" value="Answer Choice 2" />

我已经编写了一个辅助方法,它将获取当前上下文的前缀索引并将其存储在隐藏的“.Index”字段中,以便可以正确绑定非顺序索引。只是想知道EditorFor如何分配索引,以便我可以覆盖它(或任何其他工作解决方案)。

5 个答案:

答案 0 :(得分:2)

前段时间我解决了这个问题,并从S. Sanderson(Knockoutjs的创建者)发了一篇文章,在那里他描述并解决了类似的问题。我使用了部分代码并尝试修改它以满足我的需要。我将下面的代码放在某个类中(exapmle:Helpers.cs),在web.config中添加命名空间。

    #region CollectionItem helper
    private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

    public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
    {
        var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
        string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

        // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
        html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, itemIndex));

        return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
    }

    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
    {
        return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
    {
        // We need to use the same sequence of IDs following a server-side validation failure,  
        // otherwise the framework won't render the validation error messages next to each item.
        string key = idsToReuseKey + collectionName;
        var queue = (Queue<string>)httpContext.Items[key];
        if (queue == null)
        {
            httpContext.Items[key] = queue = new Queue<string>();
            var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
            if (!string.IsNullOrEmpty(previouslyUsedIds))
                foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                    queue.Enqueue(previouslyUsedId);
        }
        return queue;
    }

    private class HtmlFieldPrefixScope : IDisposable
    {
        private readonly TemplateInfo templateInfo;
        private readonly string previousHtmlFieldPrefix;

        public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
        {
            this.templateInfo = templateInfo;

            previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
            templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
        }

        public void Dispose()
        {
            templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
        }
    }

    #endregion

你可以拥有EditorTemplate或者像这样

@using (Html.BeginCollectionItem("AnswerChoices"))
{
@Html.HiddenFor(m => m.AnswerChoiceId)
@Html.TextBoxFor(m => m.Name)
}

并通过列表呈现模板(部分)进行枚举。

答案 1 :(得分:2)

我花了很长时间来解决这个问题。每个人都在努力工作。秘诀就是这四行代码:

&#13;
&#13;
        @{
            var index = Guid.NewGuid();
            var prefix = Regex.Match(ViewData.TemplateInfo.HtmlFieldPrefix, @"^(.+)\[\d+\]$").Groups[1].Captures[0].Value;
            //TODO add a ton of error checking and pull this out into a reusable class!!!!
            ViewData.TemplateInfo.HtmlFieldPrefix = prefix + "[" + index + "]";
        }
        <input type="hidden" name="@(prefix).Index" value="@index"/>
&#13;
&#13;
&#13;

现在,这是做什么的?我们得到一个新的guid,这是我们新的索引来替换自动分配的整数。接下来我们得到默认字段前缀,我们剥离了我们不想要的int索引。在确认我们已经创建了一些技术债务之后,我们然后更新了viewdata,以便所有的editorfor调用现在都使用它作为新的前缀。最后,我们添加一个返回到模型绑定器的输入,指定它应该用于将这些字段绑定在一起的索引。

这种魔法需要在哪里发生?在您的编辑器模板中:/Views/Shared/EditorTemplates/Phone.cshtml

&#13;
&#13;
@using TestMVC.Models
@using System.Text.RegularExpressions
@model Phone
    <div class="form-horizontal">
        <hr />
        @{
            var index = Guid.NewGuid();
            var prefix = Regex.Match(ViewData.TemplateInfo.HtmlFieldPrefix, @"^(.+)\[\d+\]$").Groups[1].Captures[0].Value;
            //TODO add a ton of error checking and pull this out into a reusable class!!!!
            ViewData.TemplateInfo.HtmlFieldPrefix = prefix + "[" + index + "]";
        }
        <input type="hidden" name="@(prefix).Index" value="@index"/>
        <div class="form-group">
            @Html.LabelFor(model => model.Number, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Number, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Number, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.IsEnabled, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                <div class="checkbox">
                    @Html.EditorFor(model => model.IsEnabled)
                    @Html.ValidationMessageFor(model => model.IsEnabled, "", new { @class = "text-danger" })
                </div>
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Details, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextAreaFor(model => model.Details, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Details, "", new { @class = "text-danger" })
            </div>
        </div>
    </div>
&#13;
&#13;
&#13;

EditorTemplate?什么?!怎么样?!只需使用文件名的对象名称将其放在上面提到的目录中。让MVC大会发挥其神奇作用。在主视图中,只需为该IEnumerable属性添加编辑器:

&#13;
&#13;
<div class="form-group">
@Html.LabelFor(model => model.Phones, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
    @Html.EditorFor(model => model.Phones, new { htmlAttributes = new { @class = "form-control" } })
</div>
</div>
&#13;
&#13;
&#13;

现在,回到你的控制器中,确保你更新你的方法签名以接受那个可相关的(绑定包含电话):

        [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "ContactId,FirstName,LastName,Phones")] Contact contact)
    {
        if (ModelState.IsValid)
        {

            db.Contacts.Add(contact);
            db.SaveChanges();
            //TODO need to update this to save phone numbers
            return RedirectToAction("Index");
        }

        return View(contact);
    }

如何在页面上添加和删除它们?添加一些按钮,绑定一些JavaScript,向控制器添加一个方法,该方法将返回该模型的视图。 Ajax回来抓住它并将其插入页面。我会让你弄清楚这些细节,因为此时此刻正忙着工作。

答案 2 :(得分:0)

Html.EditorFor不是所谓的Html辅助方法,它使input具有所有适当的属性。

我想到的唯一解决方案是编写自己的解决方案。它必须非常简单 - 5-10行。看一下这个Creating Custom Html Helpers Mvc

答案 3 :(得分:0)

史蒂夫桑德森已经提供了simple implementation可能会做你正在寻找的东西。我最近开始自己使用它;它并不完美,但确实有效。不幸的是,你必须做一些神奇的穿线来使用他的BeginCollectionItem方法;我正在努力解决这个问题。

答案 4 :(得分:0)

另一种选择是覆盖id属性,如下所示:

@Html.TextBoxFor(m => m.Name, new { id = @guid })