Edited at
ASP.NETDay 19

System.Web.Mvc.Ajaxとunobtrusive-ajax

More than 1 year has passed since last update.


はじめに

AjaxExtensions.ActionLink()Html.AntiForgeryToken()を使ってPOSTする」コードを通じて「System.Web.Mvc.Ajaxとunobtrusive-ajax」を知ろうとしたお話です。基礎的な内容になりますが、案外まとまった情報が無いっぽいので、この場をお借りして書き残したいと思います。

実は「全部ここにまとまってる」と言われると、この記事の存在意義無いんですが、それは忘れておきます。。

あと、CSRF対策やtoken自体のお話は他に譲ります。


1. この記事の趣旨

tokenを利用してPOSTするには、以下のように実装します。


  • Controller側


    • リクエストを受信するメソッドにValidateAntiForgeryTokenAttribute属性を付与

    • リクエストを受信するメソッドにHttpPostAttribute属性を付与



  • View側


    • あらかじめHtml.AntiForgeryToken()を使いtokenを生成する

    • tokenをリクエストに含めて送信する



Ajax.BeginForm()で生成される<form>と、<input type="submit">の組み合わせにより、ボタンをクリックさせてPOSTするケースが一般的です。一方で、<a>で普通のテキストリンクをクリックしたときにPOSTしたいケースもよくあります。このとき、AjaxExtensions.ActionLink()を使いますが、無策で@Html.AntiForgeryToken()を使いPOSTすると、tokenがサーバーに送信されないため、エラーになります。

この記事の趣旨は、「AjaxExtensions.ActionLink()でもAntiForgeryToken使えるよ~」というメッセージです。


2. POSTでtokenが送信されている様子を確認する


2-1. AjaxExtensions.BeginForm()でPOSTのサンプル

まずは気軽に。基礎的なパターンを使って振り返ります。以下は「submit」というボタンをクリックすると、NowWithToken1というid属性を持つdiv要素が現在時刻で更新されるコードです。


Home.cshtml

@using (Ajax.BeginForm("NowWithToken", new AjaxOptions

{
UpdateTargetId = "NowWithToken1",
InsertionMode = InsertionMode.Replace,
}))
{
@Html.AntiForgeryToken();
<input type="submit" name="submit" />
}
<div id="NowWithToken1">
<!--/* ここがPOSTの結果(DateTime.now)で更新されます */-->
</div>


2-2. tokenはどこに?

上記のコードでリクエストを送信する様子を開発ツールで確認してみます(大体のブラウザでF12キー押すと出てきます。詳細は使い方お使いのブラウザによるので、割愛します)。


3. AjaxExtensions.ActionLinkでtokenを送る方法

開発ツールで見たFrom Dataにtokenを加えてあげればよいのです。


3-1. AjaxExtensions.ActionLinkでPOSTするコード

以下は「action link.」というリンクをクリックすると、NowWithToken3というid属性を持つdiv要素が現在時刻で更新されるコードです。


Home.cshtml

<!-- この位置である必要はない -->

<span id="token">
@Html.AntiForgeryToken()
</span>

<!-- linkを出力 -->
@Ajax.ActionLink("action link.", "NowWithToken", new AjaxOptions
{
UpdateTargetId = "NowWithToken3",
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
OnBegin = "onBegin",
})

<div id="NowWithToken3">
<!--/* ここがPOSTの結果(DateTime.now)で更新されます */-->
</div>

@section scripts{
@Scripts.Render("~/Scripts/jquery.unobtrusive-ajax.min.js")
}

<script type=text/javascript>
function onBegin(jqXHR, settings) {
var token = $('#token [name=__RequestVerificationToken]').val();
settings.data = settings.data + '&__RequestVerificationToken=' + token;
}
</script>


上記のコードは、次のHTMLを出力します。


output_Of_Home.cshtml

<span id="token">

