以下代码已被删除很多,但基本上我想要达到的目的如下:
我希望能够编辑问题及其包含的答案选择,同时能够从页面动态添加/删除问题/答案选择。理想情况下,我的项目的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如何分配索引,以便我可以覆盖它(或任何其他工作解决方案)。
答案 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)
我花了很长时间来解决这个问题。每个人都在努力工作。秘诀就是这四行代码:
@{
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;
现在,这是做什么的?我们得到一个新的guid,这是我们新的索引来替换自动分配的整数。接下来我们得到默认字段前缀,我们剥离了我们不想要的int索引。在确认我们已经创建了一些技术债务之后,我们然后更新了viewdata,以便所有的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>
&#13;
EditorTemplate?什么?!怎么样?!只需使用文件名的对象名称将其放在上面提到的目录中。让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>
&#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 })