ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

ASP.NET MVC の Ajax 通信で例外を処理する

ASP.NET MVC 5 の Ajax 通信でページを部分更新する際に、どのように例外を処理すべきかを悩んだので、まとめておきます。

現象

Ajax 通信で PartialView を返すアプリをシンプルなコードで実装します。

#HomeController.cs
public class HomeController : Controller
{
        public ActionResult Ajax()
        {
            ViewBag.Message = "Ajax Test page.";
            return View();
        }
 
        [HttpPost]
        public ActionResult AjaxTest()
        {
            if (Request.IsAjaxRequest())
            {
                return PartialView("_AjaxResult");
            }
            return Content("Ajax 通信以外のアクセスはできません。");
        }
}
#_AjaxResult.cshtml
<h3 class="text-success">Ajax 通信で返却された部分ビュー</h3>
#Ajax.cshtml
<h2>AjaxTest</h2>
 
@Html.TextBox("test", "Test", new { type = "button", @class = "btn btn-default" })
<div id="ajax-result"></div>
 
@section Scripts
{
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        $(function () {
            $('#test').click(function () {
                $.ajax({
                    type: 'POST',
                    url: 'AjaxTest'
                })
                .done(function (data, textStatus, jqXHR) {
                    $('#ajax-result').html(data);
                });
            })
        })
    </script>
}

Test ボタンを押すと、「/Home/AjaxTest」にリクエストが送信され、ページが部分更新されます。

ajax01

例えば、Form 認証でタイムアウトが発生すると、部分ビューの領域にログインページが表示されてしまいます。

ajax02

また、コントローラーのアクションメソッドで Exception が発生しても、エラーページには遷移しません。

対応

Form 認証でタイムアウトが発生した際にログインページへ遷移されるのは、Web.config で「/Account/Login/」へのリダイレクトを設定しているからです。Ajax 通信の場合は、リダイレクトされたログインページを表示する領域が部分ビューになっているため、上記の現象が発生していました。そのため、Webconfig の設定に頼らずに、View から Ajax 通信したリクエストが返ってきたタイミングでステータスコードを判断して、リダイレクトする方法を取りました。

#Ajax.cshtml
@section Scripts
{
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        $(function () {
            $('#test').click(function () {
                $.ajax({
                    type: 'POST',
                    url: 'AjaxTest'
                })
                .done(function (data, textStatus, jqXHR) {
                    $('#ajax-result').html(data);
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    if (jqXHR.status == 401) {
                        $(location).attr("href", "/Account/Login/");
                    }
                    else {
                        $(location).attr("href", "/Error/AjaxError/");
                    }
                });
            })
        })
    </script>
}

Ajax 通信の場合は、Location ヘッダーにログインページの URL が指定された HttpStatusCode.Found(302)ではなく、HttpStatusCode.Unauthorized(401)を返して欲しいので、前回の投稿で書いた SuppressFormsAuthenticationRedirect プロパティを設定します。

gooner.hateblo.jp

#HomeController.cs
public class HomeController : Controller
{
        protected override void Initialize(RequestContext requestContext)
        {
            if (requestContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
            {
                requestContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
            }
            base.Initialize(requestContext);
        }
}

ログインページへのリダイレクトについては、ここまでの実装で対応できますが、エラーページの遷移にも対応させるため、Ajax 通信用のアクションメソッドを追加します。

#ErrorController.cs
[AllowAnonymous]
public class ErrorController : Controller
{
    public ActionResult AjaxError()
    {
        return View("Error");
    }
}

まとめ

部分更新ではない通常のページは、Web.config のリダイレクトの設定だけで Form 認証のタイムアウトに対応できますし、HandleErrorAttribute のフィルター属性を全体に適用することで、エラーページに遷移させることができます。

Ajax 通信でページを部分更新する際には、クライアント(View)側で個別に対応しなくてはならないのが面倒ですが、仕方ないのかもしれません。やり方はいろいろあると思いますので、ひとつの例として参考までに。