Razor構文・PartialView・ViewComponent・DOM操作などで生成したHTMLをモデルバインドする方法

投稿者: | 2022年2月23日

asp.netにはモデルバインドという便利な機能があるのですが下記の機能を使った場合のバインド方法について、実務で詰まったので解決方法を記事にまとめます。

  • Razor構文のループ処理で生成したHTMLタグとコレクションのデータをモデルバインドする方法
  • 同じ部分ビューを1つのページで使用する場合のモデルバインドする方法
  • ビューコンポーネントをモデルバインドする方法
  • JavascriptでDOM操作したHTML情報をモデルバインドする方法

前提知識

モデルバインドを説明する前にクライアントのデータ送信サーバーサイドのデータ解析の説明から、このWebサイトの基本さえ押さえればデータバインドは簡単に理解できる。

クライアントのデータ送信

ブラウザはユーザーがボタンを押したときにformタグに囲まれたinputタグからキーと値のペアのデータを生成しサーバに送信します。

送信するデータの形式はメソッドによって異なり、GetメソッドはクエリパラメータPostメソッドはフォームデータという形サーバサイドに送信します。

下記はuseridとemailのデータを送信するサンプルです。

Getメソッドのサンプル

<form method="get" asp-Page="Sample" >
    <input type="text" name="userid" value="nanasi" >
    <input type="text" name="email" value="sample@gmail.com">
    <button type="submit">送信</button>
</form>

ボタンを押すと下記のUrlにクエリパラメータを付与して送信します。

https://ドメイン/sample?userid=nanasi&email=sample@gmail.com

Postメソッドのサンプル

<form method="post" asp-page="Sample" >
    <input type="text" name="userid" value="nanasi" >
    <input type="text" name="email" value="sample@gmail.com">
    <button type="submit">送信</button>
</form>

ボタンを押すと下記のUrlにフォームデータを付与して送信します。

https://ドメイン/sample
userid : nanasi
email : sample@gmail.com

サーバサイドのデータ解析

さて、ブラウザからクエリパラメータもしくはフォームデータを受け取ったサーバサイドは文字列を解析してC#言語のプリミティブのデータ型に変換する必要があります。

解析方法は単純でinputタグのname属性と変数名を同一にすればinputタグのvalue属性が変数値に代入される仕組みとなっています。

さきほどのuseridとemailのサンプルを受信して解析したい場合は、下記のようなコードを作成します。

引数で実装するサンプル( Razor Page )

public class SampleModel : PageModel
{
    public void OnGet(string userid, string email){}
    public void OnPost(string userid, string email){}
}

データクラスで実装するサンプル( Razor Page )

public class SampleModel : PageModel
{
    public void OnGet(SampleModel input){}
    public void OnPost(SampleModel input){}
}

public SampleModel{
    public string UserId {get; set; }
    public string Emaill {get; set; }
}

本題

Web開発の前提知識を解説したので、ここからはasp.netのRazor構文、PartialView、ViewComponet、そして、JavascriptでDOM操作で生成したHTMLをモデルバインドする方法について説明します。

asp.netにはasp-forという入力タグヘルパーがあるので、こちらを使ってサンプルコードを記載します。簡単に説明するとクラスのプロパティからid属性やname属性を自動生成するタグヘルパーです。name属性値がデータバインドの有効性を決めるので、asp-forを使って開発する場合はページのソースを表示して生成されたname属性値を確認しながら開発を進めてください。

Razor構文でループを使うサンプル

サーバから取得したデータをリスト表示して、送信ボタンを押すとポストバックするサンプル。これをRazor構文でループ処理をして作成します。

<form method="post">
    <ul>
        @for (int i = 0; i < Model.Users.Count; i++)
        {
        <li>
            <input asp-for="@Model.Users[i].UserId" />
            <input asp-for="@Model.Users[i].Email" />
        </li>
        }
    </ul>
    <button type="submit">送信</button>
</form>

下記は生成されたHTMLです。asp-forヘルパーはidとname属性値が同じものが生成してないことが確認できます。しかし、上記のRazor構文をforではなくforeachで実装してしまうと、asp-forヘルパーは同一のidとname属性値を生成しまうのでforeachは使わないで下さい。

<form method="post">
    <ul>
        <li>
            <input type="text" id="Users_0__UserId" name="Users[0].UserId" value="susuki" />
            <input type="text" id="Users_0__Email" name="Users[0].Email" value="susuki@mail" />
        </li>
        <li>
            <input type="text" id="Users_1__UserId" name="Users[1].UserId" value="tanaka" />
            <input type="text" id="Users_1__Email" name="Users[1].Email" value="tanakaemail" />
        </li>
    </ul>
    <button type="submit">送信</button>
<input name="__RequestVerificationToken" type="hidden" value="***"></form>

サーバサイドは下記のように作成します。注意してほしいのは変数名をname属性と同じ名前になるようにする点で、今回のサンプルではusersにする必要があります。

public IActionResult OnPostAsync(IList<UserModel> users)
{
   //
}

同じ部分ビューを1つのページで使用する場合のモデルバインドする方法

さきほど解説したRazor構文で実装する方法でinputタグの部分を部分ビューとして作成してみます。

下記はその部分ビューのコードです。。

@model UserModel

<input asp-for="@Model.UserId" />
<input asp-for="@Model.Email" />

そして、その部分ビューを利用したページのサンプルを作ります。

