WebSurfer's Home

Filter by APML

FriendlyUrls を有効にしておくとページの静的メソッドが呼び出せない

by WebSurfer 27. September 2024 13:50

Visual Studio 2019 / 2022 のテンプレートを使って作成する Web Forms アプリのプロジェクトでは、Microsoft.AspNet.FriendlyUrls という NuGet パッケージがインストールされ、デフォルトで有効になるように設定されていますが、デフォルトの設定のままではクライアントスクリプトでページの静的メソッドが呼び出せないという話を書きます。

FriendlyUrls NuGet パッケージ

いろいろ説明すると長くなるのでまず最初に解決策を書いておきます。

自動生成されて App_Start フォルダに配置されている RouteConfig.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 で呼び出せるようにしたい、かつクライアントスクリプトでページの静的メソッドを呼び出せるようにしたいのであれば、他に方法はなさそうです。


以下にどういうメカニズムになっているかなどの説明を書いておきます。興味があれば読んでください。

(1) 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 にリダイレクト指示が出ています。

FriendlyUrls NuGet パッケージ

ブラウザはリダイレクト指示を受けて Default を要求します。上の Fiddler の画像の #14 がそれです。Default を要求されたサーバー側では、FriendlyUrls がそれを Default.aspx にルーティングしてくれるので、Default.aspx が応答として返されます。

リダイレクトの際の HTTP 応答は 301 Moved Permanently なので、ブラウザは Default.aspx ⇒ Default に恒久的に移ったという情報をキャッシュします。なので2 回目以降は、たとえブラウザのアドレスバーに Default.aspx と入力して要求をかけても、ブラウザからは Default という url で要求が出ます。

(2) ページの静的メソッドとは何か

ページの静的メソッドとは、ASP.NET Web Forms のページ (.aspx.cs) に WebMethodAttribute 属性を付与して配置した public static メソッドで、AJAX を利用してクライアントスクリプトから呼び出すことができるものです。

jQuery ajax や fetch 等を使ったクライアントスクリプトで JSON 形式のデータを送信して JSON 形式の応答を受けるという Web API 的な使い方ができるので、ASP.NET Web Forms アプリでは利用価値は高いと思います。

詳しくは、先の記事「ASP.NET AJAX でページの静的メソッド呼び出し」を見てください。

(3) FriendlyUrls のリダイレクトモード

リダイレクトモードは Permanent, Temporary, Off のいずれかに設定でき、設定によって以下のように動きが異なります。

  • Permanent の場合、ブラウザから Default.aspx というように拡張子 .aspx 付きで要求を受けたときは 301 Moved Permanently 応答が返され、Default にリダイレクトされるようになっています。
  • Temporary の場合は 302 Found 応答でリダイレクトされる以外は Permanent と同じになります。
  • Off の場合はリダイレクトされません。Default.aspx で要求を受けるとそのまま Default.aspx が返されます。Default で要求を受けると、FriendlyUrls によって Default.aspx にルーティングされ、Default.aspx が返されます。

(4) リダイレクトモードが Permanent / Temporary の時の動作

リダイレクトモードが Permanent で、以下のように 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 の場合も同じです。

Fiddler で見た応答

要求が WebForm1.aspx と拡張子 .aspx が付いているのでリダイレクト応答を返す動きになるはずですが、要求を受けてリダイレクト応答を返すまでのプロセスのどこかで 401 Unauthorized となり (赤枠部分)、認証プロセスに入ろうとしたが失敗したのでその旨応答を返した (青枠部分) ということのように見えます。プロセスのどこでどういう理由でそうなるかなど詳しいことは分かりません。

ちなみに、上の画像の青枠部分の JSON 文字列は、jQuery ajax のコードの変数 response に、JavaScript オブジェクトにデシリアライズした形で受け取ることができます。

(5) リダイレクトモードが 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 ページ本体が返すべきリソースと判断されるからだと思います。

(6) 検証に使ったサンプルコード

この記事を書くにあたって検証用に作成した WebForm1.aspx.cs、WebForm1.aspx のコードを載せておきます。以下の画像が実行結果で、LinkButton クリックで jQuery ajax を使って静的メソッド MyWebMethod を呼び出し、その応答を GridView の下に表示しています。

WebMethod の呼び出し

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>

Tags: , , , ,

ASP.NET

ListView に thead と tbody 追加

by WebSurfer 1. April 2024 13:11

ASP.NET Web Forms アプリ用の ListView コントロールからレンダリングされる html ソースの table 要素内に thead, tbody 要素を追加する方法を書きます。

Visual Studio のデザイナを使って ListView のテーブル形式のコードを自動生成させると aspx ファイルに以下のような LayoutTemplate が生成されます。まず、それに赤枠と青枠で示したような thead, tbody 要素を追記します。

aspx ファイル

さらに、自動生成されたコードでは親の table 要素に runat="server" 属性が付与されているのでそれを削除する必要があります。上の画像のコメントを見てください。

runat="server" 属性が付与されているとその要素はサーバーコントロールになります。親の table 要素がサーバーコントロールになると、ASP.NET が html ソースをレンダリングする際に追加した thead, tbody 要素は削除されてしまいますので。

結果の html ソースは以下のようになります。

結果の html ソース

runat="server" 属性を削除するとサーバー側のコードでその要素を操作(例えばプロパティを動的に設定するなど)できなくなりますが、それ以外の不都合はないと思います。

GridView の例は別の記事「GridView と thead, tbody, tfoot」に書きましたのでそちらを見てください。