<input name="__RequestVerificationToken" type="hidden" value="xxxx-tokenの文字列-xxx" />
</span>

<a data-ajax="true" data-ajax-begin="onBegin" data-ajax-method="POST" data-ajax-mode="replace" data-ajax-update="#NowWithToken3" href="/Home/NowWithToken">action link.</a>

<div id="NowWithToken3"></div>

<script type=text/javascript>
function onBegin(jqXHR, settings) {
var token = $('#token [name=__RequestVerificationToken]').val();
settings.data = settings.data + '&__RequestVerificationToken=' + token;
}
</script>



3-2. なにをしているか?

送信前に呼ばれるfunctionで、リクエストにtokenを追加しているだけです。

@Ajax.ActionLink()AjaxOptionを渡します。AjaxOptionは送信前にコールしたいjavascriptのfunctionを指定できるため、送信前に#tokenからtoken文字列を取得してリクエストに付加しています。


4. System.Web.Mvc.Ajax名前空間とunobtrusive-ajax

ここからが本質なんですが、時間が足りずちょっと雑な仕上がりになりそう。ご容赦ください。

unobtrusive-ajaxをあまり意識したことがないとしたら、下記は目からウロコかもしれません。unobtrusive-ajaxってなんですか~?みたいなお話も、他に譲ります。


4-1. System.Web.Mvc.Ajax名前空間

上記のようにhtmlが生成される過程は、unobtrusive-ajaxSystem.Web.Mvc.Ajax名前空間のAjaxExtensionsAjaxOptionsが何をやっているか知る必要があります。ソース嫁で終わるんですが、ちょっとだけソース貼り付けておきます。

最終的にはAjaxOptions.ToUnobtrusiveHtmlAttributes()が効いてきます。

このメソッドのおかげで、unobtrusive-ajax.jsが活躍します。


System.Web.Mvc.Ajax.AjaxExtensions.cs

// https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Ajax/AjaxExtensions.cs#L196

public static MvcHtmlString ActionLink(this AjaxHelper ajaxHelper, string linkText, string actionName, AjaxOptions ajaxOptions)
{
return ActionLink(ajaxHelper, linkText, actionName, (string)null /* controllerName */, ajaxOptions);
}

public static MvcHtmlString RouteLink(this AjaxHelper ajaxHelper, string linkText, string routeName, string protocol, string hostName, string fragment, RouteValueDictionary routeValues, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
{
if (String.IsNullOrEmpty(linkText))
{
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "linkText");
}

string targetUrl = UrlHelper.GenerateUrl(routeName, null /* actionName */, null /* controllerName */, protocol, hostName, fragment, routeValues ?? new RouteValueDictionary(), ajaxHelper.RouteCollection, ajaxHelper.ViewContext.RequestContext, false /* includeImplicitMvcValues */);

return MvcHtmlString.Create(GenerateLink(ajaxHelper, linkText, targetUrl, GetAjaxOptions(ajaxOptions), htmlAttributes));
}

