web-dev-qa-db-ja.com

ASP.NET MVCのIEnumerableモデルでカスタムエディターテンプレートを使用する正しい慣用的な方法

この質問は DisplayEnumがIEnumerable <DateTime>をループしないのはなぜですか?のフォローアップです


クイックリフレッシュ。

いつ:

  • モデルには、タイプ_IEnumerable<T>_のプロパティがあります
  • ラムダ式のみを受け入れるオーバーロードを使用して、このプロパティをHtml.EditorFor()に渡します。
  • views/Shared/EditorTemplatesの下にタイプTのエディターテンプレートがあります。

その後、MVCエンジンは列挙可能なシーケンス内の各アイテムのエディターテンプレートを自動的に呼び出し、結果のリストを生成します。

たとえば、プロパティOrderを持つモデルクラスLinesがある場合:

_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)を呼び出すと、1行だけでなく各注文行のテキストボックスを含むページが表示されます。


ただし、リンクされた質問でわかるように、これはEditorForの特定のオーバーロードを使用する場合にのみ機能します。 (OrderLineクラスにちなんで命名されていないテンプレートを使用するために)テンプレート名を指定すると、自動シーケンス処理は行われず、代わりにランタイムエラーが発生します。

その時点で、カスタムテンプレートのモデルを_IEnumebrable<OrderLine>_として宣言し、何らかの方法でアイテムを手動で反復して、すべてを出力する必要があります。

_@foreach (var line in Model.Lines) {
    @Html.EditorFor(m => line)
}
_

そして、そこから問題が始まります。

この方法で生成されたHTMLコントロールは、すべて同じIDと名前を持っています。後でPOST=それらを使用すると、モデルバインダーはOrderLinesの配列を構築できなくなり、コントローラーのHttpPostメソッドで取得するモデルオブジェクトはnullになります。
ラムダ式を見ると、これは理にかなっています-実際に構築されているオブジェクトを、それが由来するモデル内の場所にリンクしません。

アイテムを反復処理するさまざまな方法を試しましたが、唯一の方法はテンプレートのモデルを_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コントロールを提供します。


これは機能しますが、非常に間違っているように感じます。

エンジンがモデルバインダーに適したHTMLを生成できるようにするすべての論理リンクを維持しながら、EditorForでカスタムエディターテンプレートを使用する正しい慣用的な方法は何ですか?

49
GSerg

Erik Funkenbuschとの議論 の後、 MVCソースコード を調べることになりましたが、それを行うための2つのより良い(正しい、慣用的な)方法があるように見えます。

どちらも、正しいhtml名のプレフィックスをヘルパーに提供し、デフォルトのEditorForの出力と同一のHTMLを生成することを伴います。

とりあえずここに残しておき、さらにネストされたシナリオで機能することを確認するためにさらにテストを行います。

次の例では、OrderLineクラスの2つのテンプレートOrderLine.cshtmlDifferentOrderLine.cshtmlがすでにあるとします。


方法1-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")

方法2-IEnumerable<T>の中間テンプレートなし

メインの注文テンプレートで:

@model Order

@{
    int i = 0;

    foreach (var line in Model.Lines)
    {
        @Html.EditorFor(m => line, "DifferentOrderLine", "Lines[" + i++ + "]")
    }
}
32
GSerg

これを達成する簡単な方法は、 @ 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 + ']'))));
}
3
Anders

この問題に対処する方法はいくつかあります。 EditorForでテンプレート名を指定する場合、エディターテンプレートでデフォルトのIEnumerableサポートを取得する方法はありません。まず、同じコントローラーに同じタイプの複数のテンプレートがある場合、コントローラーの責任が多すぎるため、リファクタリングを検討することをお勧めします。

そうは言っても、最も簡単な解決策はカスタムDataTypeです。 MVCは、UIHintsおよびtypenamesに加えてDataTypesを使用します。見る:

DataType.DateのMVC4で使用されていないカスタムEditorTemplate

だから、あなただけ言う必要があります:

[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として扱うことができます。

これがあまりにも「ハッキング」されている場合は、エディターテンプレートの「additionalViewData」パラメーターを介して渡されたパラメーターに基づいて異なるレンダリングを行うテンプレートをいつでも作成できます。

別のオプションは、テンプレート名を渡すバージョンを使用して、テーブルタグの作成や他の種類のフォーマットなど、タイプの「セットアップ」を行い、より一般的なタイプバージョンを使用して、名前付きテンプレート内からのより一般的な形式。

これにより、個々の行項目(以前の提案と組み合わせることができる)を除いて、CreateMyTypeテンプレートとEditMyTypeテンプレートを使用できます。

もう1つのオプションは、このタイプにDisplayTemplatesを使用していない場合、代替テンプレートにDisplayTempatesを使用できます(カスタムテンプレートを作成する場合、これは単なる慣習です..組み込みテンプレートを使用する場合は、作成するだけです表示バージョン)。確かに、これは直感に反しますが、使用する必要のある同じタイプのテンプレートが2つしかなく、対応する表示テンプレートがない場合に問題を解決します。

もちろん、IEnumerableを常にテンプレート内の配列に変換するだけでよく、モデルタイプを再宣言する必要はありません。

@model IEnumerable<MyType>
@{ var model = Model.ToArray();}
@for(int i = 0; i < model.Length; i++)
{
    <p>@Html.TextBoxFor(x => model[i].MyProperty)</p>
}

私はおそらくこの問題を解決するために他の多くの方法を考えることができますが、実際には、私がそれを経験したときはいつでも、私はそれについて考えれば、私はそのようなモデルまたはビューを単純に再設計できることがわかりました解決する必要がなくなりました。

言い換えれば、この問題は「コード臭」であり、おそらく何か間違ったことをしている兆候であり、プロセスを再考することで、問題のないより良い設計が得られると考えています。

あなたの質問に答えるために。正しい、慣用的な方法は、この問題が存在しないようにコントローラーとビューを再設計することです。それを除いて、最も不快な「ハック」を選択しないあなたが望むものを達成する

3