Visual Studio 2019 / 2022 のテンプレートを使って作成する Web Forms アプリのプロジェクトでは、Microsoft.AspNet.FriendlyUrls という NuGet パッケージがインストールされ、デフォルトで有効になるように設定されていますが、デフォルトの設定のままではクライアントスクリプトでページの静的メソッドが呼び出せないという話を書きます。
いろいろ説明すると長くなるのでまず最初に解決策を書いておきます。
自動生成されて App_Start フォルダに配置されている RoutConfig.cs で、以下のコードの通りリダイレクトモードが Permanent に設定されていますが、解決策はそれを Off に変更することです。
using Microsoft.AspNet.FriendlyUrls;
using System.Web.Routing;
namespace WebForms3
{
public static class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
var settings = new FriendlyUrlSettings();
// デフォルトは Permanent
// settings.AutoRedirectMode = RedirectMode.Permanent;
// Off に変更
settings.AutoRedirectMode = RedirectMode.Off;
routes.EnableFriendlyUrls(settings);
}
}
}
Off にするのは SEO 的に好ましくないかもしれません。しかし、FriendlyUrls を使って拡張子 .aspx なしの url で呼び出せるようにしたい、かつクライアントスクリプトでページの静的メソッドを呼び出せるようにしたいのであれば、他に方法はなさそうです。
以下にどういうメカニズムになっているかなどの説明を書いておきます。興味があれば読んでください。
FriendlyUrls とは何か
ASP.NET Web Forms アプリの場合、ブラウザからページを呼び出す際、url に https://.../default.aspx というように目的のページのファイル名を拡張子を付けて指定する必要があります。
FriendlyUrls を利用すると、一番最初にブラウザから拡張子 .aspx 付きで要求した場合は、デフォルトのリダイレクトモードの設定 Permanent では 301 Moved Permanently 応答が返ってきて、拡張子 .aspx 無しの url にリダイレクトされます。
下の Fiddler の画像の #9 を見てください。青枠で示したように Default.aspx を要求すると、赤枠で示したように 301 Moved Permanently 応答が返ってきて Default にリダイレクト指示が出ています。
ブラウザはリダイレクト指示を受けて Default を要求します。上の Fiddler の画像の #14 がそれです。Default を要求されたサーバー側では、FriendlyUrls がそれを Default.aspx にルーティングしてくれる���で、Default.aspx が応答として返されます。
リダイレクトの際の HTTP 応答は 301 Moved Permanently なので、ブラウザは Default.aspx ⇒ Default に恒久的に移ったという情報をキャッシュします。なので2 回目以降は、たとえブラウザのアドレスバーに Default.aspx と入力して要求をかけても、ブラウザからは Default という url で要求が出ます。
ページの静的メソッド
ページの静的メソッドとは、ASP.NET Web Forms のページ (.aspx.cs) に WebMethodAttribute 属性を付与して配置した public static メソッドで、AJAX を利用してクライアントスクリプトから呼び出すことができるものです。
jQuery ajax や fetch 等を使ったクライアントスクリプトで JSON 形式のデータを送信して JSON 形式の応答を受けるという Web API 的な使い方ができるので、ASP.NET Web Forms アプリでは利用価値は高いと思います。
詳しくは、先の記事「ASP.NET AJAX でページの静的メソッド呼び出し」を見てください。
FriendlyUrls のリダイレクトモード
リダイレクトモードは Parmanent, Temporary, Off のいずれかに設定でき、設定によって以下のように動きが異なります。
Permanent の場合、ブラウザから Default.aspx というように拡張子 .aspx 付きで要求を受けたときは 301 Moved Permanently 応答が返され、Default にリダイレクトされるようになっています (Temporary の場合は 302 Found 応答でリダイレクトされます)。
Off の場合はリダイレクトされません。Default.aspx で要求を受けるとそのまま Default.aspx が返されます。Default で要求を受けると、FriendlyUrls によって Default.aspx にルーティングされ、Default.aspx が返されます。
リダイレクトモードが Parmanent の時の動作
リダイレクトモードが Parmanent で、以下のように jQuery ajax を使って WebForm1.aspx ページにある静的メソッド MyWebMethod を呼び出したとします。
function CallWebMthod(productId) {
$.ajax({
type: "POST",
url: "/WebForm1.aspx/MyWebMethod",
contentType: "application/json; charset=utf-8",
data: `{ "id": ${productId} }`
}).done(response => {
// ・・・中略・・・
});
}
その応答を Fiddler で応答を見ると以下の通りとなっていました。これはリダイレクトモードが Temporary の場合も同じです。
要求が WebForm1.aspx と拡張子 .aspx が付いているのでリダイレクト応答を返す動きになるはずですが、要求を受けてリダイレクト応答を返すまでのプロセスのどこかで 401 Unauthorized となり (赤枠部分)、認証プロセスに入ろうとしたが失敗したのでその旨応答を返した (青枠部分) ということのように見えます。プロセスのどこでどういう理由でそうなるかなど詳しいことは分かりません。
ちなみに、上の画像の青枠部分の JSON 文字列は、jQuery ajax のコードの変数 response に、JavaScript オブジェクトにデシリアライズした形で受け取ることができます。
リダイレクトモードが Off の時の動作
FriendlyUrls が有効なまま静的メソッド MyWebMethod を呼び出せるようにするには、上に書いたように RoutConfig.cs のコードのリダイレクトモードを Off に変更してやります。
そうすれば上の jQuery のコードの /WebForm1.aspx/MyWebMethod のように拡張子 .aspx が付いた url の要求を受けてもリダイレクト応答を返すためのプロセスが動くことはなく、即 WebForm1.aspx ページの静的メソッド MyWebMethod が呼ばれて期待通り MyWebMethod の応答が返ってきます。
注: url: "/WebForm1/MyWebMethod" とすると (.aspx 拡張子を削除すると)、リダイレクトモード Permanent, Temporary, Off いずれの設定でも WebForm1.aspx ページ本体が応答として返ってきます。それは FriendlyUrls により WebForm1 は WebForm1.aspx にルーティングされ、MyWebMethod はサーバーに渡すパラメータと解釈され、WebForm1.aspx ページ本体が返すべきリソースと判断されるからだと思います。
サンプルコード
この記事を書くにあたって検証用に作成した WebForm1.aspx.cs、WebForm1.aspx のコードを載せておきます。以下の画像が実行結果で、LinkButton クリックで jQuery ajax を使って静的メソッド MyWebMethod を呼び出し、その応答を GridView の下に表示しています。
WebForm1.aspx.cs
using System;
using System.Web.Services;
namespace WebForms3
{
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
[WebMethod]
public static string MyWebMethod(int id)
{
return $"WebMethod called with id={id}";
}
protected string SetOnClientClick(int id)
{
return $"CallWebMthod({id}); return false;";
}
}
}
WebForm1.aspx
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="WebForm1.aspx.cs"
Inherits="WebForms3.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="Scripts/jquery-3.7.0.js"></script>
<script type="text/javascript">
function CallWebMthod(productId) {
$.ajax({
type: "POST",
url: "/WebForm1.aspx/MyWebMethod",
contentType: "application/json; charset=utf-8",
data: `{ "id": ${productId} }`
}).done(response => {
$("#result").empty;
// .NET 3.5 で追加された d パラメータの処置。
if (response.hasOwnProperty('d')) {
$("#result").text(response.d);
}
// リダイレクトモードが Parmanent の時、上の url
// を要求するとサーバー内で 401 エラーとなって下
// の応答が返ってくる:
// {"Message":"認証に失敗しました。" ...}
if (response.hasOwnProperty('Message')) {
$("#result").text(response.Message);
}
});
}
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:SqlDataSource ID="SqlDataSource1"
runat="server"
ConnectionString="<%$ ConnectionStrings:NORTHWINDConnectionString %>"
SelectCommand="SELECT TOP(10) [ProductID], [ProductName] FROM [Products]">
</asp:SqlDataSource>
<asp:GridView ID="GridView1"
runat="server"
AutoGenerateColumns="False"
DataKeyNames="ProductID"
DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="ProductID"
HeaderText="ProductID"
InsertVisible="False"
ReadOnly="True"
SortExpression="ProductID" />
<asp:BoundField DataField="ProductName"
HeaderText="ProductName"
SortExpression="ProductName" />
<asp:TemplateField HeaderText="Click to call WebMethod">
<ItemTemplate>
<asp:LinkButton ID="LinkButton1"
runat="server"
OnClientClick='<%# SetOnClientClick((int)Eval("ProductID")) %>'>
LinkButton
</asp:LinkButton>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<div id="result"></div>
</form>
</body>
</html>