ASP.NET MVC部分视图:输入名称前缀

126
假设我有以下的 ViewModel:
public class AnotherViewModel
{
   public string Name { get; set; }
}
public class MyViewModel
{
   public string Name { get; set; }
   public AnotherViewModel Child { get; set; }
   public AnotherViewModel Child2 { get; set; }
}

在视图中,我可以使用以下代码呈现一个局部内容:

<% Html.RenderPartial("AnotherViewModelControl", Model.Child) %>

在我要做的部分中

<%= Html.TextBox("Name", Model.Name) %>
or
<%= Html.TextBoxFor(x => x.Name) %>

然而,问题在于两者都会呈现为name="Name",而我需要使用name="Child.Name"才能使模型绑定器正常工作。或者,当我使用相同的局部视图呈现第二个属性时,需要使用name="Child2.Name"。

我该如何使我的局部视图自动识别所需的前缀?我可以将其作为参数传递,但这太麻烦了。当我想要递归地呈现它时,情况变得更糟。是否有一种方法可以使用前缀呈现局部视图,或者更好的是,自动识别调用lambda表达式的方式呈现局部视图,以便

<% Html.RenderPartial("AnotherViewModelControl", Model.Child) %>

会自动将正确的“Child.”前缀添加到生成的名称/ID字符串中吗?
我可以接受任何解决方案,包括第三方视图引擎和库——我实际上使用了Spark View Engine(我使用它的宏“解决”问题)和MvcContrib,但在那里没有找到解决方案。XForms、InputBuilder、MVC v2——任何提供此功能的工具/见解都很棒。
目前,我考虑自己编写代码,但这似乎是浪费时间,我无法相信这种琐碎的东西还没有被实现。
可能存在许多手动解决方案,欢迎所有这些解决方案。例如,我可以强制我的局部视图基于IPartialViewModel< T > { public string Prefix; T Model; },但我更喜欢一些现有/已批准的解决方案。
更新:有一个类似的问题没有答案在这里
11个回答

113

你可以通过以下方式扩展Html助手类:

using System.Web.Mvc.Html


 public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, string partialViewName)
    {
        string name = ExpressionHelper.GetExpressionText(expression);
        object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
        var viewData = new ViewDataDictionary(helper.ViewData)
        {
            TemplateInfo = new System.Web.Mvc.TemplateInfo
            {
                HtmlFieldPrefix = name
            }
        };

        return helper.Partial(partialViewName, model, viewData);

    }

然后在您的视图中像这样简单地使用它:

<%= Html.PartialFor(model => model.Child, "_AnotherViewModelControl") %>

然后你会看到一切都没问题!


18
对于嵌套的部分渲染,这种方法是不正确的。你需要将新前缀追加到来自helper.ViewData.TemplateInfo.HtmlFieldPrefix的旧前缀中,格式为{oldprefix}.{newprefix} - Ivan Zlatev
@Mahmoud,你的代码很好用,但是我发现在执行Partial中的代码时,ViewData/ViewBag为空。我发现HtmlHelper<TModel>类型的helper有一个新属性ViewData,它隐藏了基本模型的ViewData。因此,我用new ViewDataDictionary(((HtmlHelper)helper).ViewData)替换了new ViewDataDictionary(helper.ViewData)。你觉得这样有什么问题吗? - Pat Newell
2
@IvanZlatev 是正确的。在设置名称之前,您应该执行string oldPrefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix; if (oldPrefix != "") name = oldPrefix + "." + name; - kennethc
2
解决嵌套模板的简便方法是使用HtmlFieldPrefix = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name)。 - Aman Mahajan
错误,提交PR的正确位置是https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/Html/PartialExtensions.cs。 - usr-local-ΕΨΗΕΛΩΝ
显示剩余3条评论

101

4
感谢提供链接,这绝对是列出的最佳选择。 - B Z
34
我很晚才参加这个派对,但如果你采用这种方法,你应该使用带有当前ViewDataViewDataDictionary构造函数,否则你会失去模型状态错误、验证数据等。 - bhamlin
10
在Bhamlin的评论基础上进行补充。您还可以传递嵌套前缀,例如(对不起,这是VB.NET示例):Html.RenderPartial("AnotherViewModelControl", Model.PropX, New ViewDataDictionary(ViewData) With {.TemplateInfo = New TemplateInfo() With {.HtmlFieldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix & ".PropX"}})。 - hubson bropa
2
很棒的答案,+1。也许值得编辑一下,以考虑@bhamlin的评论。 - ken2k
2
请注意,如果您需要从控制器返回这部分内容,您也需要在控制器中设置相应的前缀。https://dev59.com/cWw15IYBdhLWcg3wbbNx - The Muffin Man
显示剩余3条评论

12