private static string GenerateLink(AjaxHelper ajaxHelper, string linkText, string targetUrl, AjaxOptions ajaxOptions, IDictionary<string, object> htmlAttributes)
{
TagBuilder tag = new TagBuilder("a")
{
InnerHtml = HttpUtility.HtmlEncode(linkText)
};

tag.MergeAttributes(htmlAttributes);
tag.MergeAttribute("href", targetUrl);

if (ajaxHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
{
tag.MergeAttributes(ajaxOptions.ToUnobtrusiveHtmlAttributes()); // ← ココ!
}
else
{
tag.MergeAttribute("onclick", GenerateAjaxScript(ajaxOptions, LinkOnClickFormat));
}

return tag.ToString(TagRenderMode.Normal);
}


そして、ToUnobtrusiveHtmlAttributes()では、以下のように出力するhtmlの文字列を組み立てていきます。


System.Web.Mvc.Ajax.AjaxOptions

public IDictionary<string, object> ToUnobtrusiveHtmlAttributes()

{
var result = new Dictionary<string, object>
{
{ "data-ajax", "true" },
};

AddToDictionaryIfSpecified(result, "data-ajax-url", Url);
AddToDictionaryIfSpecified(result, "data-ajax-method", HttpMethod);
AddToDictionaryIfSpecified(result, "data-ajax-confirm", Confirm);

AddToDictionaryIfSpecified(result, "data-ajax-begin", OnBegin);
AddToDictionaryIfSpecified(result, "data-ajax-complete", OnComplete);
AddToDictionaryIfSpecified(result, "data-ajax-failure", OnFailure);
AddToDictionaryIfSpecified(result, "data-ajax-success", OnSuccess);
...


合わせて、上記で出力したhtmlを利用するjs側です。functionをコールしている様子が見て取れます。


jquery.unobtrusive-ajax.js

// https://github.com/aspnet/jquery-ajax-unobtrusive/blob/master/jquery.unobtrusive-ajax.js#L91

$.extend(options, {
type: element.getAttribute("data-ajax-method") || undefined,
url: element.getAttribute("data-ajax-url") || undefined,
cache: !!element.getAttribute("data-ajax-cache"),
beforeSend: function (xhr) {
var result;
asyncOnBeforeSend(xhr, method);
result = getFunction(element.getAttribute("data-ajax-begin"), ["xhr"]).apply(element, arguments);
if (result !== false) {
loading.show(duration);
}
return result;
},
complete: function () {
loading.hide(duration);
getFunction(element.getAttribute("data-ajax-complete"), ["xhr", "status"]).apply(element, arguments);
},
success: function (data, status, xhr) {
asyncOnSuccess(element, data, xhr.getResponseHeader("Content-Type") || "text/html");
getFunction(element.getAttribute("data-ajax-success"), ["data", "status", "xhr"]).apply(element, arguments);
},
error: function () {
getFunction(element.getAttribute("data-ajax-failure"), ["xhr", "status", "error"]).apply(element, arguments);
}
});



ということで

結局はjQueryで$.Ajax()とやっているのと同じです。なので、jQueryのリファレンスが参考になります。ただし、橋渡しの過程で方言が生まれており、jQueryすなわちjquery.unobtrusive-ajax.jsではbeforeSend、C#(Razor)の世界ではOnBeginになっています。微妙なのは、AjaxOptionsのソレと$.Ajax()のソレ(関数名)が似ているけど微妙に違うことです。ちょっと混乱します。

@Ajax.ActionLink("action link.", "NowWithToken", new AjaxOptions

{
UpdateTargetId = "NowWithToken",
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
OnBegin = "onBegin",
})



OnBegin = "onBegin"

とか

<script type=text/javascript>

function onBegin(jqXHR, settings) {
var token = $('#token [name=__RequestVerificationToken]').val();
settings.data = settings.data + '&__RequestVerificationToken=' + token;
}
</script>



function onBegin(jqXHR, settings) {

あたりがめっちゃ気になったります。jqXHRとか唐突感ありますよね。

それぞれ、

http://api.jquery.com/jquery.ajax/

https://github.com/aspnet/jquery-ajax-unobtrusive/blob/master/jquery.unobtrusive-ajax.js

を比較しながら繋がりが確認できると「なるほど~」となり、応用が効きます。


さいごに

なんだかとても長くなってしまった。。。一般論として、リクエスト&レスポンスで何を送受信しているかは、把握しておくと色々と捗りますね。同様にViewでRazorを書きますが、その生成結果とjavascript側との関連も押さえないといけませんね(自戒を込めて)。

サンプルソースをgithubに置きました。

aspnet_sample/01_AntiForgeryTokenWithAjax at master · mtaniuchi/aspnet_sample :

https://github.com/mtaniuchi/aspnet_sample/tree/master/01_AntiForgeryTokenWithAjax