<form method="post">
    <ul>
        @for (int i = 0; i < Model.Users.Count; i++)
        {
            <li>
                <partial name="User.cshtml" model="@Model.Users[i]" />
            </li>
        }
    </ul>
    <button type="submit">送信</button>
</form>

簡単ですね。しかし、下記のようにModel側が配列でない場合はどうでしょうか?

<form method="post">
    <ul>
        <li>
            <partial name="User.cshtml" model="@Model.UserA" />
        </li>
        <li>
            <partial name="User.cshtml" model="@Model.UserB" />
        </li>
    </ul>
    <button type="submit">送信</button>
</form>

生成されたHTMLを確認してみます。

<form method="post">
    <ul>
       <li>
         <input type="text" id="UserId" name="UserId" value="susuki" />
         <input type="text" id="Email" name="Email" value="susuki@mail" />
       </li>
       <li> 
         <input type="text" id="UserId" name="UserId" value="tanaka" />
         <input type="text" id="Email" name="Email" value="tanaka@mail" />
       </li>
   </ul>
   <button type="submit">送信</button>
   <input name="__RequestVerificationToken" type="hidden" value="***" />
</form>

同名のname属性値が生成されてしまっています。これではサーバサイド側で解析ができません。なので、部分ビューで識別子を付与して区別できるようにします

まず、Modelクラスを継承してPrefixというプロパティを付与したViewModelクラスを作成します。

    public class UserViewModel : UserModel
    {
        public string Prefix { get; set; }
    }

そして、部分ビューは値にPrefixを付与するように下記のように書き換えます。

@model TaskNote.JQruery.Pages.UserViewModel
@{
    var userid = $"{Model.Prefix}.{nameof(Model.UserId)}";
    var email = $"{Model.Prefix}.{nameof(Model.Email)}";
}

<input id="@userid" name="@userid" asp-for="@Model.UserId" />
<input id="@email" name="@email" asp-for="@Model.Email" />

これで、一意の値になりました。下記はサーバサイド側のコードです。

public UserViewModel UserA { get; set; } = new UserViewModel()
{
    Prefix = nameof(UserA),
};
public UserViewModel UserB { get; set; } = new UserViewModel()
{
    Prefix= nameof(UserB),
};  

public IActionResult OnPostAsync(Response response) 
{
   // 
}

public class Response {
    public UserModel UserA { get; set; }
    public UserModel UserB { get; set; }
}

ビューコンポーネントをモデルバインドする方法

方法は部分ビューと同じで、Prefixを付与して一意のname属性値を作成できるようにします。

Razor構文は部分ビューと全く同じです。

@model TaskNote.JQruery.Pages.UserViewModel
@{
    var userid = $"{Model.Prefix}.{nameof(Model.UserId)}";
    var email = $"{Model.Prefix}.{nameof(Model.Email)}";
}

<input id="@userid" name="@userid" asp-for="@Model.UserId" />
<input id="@email" name="@email" asp-for="@Model.Email" />

ビューコンポーネントなので、部分ビューと違ってモデルデータはメインページから受け取りません。下記のようなコードを作成してサーバから直接を取得してデータを表示します。

    public enum UserViewComponentType
    {
        Default,
    }

    public class UserViewComponent : ViewComponent
    {
        public UserViewComponent()
        {
        }

        public async Task<IViewComponentResult> InvokeAsync(string prefix, UserViewComponentType type)
        {
            var model = new UserViewModel()
            {
                Prefix = prefix,
            };
            // 本来はサーバから取得
            model.UserId = "nanasi";
            model.Email = "nanasi@mail";
            return View(type.ToString(), model);
        }
    }

下記はそのビューコンポーネントを使ったサンプル

<form method="post">
    <ul>
        <li>
            <vc:user prefix="UserA" type="Default" />
        </li>
        <li>
            <vc:user prefix="UserB" type="Default" />
        </li>
    </ul>
    <button type="submit">送信</button>
</form>

ちなみにvcタグは初期設定をしないと使えません。
タグ ヘルパーとしてビュー コンポーネントを呼び出す

JavascriptでDOM操作したHTML情報をモデルバインドする方法

流行りのSPA開発だとHTMLをJavascriptで複雑に操作するのでデータ送信にformとinputタグを使うよりもfetchApiを使います。

ですが、ページ遷移を伴う通信はfetchApiでは実装できないので、formタグの子にinputタグを生成するスクリプトをbuttonタグのonclickイベントに登録する方法をとります。

<form id="sample-form" method="post">
  <button type="submit" asp-controller="OnPost" asp-action="ActionMethod" onclick="createInputElement();">出力</button>
</form>
function createInputElement() {
  const form = document.getElementById('sample-form');

  // ファイルのダウンロードなどページ遷移しない場合でも使えるように
  // inputタグは全削除する処理を入れておいたほうが良い
  var children = Array.from(form.getElementsByChild('input'));
  children.forEach(child => form.removeChild(child));

  // inputタグを追加する
  let index = 0;
  jsonDatas.forEach(data => {
    const userid = document.createElement('input');
    userid.type = 'hidden';
    userid.id = '[' + index + '].userid';
    form.appendChild(input);

    const email = document.createElement('input');
    email.type = 'hidden';
    email.id = '[' + index + '].email';
    form.appendChild(input);
  });
}

参考文献

[ASP.NET Core] Post メソッドで配列やリストを渡す

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)