根据Mahmoud Moravej的回答和Ivan Zlatev的评论,我的回答如下。

    public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, System.Linq.Expressions.Expression<Func<TModel, TProperty>> expression, string partialViewName)
    {
            string name = ExpressionHelper.GetExpressionText(expression);
            object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
            StringBuilder htmlFieldPrefix = new StringBuilder();
            if (helper.ViewData.TemplateInfo.HtmlFieldPrefix != "")
            {
                htmlFieldPrefix.Append(helper.ViewData.TemplateInfo.HtmlFieldPrefix);
                htmlFieldPrefix.Append(name == "" ? "" : "." + name);
            }
            else
                htmlFieldPrefix.Append(name);

            var viewData = new ViewDataDictionary(helper.ViewData)
            {
                TemplateInfo = new System.Web.Mvc.TemplateInfo
                {
                    HtmlFieldPrefix = htmlFieldPrefix.ToString()
                }
            };

        return helper.Partial(partialViewName, model, viewData);
    }

编辑: Mohamoud的答案对于嵌套的部分渲染是不正确的。只有在必要时,您才需要将新前缀附加到旧前缀上。这在最新的答案中并不清楚(:


6
如果您能详细说明上述改进如何优于现有答案,将对他人有所帮助。 - Leigh
2
由于手指粗大的复制和粘贴错误,这个在ASP.NET MVC 5中完美运行。+1。 - Greg Burghardt

10

使用MVC2可以实现此功能。

以下是强类型视图:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcLearner.Models.Person>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Create</h2>

    <% using (Html.BeginForm()) { %>
        <%= Html.LabelFor(person => person.Name) %><br />
        <%= Html.EditorFor(person => person.Name) %><br />
        <%= Html.LabelFor(person => person.Age) %><br />
        <%= Html.EditorFor(person => person.Age) %><br />
        <% foreach (String FavoriteFoods in Model.FavoriteFoods) { %>
            <%= Html.LabelFor(food => FavoriteFoods) %><br />
            <%= Html.EditorFor(food => FavoriteFoods)%><br />
        <% } %>
        <%= Html.EditorFor(person => person.Birthday, "TwoPart") %>
        <input type="submit" value="Submit" />
    <% } %>

</asp:Content>

这是子类的强类型视图(必须存储在视图目录的名为EditorTemplates的子文件夹中):

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

<%= Html.LabelFor(birthday => birthday.Day) %><br />
<%= Html.EditorFor(birthday => birthday.Day) %><br />

<%= Html.LabelFor(birthday => birthday.Month) %><br />
<%= Html.EditorFor(birthday => birthday.Month) %><br />

以下是控制器代码:

public class PersonController : Controller
{
    //
    // GET: /Person/
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Index()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult Create()
    {
        Person person = new Person();
        person.FavoriteFoods.Add("Sushi");
        return View(person);
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Person person)
    {
        return View(person);
    }
}

以下是自定义类:

public class Person
{
    public String Name { get; set; }
    public Int32 Age { get; set; }
    public List<String> FavoriteFoods { get; set; }
    public TwoPart Birthday { get; set; }

    public Person()
    {
        this.FavoriteFoods = new List<String>();
        this.Birthday = new TwoPart();
    }
}

public class TwoPart
{
    public Int32 Day { get; set; }
    public Int32 Month { get; set; }
}

输出的源代码:

<form action="/Person/Create" method="post"><label for="Name">Name</label><br /> 
    <input class="text-box single-line" id="Name" name="Name" type="text" value="" /><br /> 
    <label for="Age">Age</label><br /> 
    <input class="text-box single-line" id="Age" name="Age" type="text" value="0" /><br /> 
    <label for="FavoriteFoods">FavoriteFoods</label><br /> 
    <input class="text-box single-line" id="FavoriteFoods" name="FavoriteFoods" type="text" value="Sushi" /><br /> 
    <label for="Birthday_Day">Day</label><br /> 
    <input class="text-box single-line" id="Birthday_Day" name="Birthday.Day" type="text" value="0" /><br /> 

    <label for="Birthday_Month">Month</label><br /> 
    <input class="text-box single-line" id="Birthday_Month" name="Birthday.Month" type="text" value="0" /><br /> 
    <input type="submit" value="Submit" /> 
</form>
现在这已经完成。在“创建帖子”控制器操作中设置断点以进行验证。但是不要在列表中使用它,因为它不会起作用。有关在IEnumerable上使用EditorTemplates的更多信息,请参见我的问题。

是的,看起来是这样。问题在于列表不起作用(而嵌套视图模型通常在列表中),而且这是v2...我还没有准备好在生产中使用。但仍然很好知道它将是我需要的东西...当它到来时(所以+1)。 - queen3
我前几天也看到了这个链接:http://www.matthidinger.com/archive/2009/08/15/creating-a-html.displayformany-helper-for-mvc-2.aspx,你可以尝试一下如何将它扩展到编辑器上(虽然还是在MVC2中)。我花了几分钟时间研究,但由于我对表达式还不够熟悉,所以一直遇到问题。也许你能比我做得更好。 - Nick Larsen
1
一个有用的链接,谢谢。我认为在这里让EditorFor工作是不可能的,因为它不会生成[0]索引(我打赌它根本不支持)。一种解决方案是将EditorFor()输出呈现为字符串,并手动调整输出(附加所需的前缀)。虽然是个肮脏的hack,但也是一种方法。嗯!我可以为Html.Helper().UsePrefix()编写扩展方法,它只会将name="x"替换为name="prefix.x"...在MVC v1中仍需要一些工作,但并不多。还有Html.WithPrefix("prefix").RenderPartial(),可以成对使用。 - queen3
推荐使用Html.EditorFor方法来呈现子表单,这正是编辑器模板的预期用途。这应该就是答案。 - Greg Burghardt

10

这是一个旧问题,但对于任何需要解决此问题的人,请考虑使用EditorFor。如https://dev59.com/i10a5IYBdhLWcg3w6sVk#29809907中的评论建议的那样。若要从部分视图转移到编辑器模板,请按照以下步骤进行。

  1. 确认你的部分视图绑定到 ComplexType

  2. 将你的部分视图移动到当前视图文件夹的子文件夹EditorTemplates中,或者移动到Shared文件夹中。现在,它是一个编辑器模板了。

  3. @Html.Partial("_PartialViewName", Model.ComplexType) 改为 @Html.EditorFor(m => m.ComplexType, "_EditorTemplateName")。如果它是复合类型的唯一模板,则编辑器模板是可选的。

HTML输入元素的名称将自动命名为ComplexType.Fieldname


9

PartailFor 是针对asp.net Core 2的,如果有人需要的话。

    public static ModelExplorer GetModelExplorer<TModel, TResult>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TResult>> expression)
    {
        if (expression == null)
            throw new ArgumentNullException(nameof(expression));
        return ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
    }

    public static IHtmlContent PartialFor<TModel, TResult>(this IHtmlHelper<TModel> helper, Expression<Func<TModel, TResult>> expression, string partialViewName, string prefix = "")
    {
        var modelExplorer = helper.GetModelExplorer(expression);
        var viewData = new ViewDataDictionary(helper.ViewData);
        viewData.TemplateInfo.HtmlFieldPrefix += prefix;
        return helper.Partial(partialViewName, modelExplorer.Model, viewData);
    }

