Asp.net mvc 如何在ASP.NET MVC 4 with Razor中为具有列表属性的对象生成编辑表单

Asp.net mvc 如何在ASP.NET MVC 4 with Razor中为具有列表属性的对象生成编辑表单,asp.net-mvc,Asp.net Mvc,我有一个MVC应用程序的编辑页面,使用Razor 我有一个这样的模型: public class MyModelObject { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public List<MyOtherModelObject> OtherModelObjects { get; se

我有一个MVC应用程序的编辑页面,使用Razor

我有一个这样的模型:

public class MyModelObject
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public List<MyOtherModelObject> OtherModelObjects { get; set; }
}
我正在为MyModelObject创建编辑页面。我需要一种方法在MyModelObject的编辑页面上为表单添加空间,以便用户创建/添加尽可能多的MyThermodeObject实例到其他ModelObject列表中

我认为用户可以点击一个按钮,对另一个操作执行ajax操作,该操作返回表单元素的部分视图(没有表单标记,因为这是我编辑页面上表单的一部分)。当用户添加了他们想要的所有MyThermodelObject并填写了数据后,他们应该能够保存对现有MyModelObject的编辑,这将HttpPost发布到编辑操作中,并且希望所有MyThermodelObject都在正确的列表中

我还需要用户能够重新订购的项目,一旦他们添加了他们

有人知道怎么做吗?是否已实施此解决方案的示例项目或在线示例演练?

这包含一个分步指南,说明如何实现此目标


更新:

根据评论部分的要求,我正在逐步说明如何使上述文章适应您的场景

型号:

public class MyOtherModelObject
{
    public string Name { get; set; }
    public string Description { get; set; }
}

public class MyModelObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List<MyOtherModelObject> OtherModelObjects { get; set; }
}
视图(
~/Views/Home/Index.cshtml
):

脚本:

$('#addItem').click(function () {
    $.ajax({
        url: this.href,
        cache: false,
        success: function (html) {
            $('#editorRows').append(html);
        }
    });
    return false;
});

$('a.deleteRow').live('click', function () {
    $(this).parents('div.editorRow:first').remove();
    return false;
});

备注:
BeginCollectionItem
custom helper取自我链接的同一篇文章,但为了完整起见,我在这里提供它:

