tekitoumemo’s diary

思ったことを書くだけ。長文版Twitter

【ASP.NET MVC, C#】部分ビューでフォームとAjaxを扱う

f:id:tekitoumemo:20180220214244p:plain
MVCで開発してて便利な部分ビューがありますが、フォームとAjaxを扱うことでめんどくさいエラー処理やモーダルで扱う入力処理などめちゃくちゃ楽になったので書きます。サンプルは以下に貼っておきます、参考程度に。
github.com

「何言ってるかわからない」

と思う人も多いかと思いますので、事例を使ってどういうときに便利なのか説明します。
以下のようなモーダルで表示するログイン画面がありますが、モーダル以外にも独立したログイン画面があって複数書くのがめんどくさかったり、冗長になってしまいます。さらにWEBサービスで導線を考えるときにコンバージョンが会員登録、問い合わせになったりするのでいろんなところから多様して使うことも非常に多いです。
f:id:tekitoumemo:20180220215056p:plain
しかもこのような会員登録処理や問い合わせ画面は、エラー処理などアノテーションで対応するのでフォームを使っていることが多く、がちがちに処理が固定されているので汎用的に使えなく結局は別で作成するという羽目になったりします(実際にこういう仕事をしました>馬鹿だった)そこで、部分ビューとして固定されているフォームを使ってアノテーションで判定されたエラーをAjaxで処理出来るとそのまま扱えて楽ちんだねって手法を見つけたのでどのようにやったか説明します。

まずはGetアクションを用意します。ChildActionOnlyは子ビューでしかつかえないよ!ってやつなので僕は必ずつけます。モデルは必要に応じてアノテーションを付けときます。アノテーションのメッセージはデフォルトだと使い物にならないのでErrorMessageを使ってそれとなく通じるメッセージに変更しておきましょう。今回は指定しません。

        [ChildActionOnly]
        public ActionResult Child()
        {
            var model = new ChildModel();
            return View(model);
        }
...
    // 子アクションに使うモデル
    public class ChildModel
    {
        [Required]
        [StringLength(10)]
        public string Name { get; set; }
        [Required]
        [StringLength(255)]
        [EmailAddress]
        public string Email { get; set; }
    }

ビューを用意します。簡単に名前とメールアドレスのテキストとsubmitボタンを用意します。Layout はヘッダー、フッターのデザインマスタを指定しますが部分ビューなので空にしておきます。

@{
    ViewBag.Title = "Child Page";
    // 子ビューだからいらない!
    Layout = "";
}

<div id="MailForm">
    <div class="col-md-4">
        <h2>Child Action</h2>
        @using (Html.BeginForm("Child", "Home", FormMethod.Post))
        {
            @Html.ValidationSummary("")
            @Html.AntiForgeryToken()
            @Html.TextBoxFor(model => model.Name)
            @Html.TextBoxFor(model => model.Email)
            <input id="send" type="submit" value="送信" />
        }
    </div>
</div>

次にPostメソッドを用意します。フォーム通信には必ずValidateAntiForgeryTokenを付けましょう。ValidateAntiForgeryTokenはフォームで生成したトークン(Html.AntiForgeryToken())をチェックし、違っていたら処理をはじいてくれます、超便利。ModelState.IsValidはモデルの状態を表していて、アノテーションでチェックに引っかかった場合にFalseとなります。なにも処理せずにViewを返すと@Html.ValidationSummary("")でリストタグとしてエラーを勝手に表示してくれます。

        [HttpPost]
        [ValidateAntiForgeryToken()]
        public ActionResult Child(ChildModel model)
        {
            if (ModelState.IsValid)
            {
                // 成功
                var result = new ContentResult() { Content = "Success" };
                return result;
            }
            
            return View(model);
        }

次に通信するためのAjaxを用意します。Ajaxはヘルパーでも使えるのですが、あまり使いやすくない(知識ないだけ)のでゴリゴリに書きました。ここでのコツはevent.preventDefault();で送信処理をキャンセルしてください、2回Postされちゃうので。$('form').serialize();でフォームの値が取得できるのでそれをAjaxにセットします。functionでエラーの場合はHTMLがごっそり返ってくるのでJqueryのDom操作を使ってごっそり入れ替えます。そうすると、エラーの場合はフォームで生成されたエラーエッセージが@Html.ValidationSummary("")を配置した場所にリストタグとして出力され、成功した場合は他の処理をすればよいだけです。

$(document).on('click', '#send', function (event) {
        // HTMLでの送信をキャンセル
        event.preventDefault();
        // formデータを取得&送信
        var formData = $('form').serialize();
        $.ajax({
            async: false,
            url: ' /Home/Child',
            type: 'POST',
            data: formData,
            timeout: 10000,
            dataType: 'text',
            success: function (data) {
                if (data == "Success") {
                    location.href = "/";
                } else {
                    //エラーの場合、MailFormのDomを差し替える
                    $("#MailForm").html(data);
                }

            }
        });
    });

これらを駆使すれば、フォームを使っていた処理をそのまま使えてAjax通信でリロードしなくてもエラーメッセージが使えるので非常に便利です。子ビューは覚えるといっぱい使いたくなりますが、いろんなところで扱うビューだけにしたほうがよいです。僕も結構好きでいろいろ使いましたがわかりにくくなっちゃったりしたので微妙でした、Angularなどのコンポーネント志向にどっぷりつかる感じと似てるかも。

割とメジャーな技術なので、知ってる方も多いと思いますが、この記事でちゃんとしたところ(無駄に子ビューを作らない)で汎用的に部分ビューを扱えればと良いなと思います。