ASP.NET MVC - 部分ビューにあるinput要素のname属性にプレフィックスを付ける

探していたことはまさにコレでした。

stackoverflow.com

TemplateInfoもよくわかっていなかったので真似してサンプルを書いてみました。

やりたいこと

まずはやりたいことを整理します。

次のようにAddressクラスのプロパティを2つ持つモデル、SampleInputModelがあるとします。コントローラのアクションでこのモデルをバインドしたいとします。

// 住所氏名
public class Address {
    [Display(Name = "住所")]
    public string Place { get; set; }

    [Display(Name = "名前")]
    public string Name { get; set; }
}

// 入力モデル
// Addressクラスを2つ持つ
public class SampleInputModel {
    [Display(Name = "送付先")]
    public Address ToAddress { get; set; }

    [Display(Name = "送付元")]
    public Address FromAddress { get; set; }
}

そしてAddressクラス用に部分ビューを用意したいとします。(このサンプルだと部分ビューにするまでも・・・という気がしますが、モデルもビューのマークアップももっと複雑だという体でお願いします。)

@* _Address.cshtml *@
@* Addressクラス用の部分ビュー *@

@model Address

<div>@Html.LabelFor(model => model.Place):@Html.TextBoxFor(model => model.Place)</div>
<div>@Html.LabelFor(model => model.Name):@Html.TextBoxFor(model => model.Name)</div>

RenderPartialメソッドを使って部分ビューを呼び出しますが、input要素のname属性が重複してしまいます。

<form action="@Url.Action()" method="post">
    @Html.LabelFor(model => model.ToAddress)
    @{
        Html.RenderPartial("_Address", Model.ToAddress);
    }
    <hr />
    @Html.LabelFor(model => model.FromAddress)
    @{
        Html.RenderPartial("_Address", Model.FromAddress);
    }
    <hr />
    <button type="submit">保存</button>
</form>

<!-- 生成されるhtml -->
<!-- input要素のname属性が重複している -->
<form action="/" method="post">
    <label for="ToAddress">送付先</label>
    <div><label for="Place">住所</label><input id="Place" name="Place" type="text" value="" /></div>
    <div><label for="Name">名前</label><input id="Name" name="Name" type="text" value="" /></div>
    <hr />
    <label for="FromAddress">送付元</label>
    <div><label for="Place">住所</label><input id="Place" name="Place" type="text" value="" /></div>
    <div><label for="Name">名前</label><input id="Name" name="Name" type="text" value="" /></div>
    <hr />
    <button type="submit">保存</button>
</form>

これではモデルにうまくバインドできません。id属性も重複していてダメですし。name属性をToAddress.NameFromAddress.Nameといった感じにする必要があります。

解決するには

モデルにバインドできるようにname属性を設定するには、TemplateInfo.HtmlFieldPrefixプロパティを使います。

このプロパティはTemplateInfo.GetFullHtmlFieldメソッド内で使われています。TemplateInfo.GetFullHtmlFieldメソッドはTextBoxForといったHtmlHelperの拡張メソッド内でname属性を作るのに使われています。

ということでTemplateInfo.HtmlFieldPrefixプロパティを使うRenderPartialFor拡張メソッドを書いてみます。

public static class HtmlHelperExtensions {
    // RenderPartialのラムダ式版
    public static void RenderPartialFor<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        string partialViewName,
        Expression<Func<TModel, TProperty>> expression) {

        // ラムダ式からモデルのメタデータを取得して、
        // さらにメタデータからモデルを取得する
        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        var model = metadata.Model;

        // TemplateInfoは、TextBoxForなどのHtmlHelperメソッドの拡張メソッドで
        // name属性を取得するのに使っているクラス
        var templateInfo = new TemplateInfo {
            // ラムダ式から「プロパティをドットで連結する文字列」を取得して、
            // プレフィックスに指定する
            HtmlFieldPrefix = ExpressionHelper.GetExpressionText(expression),
        };

        // TemplateInfoを差し替えるためにビューデータも作成する
        var viewData = new ViewDataDictionary(htmlHelper.ViewData) {
            TemplateInfo = templateInfo,
        };

        htmlHelper.RenderPartial(partialViewName, model, viewData);
    }
}

ビューでRenderPartialメソッドの代わりにRenderPartialForメソッドを使ってみると。

<form action="@Url.Action("Edit")" method="post">
    @Html.LabelFor(model => model.ToAddress)
    @{
        Html.RenderPartialFor("_Address", model => model.ToAddress);
    }
    <hr />
    @Html.LabelFor(model => model.FromAddress)
    @{
        Html.RenderPartialFor("_Address", model => model.FromAddress);
    }
    <hr />
    <button type="submit">保存</button>
</form>

<!-- 生成されるhtml -->
<form action="/" method="post">
    <label for="ToAddress">送付先</label>
    <div><label for="ToAddress_Place">住所</label><input id="ToAddress_Place" name="ToAddress.Place" type="text" value="" /></div>
    <div><label for="ToAddress_Name">名前</label><input id="ToAddress_Name" name="ToAddress.Name" type="text" value="" /></div>
    <hr />
    <label for="FromAddress">送付元</label>
    <div><label for="FromAddress_Place">住所</label><input id="FromAddress_Place" name="FromAddress.Place" type="text" value="" /></div>
    <div><label for="FromAddress_Name">名前</label><input id="FromAddress_Name" name="FromAddress.Name" type="text" value="" /></div>
    <hr />
    <button type="submit">保存</button>
</form>

name属性がいい感じに出力されています。これでモデルへのバインドもうまくできます。

さすがMVC