public static class HtmlPrefixScopeExtensions
{
    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, html.Encode(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;
        }
    }
}
公共静态类HtmlPrefixScopeExtensions
{
私有常量字符串idsToReuseKey=“\uHTMLPrefixScopeExtensions\uIDStoreUse”;
公共静态IDisposable BeginCollectionItem(此HtmlHelper html,字符串集合名称)
{
var idsToReuse=GetIdsToReuse(html.ViewContext.HttpContext,collectionName);
string itemIndex=idsToReuse.Count>0?idsToReuse.Dequeue():Guid.NewGuid().ToString();
//需要autocomplete=“off”来解决一个非常恼人的Chrome行为,即在用户单击“后退”后重用旧值,这会导致xyz.index和xyz[…]值不同步。
html.ViewContext.Writer.WriteLine(string.Format(“”,collectionName,html.Encode(itemIndex));
返回BeginHtmlFieldPrefixScope(html,string.Format(“{0}[{1}]”,collectionName,itemIndex));
}
公共静态IDisposable BeginHtmlFieldPrefixScope(此HtmlHelper html,字符串htmlFieldPrefix)
{
返回新的HtmlFieldPrefixScope(html.ViewData.TemplateInfo,htmlFieldPrefix);
}
私有静态队列GetIdsToReuse(HttpContextBase httpContext,string collectionName)
{
//在服务器端验证失败后,我们需要使用相同的ID序列,
//否则,框架将不会在每个项旁边呈现验证错误消息。
string key=idsToReuseKey+collectionName;
var queue=(queue)httpContext.Items[key];
if(队列==null)
{
httpContext.Items[key]=队列=新队列();
var previouslyUsedIds=httpContext.Request[collectionName+“.index”];
如果(!string.IsNullOrEmpty(以前使用的DIDS))
foreach(previouslyUsedIds.Split(',')中的字符串previouslyUsedId)
queue.Enqueue(以前使用的did);
}
返回队列;
}
私有类HtmlFieldPrefixScope:IDisposable
{
私有只读TemplateInfo TemplateInfo;
私有只读字符串previousHtmlFieldPrefix;
公共HtmlFieldPrefixScope(TemplateInfo TemplateInfo,字符串htmlFieldPrefix)
{
this.templateInfo=templateInfo;
previousHtmlFieldPrefix=templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix=HtmlFieldPrefix;
}
公共空间处置()
{
templateInfo.HtmlFieldPrefix=以前的HtmlFieldPrefix;
}
}
}

我能够从这篇博文中吸取教训,并将其应用到我的案例中,其中一个ModelObject具有多个属性,其中许多属性是列表属性


我对他的脚本进行了适当的修改,以便在一个模型中处理多个列表,并将尽快在博客上发布我的解决方案。它肯定要等到我现在的冲刺之后。完成后我会发布链接。

您可能会第一眼看到(尽管这是Razor语法,也可能是MVC4语法是否能给出一个想法)。这看起来对列表有效,但对作为另一个对象的一部分的列表有效吗?它能绑定模型吗?不完全有效。它讨论了如何创建可变长度的对象列表,但没有讨论如何创建可变长度的对象列表,这些对象是另一个对象的一部分。@DaveH,但这很容易实现。您所要做的就是引入一个新的视图模型,该模型具有您试图编辑的列表的
IEnumerable
属性,就像您在问题中对
MyModelObject
所做的那样。您说得简单吗?所有这些将如何绑定回父对象MyModelObject。答案还有很大一部分需要解决,这可能涉及重新编写MVC的默认ModelBind行为。这样做是否存在安全问题。我正在研究这个示例,并在其他几个论坛帖子上找到了它,用于解决单个对象的列表,但还没有看到我给出的示例有效地解决了问题。好的,显然我必须一步一步地为您编写代码。请稍等。请看我的最新答案。正如您所看到的,代码与本文中显示的代码非常接近。没有必要重写模型绑定器:-)您只需插入模型即可。下次请多做点努力。
@model MyModelObject

@using(Html.BeginForm())
{
    @Html.HiddenFor(x => x.Id)
    <div>
        @Html.LabelFor(x => x.Name)
        @Html.EditorFor(x => x.Name)
    </div>
    <div>
        @Html.LabelFor(x => x.Description)
        @Html.TextBoxFor(x => x.Description)
    </div>
    <hr/>
    <div id="editorRows">
        @foreach (var item in Model.OtherModelObjects)
        {
            @Html.Partial("EditorRow", item);
        }
    </div>
    @Html.ActionLink("Add another...", "BlankEditorRow", null, new { id = "addItem" })

    <input type="submit" value="Finished" />
}
@model MyOtherModelObject

<div class="editorRow">
    @using (Html.BeginCollectionItem("OtherModelObjects"))
    {
        <div>
            @Html.LabelFor(x => x.Name)
            @Html.EditorFor(x => x.Name)
        </div>
        <div>
            @Html.LabelFor(x => x.Description)
            @Html.EditorFor(x => x.Description)
        </div>
        <a href="#" class="deleteRow">delete</a>
    }
</div>
$('#addItem').click(function () {
    $.ajax({
        url: this.href,
        cache: false,
        success: function (html) {
            $('#editorRows').append(html);
        }
    });
    return false;
});

$('a.deleteRow').live('click', function () {
    $(this).parents('div.editorRow:first').remove();
    return false;
});
public static class HtmlPrefixScopeExtensions
{
    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, html.Encode(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;
        }
    }
}