2

下面的代码已经被精简了很多,但基本上我想要实现的目标如下:

我希望能够编辑问题及其包含的答案选择,同时能够从页面中动态添加/删除问题/答案选择。理想情况下,我的项目的 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" />

我想知道的是如何提供 EditorFor 自定义 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 如何分配索引,以便我可以覆盖它(或任何其他工作解决方案)。

4

5 回答 5

2

之前我解决了这个问题并遇到了 S. Sanderson(Knockoutjs 的创建者)的一篇文章,他描述并解决了类似的问题。我使用了他的部分代码并尝试对其进行修改以满足我的需要。我将下面的代码放在某个类中(例如: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)
}

并枚举您的列表渲染模板(部分)。

于 2012-06-29T19:13:50.723 回答
2

我花了更长的时间才弄清楚这一点。每个人都在努力做到这一点。秘诀就是这四行代码:

        @{
            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"/>

现在,这是在做什么?我们得到一个新的 guid,这是我们用来替换自动分配的整数的新索引。接下来,我们获取默认字段前缀,并去掉我们不想要的 int 索引。在确认我们已经产生了一些技术债务之后,我们会更新视图数据,以便所有 editorfor 调用现在都使用它作为新前缀。最后,我们添加一个输入,该输入被回发到模型绑定器,指定它应该用来将这些字段绑定在一起的索引。

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

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

编辑器模板?什么?!如何?!只需使用文件名的对象名称将其放在上面提到的目录中。让 MVC 约定发挥它的魔力。从您的主视图中,只需为该 IEnumerable 属性添加编辑器:

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

现在,回到您的控制器中,确保更新您的方法签名以接受该 ienumerable(绑定包含电话):

        [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 回来抓取它并将其插入到页面中。我会让你解决这些细节,因为现在工作很忙。

于 2016-10-14T17:59:48.873 回答
0

Html.EditorFor is nothing else as a so called Html helper method, which renders input with all apropriate attributes.

The only solution which comes me to mind is to write the own one. It must be pretty simple - 5-10 lines ling. Take a look at this Creating Custom Html Helpers Mvc.

于 2012-06-29T19:07:46.813 回答
0

Steve Sanderson has provided a simple implementation that may do what you're looking for. I recently started using it myself; it is not perfect, but it does work. You have to do a little magic-stringing to use his BeginCollectionItem method, unfortunately; I'm trying to workaround that myself.

于 2012-06-29T19:08:01.470 回答
0

另一种选择是像这样覆盖 id 属性:

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

于 2012-06-29T19:14:19.787 回答