5

如此说明:https://dev59.com/i10a5IYBdhLWcg3w6sVk#58943378 - 对于 ASP.NET Core - 您可以使用部分标签助手。

<partial name="AnotherViewModelControl" for="Child" />
<partial name="AnotherViewModelControl" for="Child2" />

它生成所有必需的名称前缀。

1
不要使用<partial model="Model.blah"...,因为它将无法生成正确的前缀。 - Jess

3
我也遇到了这个问题,经过很多痛苦后,我发现重新设计我的接口是更容易的,这样我就不需要回传嵌套模型对象了。这迫使我改变了我的接口工作流程:虽然现在我需要让用户用两个步骤来完成我梦想中的一个步骤,但新方法的可用性和代码可维护性对我来说更有价值。
希望这能帮到你。

2
您可以为RenderPartial添加一个帮助程序,它会获取前缀并将其放入ViewData中。
    public static void RenderPartial(this HtmlHelper helper,string partialViewName, object model, string prefix)
    {
        helper.ViewData["__prefix"] = prefix;
        helper.RenderPartial(partialViewName, model);
    }

然后是另一个助手,它将连接 ViewData 的值。
    public static void GetName(this HtmlHelper helper, string name)
    {
        return string.Concat(helper.ViewData["__prefix"], name);
    }

and so in the view ...

<% Html.RenderPartial("AnotherViewModelControl", Model.Child, "Child.") %>

in the partial ...

<%= Html.TextBox(Html.GetName("Name"), Model.Name) %>

是的,这就是我在https://dev59.com/IXI_5IYBdhLWcg3wMf9_#1489017评论中所描述的。更好的解决方案是使用RenderPartial,它也可以采用lambda表达式,这很容易实现。不过,还是感谢您提供的代码,我想这是最简单和经典的方法。 - queen3
1
为什么不使用 helper.ViewData.TemplateInfo.HtmlFieldPrefix = prefix 呢?我在我的帮助程序中使用它,似乎运行良好。 - ajbeaven

1

就像你一样,我会在我的ViewModels中添加Prefix属性(一个字符串),并将其附加到我的模型绑定输入名称之前。(YAGNI防止以下情况)

更优雅的解决方案可能是一个基本的视图模型,它具有此属性和一些HtmlHelpers,检查视图模型是否派生自此基类,如果是,则将前缀附加到输入名称。

希望这可以帮助到你,

Dan


1
哎呀,太糟糕了。我可以想象许多优雅的解决方案,使用自定义的HtmlHelpers检查属性、属性、自定义渲染等等...但是例如如果我使用MvcContrib FluentHtml,我需要重写所有这些来支持我的hack吗?奇怪的是,没有人谈论它,好像每个人都只使用平面单层的ViewModels... - queen3
实际上,使用框架一段时间并努力编写漂亮干净的视图代码后,我认为分层的 ViewModel 是不可避免的。例如,在订单 ViewModel 中的购物篮 ViewModel。 - Daniel Elliott

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接