[英]Correct, idiomatic way to use custom editor templates with IEnumerable models in ASP.NET MVC
快速刷新。
什么時候:
IEnumerable<T>
類型的屬性Html.EditorFor()
T
類型的編輯器模板然后 MVC 引擎將自動為可枚舉序列中的每個項目調用編輯器模板,生成結果列表。
例如,當有一個帶有屬性Lines
的模型類Order
時:
public class Order
{
public IEnumerable<OrderLine> Lines { get; set; }
}
public class OrderLine
{
public string Prop1 { get; set; }
public int Prop2 { get; set; }
}
並且有一個視圖 Views/Shared/EditorTemplates/OrderLine.cshtml:
@model TestEditorFor.Models.OrderLine
@Html.EditorFor(m => m.Prop1)
@Html.EditorFor(m => m.Prop2)
然后,當您從頂級視圖調用@Html.EditorFor(m => m.Lines)
,您將獲得一個頁面,其中包含每個訂單行的文本框,而不僅僅是一個。
但是,正如您在鏈接的問題中看到的那樣,這僅在您使用EditorFor
特定重載EditorFor
。 如果您提供模板名稱(為了使用不是以OrderLine
類命名的模板),則不會發生自動序列處理,而是會發生運行時錯誤。
此時,您必須將自定義模板的模型聲明為IEnumebrable<OrderLine>
並以某種方式手動迭代其項目以輸出所有項目,例如
@foreach (var line in Model.Lines) {
@Html.EditorFor(m => line)
}
這就是問題開始的地方。
以這種方式生成的 HTML 控件都具有相同的 id 和名稱。 當您稍后發布它們時,模型綁定器將無法構造OrderLine
的數組,並且您在控制器的 HttpPost 方法中獲得的模型對象將為null
。
如果您查看 lambda 表達式,這是有道理的 - 它並沒有真正將正在構造的對象鏈接到它來自模型中的某個位置。
我嘗試了各種迭代項目的方法,似乎唯一的方法是將模板的模型重新聲明為IList<T>
並使用for
枚舉它:
@model IList<OrderLine>
@for (int i = 0; i < Model.Count(); i++)
{
@Html.EditorFor(m => m[i].Prop1)
@Html.EditorFor(m => m[i].Prop2)
}
然后在頂層視圖中:
@model TestEditorFor.Models.Order
@using (Html.BeginForm()) {
@Html.EditorFor(m => m.Lines, "CustomTemplateName")
}
它提供了正確命名的 HTML 控件,這些控件在提交時被模型綁定器正確識別。
雖然這有效,但感覺非常錯誤。
使用帶有EditorFor
的自定義編輯器模板,同時保留允許引擎生成適用於模型綁定器的 HTML 的所有邏輯鏈接的正確、慣用的方法是什么?
在與 Erik Funkenbusch 討論后,導致查看MVC 源代碼,看起來有兩種更好的(正確的和慣用的?)方法來做到這一點。
兩者都涉及為幫助程序提供正確的 html 名稱前綴,並生成與默認EditorFor
輸出相同的 HTML。
我暫時把它留在這里,將做更多的測試以確保它在深度嵌套的場景中工作。
對於以下示例,假設您已經有兩個OrderLine
類的模板: OrderLine.cshtml
和DifferentOrderLine.cshtml
。
IEnumerable<T>
的中間模板創建一個助手模板,以任何名稱保存它(例如“ManyDifferentOrderLines.cshtml”):
@model IEnumerable<OrderLine>
@{
int i = 0;
foreach (var line in Model)
{
@Html.EditorFor(m => line, "DifferentOrderLine", "[" + i++ + "]")
}
}
然后從主 Order 模板調用它:
@model Order
@Html.EditorFor(m => m.Lines, "ManyDifferentOrderLines")
IEnumerable<T>
的中間模板在主訂單模板中:
@model Order
@{
int i = 0;
foreach (var line in Model.Lines)
{
@Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
}
}
似乎沒有比@GSerg 的答案中描述的更容易實現這一目標的方法了。 奇怪的是 MVC 團隊還沒有想出一種不那么凌亂的方法。 我已經制作了這個擴展方法來至少在某種程度上封裝它:
public static MvcHtmlString EditorForEnumerable<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string templateName)
{
var fieldName = html.NameFor(expression).ToString();
var items = expression.Compile()(html.ViewData.Model);
return new MvcHtmlString(string.Concat(items.Select((item, i) => html.EditorFor(m => item, templateName, fieldName + '[' + i + ']'))));
}
有多種方法可以解決這個問題。 在 EditorFor 中指定模板名稱時,無法在編輯器模板中獲得默認的 IEnumerable 支持。 首先,我建議如果你在同一個控制器中有多個相同類型的模板,你的控制器可能有太多的責任,你應該考慮重構它。
話雖如此,最簡單的解決方案是自定義 DataType 。 除了 UIHints 和 typenames 之外,MVC 還使用 DataTypes。 看:
MVC4 中未將自定義 EditorTemplate 用於 DataType.Date
所以,你只需要說:
[DataType("MyCustomType")]
public IEnumerable<MyOtherType> {get;set;}
然后您可以在編輯器模板中使用 MyCustomType.cshtml。 與 UIHint 不同,這不會受到缺乏 IEnuerable 支持的影響。 如果您的用法支持默認類型(例如,電話或電子郵件,則更喜歡使用現有類型枚舉)。 或者,您可以派生自己的 DataType 屬性並使用 DataType.Custom 作為基礎。
您也可以簡單地將您的類型包裝在另一種類型中以創建不同的模板。 例如:
public class MyType {...}
public class MyType2 : MyType {}
然后,您可以非常輕松地創建 MyType.cshtml 和 MyType2.cshtml,並且在大多數情況下,您始終可以將 MyType2 視為 MyType。
如果這對您來說太“hackish”,您始終可以構建模板以根據通過編輯器模板的“additionalViewData”參數傳遞的參數進行不同的呈現。
另一種選擇是使用您傳遞模板名稱的版本來執行類型的“設置”,例如創建表格標簽或其他類型的格式,然后使用更通用的類型版本僅呈現行項目來自命名模板的更通用的形式。
這允許您擁有一個 CreateMyType 模板和一個 EditMyType 模板,除了單個行項目(您可以將其與之前的建議結合使用)之外,它們是不同的。
另一種選擇是,如果您沒有為此類型使用 DisplayTemplates,則可以將 DisplayTempates 用於備用模板(在創建自定義模板時,這只是一個約定......當使用內置模板時,它只會創建顯示版本)。 當然,這是違反直覺的,但如果您只有兩個需要使用的相同類型的模板,而沒有相應的顯示模板,它確實可以解決問題。
當然,您始終可以將 IEnumerable 轉換為模板中的數組,這不需要重新聲明模型類型。
@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
<p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}
我可能會想出十幾種其他方法來解決這個問題,但實際上,任何時候我都遇到過,我發現如果我考慮一下,我可以簡單地重新設計我的模型或視圖方式,不再需要它來解決。
換句話說,我認為這個問題是一種“代碼異味”,表明我可能做錯了什么,重新思考這個過程通常會產生一個沒有問題的更好的設計。
所以來回答你的問題。 正確的慣用方法是重新設計您的控制器和視圖,以便不存在此問題。 除此之外,選擇最不具攻擊性的“黑客”來實現你想要的。
在IEnumerable<T>
屬性上使用FilterUIHint
而不是常規的UIHint
。
public class Order
{
[FilterUIHint("OrderLine")]
public IEnumerable<OrderLine> Lines { get; set; }
}
不需要其他任何東西。
@Html.EditorFor(m => m.Lines)
現在為Lines
每個OrderLine
顯示一個"OrderLine"
EditorTemplate。
您可以使用UIHint
屬性來指導 MVC 您希望為編輯器加載哪個視圖。 所以你的Order
對象會像這樣使用UIHint
public class Order
{
[UIHint("Non-Standard-Named-View")]
public IEnumerable<OrderLine> Lines { get; set; }
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.