Tags: , ,

ASP.NET

文字列の長さ制限、三点リーダー表示

by WebSurfer 23. June 2023 13:52

先の記事「GridView に overflow 適用、三点リーダー表示」で、CSS の overflow: hidden と text-overflow: ellipsis を使って文字列の長さ制限するとともに三点リーダーを表示する例を紹介しましたが、文字列の中にエスケープされた文字、プロポーショナルフォント、サロゲートペア、絵文字などが含まれる場合どうなるかという話を書きます。

文字列の長さ制限、三点リーダー表示

上の画像の下側の表の description 列の各行が、文字列の長さを 320px に制限するとともに末尾を三点リーダーで表示したものです。具体的にどのようにしたかと言うと、上側の表の description 列と同じ文字列を div 要素に入れて、その div 要素に以下の CSS を適用しました。ブラウザは Chrome、フォントはメイリオ、サイズは 16px です。

<style type="text/css">
    div.style1
    {
        width: 320px;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
</style>

1 行目にある < > & という文字は、実際は &lt; &gt; &amp; というエスケープされた文字列が、ブラウザ上では < > & と表示されたものです。

4 行目の絵文字 👨‍🌾 は、👨 と 🌾 を ZeroWidthJoinerで連結したもので、Unicode で言うと U+1F468 U+200D U+1F33E となっています。

文字はすべて ASP.NET により、コードビハインドでは UTF-16 のコードがそのまま UTF-8 に変換されてブラウザに送信されます。

(コードビハインドで扱われる UFT-16 で言うと、4 行目の絵文字 🍎 と 🍏 はサロゲートペアで、🍎 は 0xD83C 0xDF4E、🍏 は 0xD83C 0xDF4F です。👨‍🌾 は、👨 と 🌾 を ZeroWidthJoiner (0x200D) で連結したもの、つまり、0xD83D 0xDC68 0x200D 0xD83C 0xDF3E となっています。この記事とは直接関係ない話ですが、せっかく調べたので書いておきます)

上の結果から分かるように、各文字の長さやバイト数とは関係なく、ブラウザ上に表示された文字列の長さで制限がかかり、CSS の width: 320px で指定された幅いっぱいに三点リーダを含めて表示されています。

上の画像ではフォントはメイリオ、サイズは 16px ですが、MS Gothic などの等幅フォントを使った場合も、フォントサイズを変えた場合も、ブラウザ上に表示される文字列の長さで制限がかかるのは同じです。

三点リーダーを表示する text-overflow:ellipsis はもともと IE の独自拡張だそうですが、最近は他のブラウザでも取り入れられているようです。Windows 10 で Chrome 114.0.5735.134, Edge 114.0.1823.58, Firefox 114.0.2, Opera 100.0.4815.21 で試してみましたが、同じ結果が得られました。

参考に、上の画像を表示するのに使った ASP.NET Web Forms アプリのコードを載せておきます。

.aspx.cs

using System;
using System.Data;

namespace WebForms1
{
    public partial class WebForm26 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                // データソースとして DataTable を作成。
                DataTable dt = new DataTable();
                DataRow dr;

                dt.Columns.Add(new DataColumn("id", typeof(Int32)));
                dt.Columns.Add(
                    new DataColumn("description", typeof(string)));

                dr = dt.NewRow();
                dr["id"] = 1;
                dr["description"] = "エスケープされた &lt; &gt; &amp; " +
                    "などの文字はどのようになるか?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 2;
                dr["description"] = "Proportional Font WWWWWWWWWWW " +
                    "iiiiiiii llllll などは?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 3;
                dr["description"] = "サロゲートペア 𠀋 𡈽 𠮟 などは" +
                    "どのようになるか?";
                dt.Rows.Add(dr);

                dr = dt.NewRow();
                dr["id"] = 4;
                dr["description"] = "絵文字 🍎 🍏 (サロゲートペア) " +
                    "👨‍🌾 (ZWJ で結合) などは?";
                dt.Rows.Add(dr);

                // 上で作成した DataTable を GridView にバインド。
                GridView1.DataSource = dt;
                GridView1.DataBind();

                GridView2.DataSource = dt;
                GridView2.DataBind();
            }
        }
    }
}

.aspx

<%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="WebForm26.aspx.cs" Inherits="WebForms1.WebForm26" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <style type="text/css">
        body {
            font-family: "メイリオ";
            font-size: 16px;
        }

        div.style1 {
            width: 320px;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <p>制限しない場合 (フォント: メイリオ, 16px)</p>
        <asp:GridView ID="GridView1"
            runat="server"
            AutoGenerateColumns="False">
            <Columns>
                <asp:BoundField DataField="id" HeaderText="id" />
                <asp:TemplateField HeaderText="description">
                    <ItemTemplate>
                        <asp:Literal ID="Literal1"
                            runat="server"
                            Text='<%# Eval("description") %>'>
                        </asp:Literal>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

        <p>overflow:hidden で制限 (フォント: メイリオ, 16px)</p>
        <asp:GridView ID="GridView2"
            runat="server"
            AutoGenerateColumns="False">
            <Columns>
                <asp:BoundField DataField="id" HeaderText="id" />
                <asp:TemplateField HeaderText="description">
                    <ItemTemplate>
                        <div class="style1">
                            <asp:Literal ID="Literal1"
                                runat="server"
                                Text='<%# Eval("description") %>'>
                            </asp:Literal>
                        </div>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>
    </form>
</body>
</html>

Tags: , , , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  April 2025  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar