WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

User-Agent Client Hints

by WebSurfer 2023年4月21日 15:27

Windows OS のブラウザから、要求ヘッダに含まれて Web サーバーに送信されてくる従来の User-Agent では、OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができません。

しかし、Microsoft のドキュメント「User-Agent クライアント ヒントを使用してWindows 11と CPU アーキテクチャを検出する」によると、User-Agent Client Hints を利用すればサーバー側で OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができるとのことなので検証してみました。

なお、ドキュメントには書いてありませんが、HTTPS 通信に限ると言うところに注意してください。また、現時点では実験的な機能であり、対応ブラウザも Edge, Chrome, Opera に限られている点にも注意してください。(参考 : ユーザーエージェントクライアントヒント API

ブラウザはデフォルトで要求ヘッダに User-Agent Client Hints 関係の情報 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform を含めます (HTTPS 通信の場合に限ります)。Windows 10 22H2 の Microsoft Edge 112.0.1722.48 の場合以下の通りとなります。

sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

上の情報からは OS が Windows だというのは分かりますがバージョンは分かりません。バージョン情報もブラウザが送信するようにするには、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version

を含めます。そうすると、次の要求からブラウザが要求ヘッダに、

sec-ch-ua-platform-version: "10.0.0"

という OS のバージョン情報を含めて送信してくれるようになります。 (上の例で OS は Windows 10 22H2)

ただし、ブラウザからの最初の要求にはバージョン情報は含まれず、次の要求からになります。そこが使いにくい点かもしれません。

そこが問題であれば、上に紹介した Microsoft のドキュメントの「検出パフォーマンスの最適化 Critical-CH」セクションに書いてあるように、Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めてクライアント(ブラウザ)に送るのが良さそうです。

具体的には、例えば、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version
Critical-CH: Sec-CH-UA-Platform-Version 

を含めると、ブラウザは最初の応答を受けた後、直ちに再度要求を出し、その要求ヘッダには Critical-CH ヘッダーに指定された情報(この例では sec-ch-ua-platform-version: "10.0.0")を含めて送信してくれます。

以降は、ユーザーがブラウザの「Cookie およびその他のサイト データ」を消去しない限り、ブラウザは指定された情報を送信し続けます。

また、再度 Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めて送信しても 2 回要求が出るということはありません。

以下に、Fiddler で要求・応答をキャプチャした画像を貼って説明を加えておきます。

(1) 最初の要求

(1) 最初の要求

Default.aspx.cs の Page_Load に以下の C# コードを実装して要求をかけたものです。

Response.AppendHeader("Accept-CH", "Sec-CH-UA-Platform-Version");
Response.AppendHeader("Critical-CH", "Sec-CH-UA-Platform-Version");

画像の青枠部分がブラウザからの要求ヘッダに含まれる User-Agent Client Hints 情報です。この時点では、バージョン情報 Sec-CH-UA-Platform-Version は含まれていません。

画像の赤枠部分に示した通り、Web サーバーからの応答ヘッダには上の C# コードで指定した Accept-CH と Critical-CH が設定されています。

(2) 自動的に再度要求が出る

(2) 自動的に再度要求が出る

ブラウザは Web サーバーからの応答ヘッダの Accept-CH と Critical-CH を見て自動的に再度 Default.aspx に要求を出します。

画像の青枠の通り要求ヘッダにバージョン情報 sec-ch-ua-platform-version が含まれています。

(3) 他のページを要求

(3) 他のページを要求

Web サーバーからの応答ヘッダに Accept-CH, Critical-CH を含まない他のページ(この例では Contact.aspx)を要求してみます。

画像の青枠の通り要求ヘッダに sec-ch-ua-platform-version が含まれています。

(4) 再度 Default.aspx を要求

(4) 再度 Default.aspx を要求

ブラウザから再度 Default.aspx を要求してみます。画像の赤枠の通り Web サーバーからの応答ヘッダには Accept-CH と Critical-CH が含まれていますが、、上の (2) のように再度要求が出ることはありません。


ASP.NET の C# のコードでブラウザからの要求ヘッダに含まれる Sec-CH-UA-Platform および Sec-CH-UA-Platform-Version の情報を取得するには以下のようにします。

string platform = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM"];
string version = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM_VERSION"];

上のコードで、platform, version に取得された文字列はダブルクォーテーション " で囲まれるので注意してください。

Tags: ,

ASP.NET

ユーザー対話モード

by WebSurfer 2022年9月18日 13:58

IIS でホストされる ASP.NET Web アプリはユーザー対話モードでは動かないという話を書きます。

Environment.UserInteractive

ASP.NET Web アプリを、開発環境で Visual Studio から実行して IIS Express 上で動かすと期待通り動くが、運用環境で IIS にデプロイすると動かないという話を耳にします。

その原因のほどんどは、(1) ワーカープロセスのアクセス権の違い、(2) プロセスがユーザー対話モードで動いているか否かです。(加えて、Session 0 分離による制約もあります。詳しくは、Microsoft の文書 Impact of Session 0 Isolation on Services and Drivers in Windows を見てください)

開発環境の時は、PC にログインしたアカウントで Visual Studio を起動しているので、(1) は PC にログインしたユーザーアカウントの権限、(2) はユーザー対話モードになっています。

運用環境で IIS にデプロイした時は、(1) はデフォルトでは Network Service または AppPoolIdentity という権限が低いアカウント、(2) は非ユーザー対話モードです。

ユーザー対話モードにならないということは、ユーザーが対話するためのグラフィカルユーザーインターフェイスが存在しないということで、モーダルダイアログやメッセージボックスは表示できません。

ワーカープロセスのアカウントは WindowsIdentity.GetCurrent メソッドで、ユーザー対話モードで実行されているか否かは Environment.UserInteractive プロパティで調べることができます。上の画像の赤枠部分が Windows 10 の IIS 10 でホストされる ASP.NET Web Forms アプリで調べた結果です。

上記 (1) の権限の問題は、Network Service または AppPoolIdentity に必要な権限を与えるとか、権限を持つアカウントを偽装するとかで解決します。

または、以下の画像のように IIS Manager を操作して、プロセスモデルの ID に必要な権限を持つユーザーアカウントを設定することでも解決できます。

ワーカープロセスのアカウント変更

しかし、(2) のユーザー対話モードについてはそれでは何ともなりません。ユーザー対話モードにする手段は少なくとも自分が調べた限りでは無かったです。

実は、上の画像のようにプロセスモデルの ID に管理者権限を持つユーザーアカウントを設定すれば Environment.UserInteractive が True(=ユーザー対話モード)になると思い違いしていました。

昔の Teratail のスレッド「WebBrowserの動作に影響するPC環境の相違点」に回答する際調べたことなのですが、すっかり忘れていたので備忘録として残しておくことにした次第です。

Tags: , ,

ASP.NET

SignalR と SqlDependency

by WebSurfer 2021年12月26日 13:38

.NET Framework 4.8 の ASP.NET Web アプリで、SqlDependency クラスを使って SQL Server のデータが更新されたときのクエリ通知を受け取れるように設定し、通知を受け取ったら更新後のデータを SQL Server から取得して、接続されている全クライアントに ASP.NET SignalR を使ってリアルタイムに配信する方法を書きます。

SignalR と SqlDependency

参考にしたのは Microsoft の「チュートリアル: SignalR 2 を使用したサーバーブロードキャスト」です。SqlDependecy 関係については Code Project の ASP.NET MVC 5 SignalR, SqlDependency and EntityFramework 6 も参考にしました。

Code Project からダウンロードできるサンプルコードは、サンプルデータベースとテーブルを作成後、接続文字列を自分の環境に合わせて変更すれば動きます。しかし、(1) 全クライアントが Entity Framework を使って直接 SQL Server にデータを取得に行く、(2) クエリ通知のサブスクリプションを設定するのに必要な情報を DbContext から取得している・・・という点が冗長だと思いました。

なので、それらを変更して (1) クエリ通知のサブスクリプションを設定するのと同時に SQL Server からデータを取得できるので (SELECT クエリを投げるのはその一回で済みます)、それをサーバーに保持しておいて Hub からクライアントに配信する、(2) サブスクリプション設定に必要な情報は接続文字列と SELECT クエリだけなので DbContext から取得という面倒なことは止めて直接コードに記述する・・・というように変更しました。

以下にアプリの作成手順を書きます。

(1) サンプルデータベースとテーブルの作成

サンプルデータベースとテーブルを SQL Server に作成します。この記事で使用した SQL Server は開発マシンの Windows 10 Pro 64-bit にインストールした SQL Server 2012 Express です。

クエリ通知はサービスブローカを使用するため、データベースに対して以下の要件がありますので注意してください。(詳しくは Microsoft のドキュメント「ADO.NET 2.0 のクエリ通知」参照)

  1. 通知クエリが実行されるデータベースでサービスブローカが有効になっている必要があります。
  2. クエリを送信するユーザーには、クエリ通知にサブスクライブするための権限が必要です。

まず SQL Server Management Studio を使って SqlDependency という名前 (名前は任意) でデータベースを作成します。上に書いた要件 1 に従ってオプションの[Broker が有効]を True に設定しましたが、それ以外はデフォルトのままです。

新しいデータベースの作成

次に Products という名前 (名前は任意) でテーブルを作成します。これも下の画像の通り SQL Server Management Studio で行いました。

Products テーブルの作成

作成したテーブルをスクリプト化すると以下の通りとなります。

USE [SqlDependency]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Products](
	[ProductID] [int] IDENTITY(1,1) NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[UnitPrice] [decimal](18, 2) NOT NULL,
	[Quantity] [int] NOT NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
	[ProductID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
       IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
       ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

(2) ASP.NET プロジェクトの作成

Visual Studio 2022 のテンプレートを使ってフレームワーク .NET Framework 4.8 で ASP.NET プロジェクトを作成します。この記事では アプリは MVC を選んで認証は「なし」としておきました (MVC である必要はなく SignalR v2 が動けば OK です)。

ASP.NET プロジェクトの作成

(3) Product クラスの作成

Models フォルダに Product.cs という名前のクラスファイルを作成し、自動生成されたコードを以下のように書き換えます。

namespace SqlDependencySignalR2.Models
{
    public class Product
    {
        public int ProductID { get; set; }
        public string Name { get; set; }
        public decimal UnitPrice { get; set; }
        public int Quantity { get; set; }
    }
}

Product クラスは Model-View-Controller (MVC) の Model とは異なり、サーバーで SQL Server のデータを保持するのと Hub からクライアントへデータを渡すために使います。

具体的には、クエリ通知のサブスクリプションを設定するのと同時に SQL Server からデータを取得し List<Product> オブジェクトとしてサーバー側に保持しておきます。それを Hub からクライアントに送信します。その際、サーバー側での List<Product> オブジェクトの JSON 文字列へのシリアライズ、クライアント側で受け取った JSON 文字列の JavaScript オブジェクトへのデシリアライズは SignalR のフレームワークがやってくれます。

(4) 接続文字列の設定

web.config に接続文字列の設定を追加します。ADO.NET の SqlConnection, SqlCommand, SqlDataReader を使ってデータを取得しますので、そのために有効な接続文字列としてください。また、上に書いた要件 2 の「クエリを送信するユーザーには、クエリ通知にサブスクライブするための権限が必要です」に注意してください。

<connectionStrings>
  <add name="ProductConnection" 
     connectionString="data source=(local)\sqlexpress;initial catalog=SqlDependency;integrated security=True" 
     providerName="System.Data.SqlClient" />
</connectionStrings>

この記事では Visual Studio 2022 を管理者権限で立ち上げて IIS Express 上で実行して検証しています。その管理者は SQL Server のログインに設定してありサーバーロールは sysadmin を持っています。上の接続文字列の例では Windows 認証を設定していますので SQL Server には sysadmin サーバーロールでログインしますので権限の問題が避け られていますが、実環境ではそうはできない点に注意してください。

(5) SignalR Hub を追加

ソリューションエクスプローラーでプロジェクトルートを右クリックして[追加(D)]⇒[新しい項目(W)...]で「SignalR Hub クラス (v2)」を選んで ProductHub.cs という名前 で追加します。

SignalR Hub クラスの作成

自動生成されたコードの内容を以下のように書き換えます。下のコードで参照している Notifier クラスは下のステップ (7) で定義します。

using Microsoft.AspNet.SignalR;
using System.Collections.Generic;
using SqlDependencySignalR2.SqlDependencyNotifier;
using SqlDependencySignalR2.Models;

namespace SqlDependencySignalR2
{
    public class ProductHub : Hub
    {
        // Notifier インスタンスへの参照を保持する
        private readonly Notifier _notifier;

        public ProductHub() : this(Notifier.Instance) { }

        public ProductHub(Notifier notifier)
        {
            _notifier = notifier;
        }

        // 初期画面のデータをクライアントが取得する時に
        // JavaScript でこのメソッドを呼び出す
        public IEnumerable<Product> GetAllProducts()
        {
            return _notifier.GetAllProducts();
        }
    }
}

(6) Startup クラスの追加

ソリューションエクスプローラーでプロジェクトルートを右クリックして[追加(D)]⇒[新しい項目(W)...]で Startup.cs という名前のクラスファイルを追加します。 内容は以下のようにします。

using Owin;
using Microsoft.Owin;

// アプリの起動時にハブにマップする。SignalR 2 では、OWIN startup
// クラスを追加するとマッピングが作成される
[assembly: OwinStartup(typeof(SqlDependencySignalR2.Startup))]
namespace SqlDependencySignalR2
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // OWIN startup クラスは、アプリが Configuration メソッド
            // を実行するときに MapSignalR を呼び出す。OwinStartup
            // assembly 属性を使用して OWIN のスタートアッププロセス
            // にクラスを追加する
            app.MapSignalR();
        }
    }
}

(7) Notifier クラスの実装

プロジェクトルート直下に SqlDependencyNotifier という名前のフォルダを設けて、その中に上のステップ (5) の Hub が使用する Notifier クラスを作成します。

クエリ通知のサブスクリプションを設定しているのが RegisterForNotifications メソッドです。ADO.NET の SqlConnection, SqlCommand, SqlDataReader を使って SELECT クエリを発行しデータを取得してキャッシュするのと同時に、通知のサブスクリプションの設定と、通知によって発生する SqlDependency.OnChange イベントで必要な処理を行うためイベントハンドラを設定しています。

Microsoft のドキュメント「クエリ通知を使用するときの特別な注意事項 (ADO.NET)」に書いてありますようにいろいろ制約があるので注意してください。

特に自分がハマったのが SELECT クエリのテーブル���です。ドキュメントには "テーブル名は 2 つの部分から構成される名前で修飾する必要があります" と書いてありますが、それは dbo.Products のようにする必要があると言うことです。SqlDependency.dbo.Products でも Products でもダメで、通知のサブスクリプションに失敗します。

ちなみに、失敗すると即 SqlDependency.OnChange イベントが発生し、RegisterForNotifications メソッドが実行されるので無限ループに陥ってしまいます。それを避けるためイベントハンドラの引数 SqlNotificationEventArgs をチェックして期待する結果と違っていたら何もしないようにしました。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Microsoft.AspNet.SignalR;
using System.Web.Configuration;
using SqlDependencySignalR2.Models;

namespace SqlDependencySignalR2.SqlDependencyNotifier
{
    public class Notifier
    {
        // シングルトンとなるよう Lazy<T> クラスを使用
        private static readonly Lazy<Notifier> _instance
            = new Lazy<Notifier>(() => new Notifier());

        private readonly IHubContext _hubContext;
        private readonly string _connString;
        private readonly string _sqlQuery;
        private List<Product> _products;

        // コンストラクタ
        private Notifier()
        {
            // SignalR コンテキストを保持
            _hubContext = GlobalHost
                          .ConnectionManager
                          .GetHubContext<ProductHub>();

            // 接続文字列
            _connString = WebConfigurationManager
                          .ConnectionStrings["ProductConnection"]
                          .ConnectionString;
            
            // SELECT クエリ。テーブル名は dbo.Products とすること。
            // SqlDependency.dbo.Products でも Products でもダメで、
            // 通知のサブスクリプションに失敗する
            _sqlQuery = "SELECT ProductID,Name,UnitPrice,Quantity" +
                        " FROM dbo.Products";

            // クエリ通知のサブスクリプションを設定するのと同時に SQL
            // Server からデータを取得し List<Product> オブジェクトと
            // してサーバーに保持しておく
            _products = RegisterForNotifications();
        }

        public static Notifier Instance
        {
            get { return _instance.Value; }
        }

        // Hub の GetAllProducts メソッドから呼ばれる。保持している
        // List<Product> オブジェクトを返す
        public IEnumerable<Product> GetAllProducts()
        {
            return _products;
        }

        // クエリ通知のサブスクリプションを設定するのと同時に SQL
        // Server からデータを取得し List<Product> として返す
        private List<Product> RegisterForNotifications()
        {
            var products = new List<Product>();
            using (var connection = new SqlConnection(_connString))
            using (var command = new SqlCommand(_sqlQuery, connection))
            {                
                var sqlDependency = new SqlDependency(command);

                // イベントハンドラの設定
                sqlDependency.OnChange += OnSqlDependencyChange;

                if (connection.State == ConnectionState.Closed)
                {
                    connection.Open();
                }

                // ExecuteReader でクエリ通知のサブスクリプションが設定
                // される。同時に SqlDataReader でデータを取得できる
                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        var product = new Product
                        {
                            ProductID = reader.GetInt32(0),
                            Name = reader.GetString(1),
                            UnitPrice = reader.GetDecimal(2),
                            Quantity = reader.GetInt32(3)
                        };
                        products.Add(product);
                    }
                }
            }
            return products;
        }

        // Products テーブルが更新されるとこのイベントハンドラに制御が
        // 飛んでくる
        private void OnSqlDependencyChange(object sender, 
                                           SqlNotificationEventArgs e)
        {
            // 引数 e が期待する結果と違っていたら何もしない
            if ((e.Info == SqlNotificationInfo.Insert ||
                e.Info == SqlNotificationInfo.Update ||
                e.Info == SqlNotificationInfo.Delete) &&
                e.Source == SqlNotificationSource.Data &&
                e.Type == SqlNotificationType.Change)
            {
                // 一度通知が行われるとサブスクリプションが解除されてしま
                // うので、以下のメソッドで再度設定するとともに更新後の
                // データを _products に取得する
                _products = RegisterForNotifications();

                // 更新後のデータを接続されている全クライアントに送信
                _hubContext.Clients.All.broadcastMessage(_products);
            }
        }
    }
}

(8) SqlDependency.Start / Stop の設定

Global.asax.cs に、接続文字列で指定される SQL Server のインスタンスから依存関係の変更通知を受け取るリスナの開始 / 停止を設定します。

using System;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using System.Web.Configuration;
using System.Data.SqlClient;

namespace SqlDependencySignalR2
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected String SqlConnectionString { get; set; }

        protected void Application_Start()
        {
            // この下の 4 行は自動生成された既存のコード
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            SqlConnectionString = WebConfigurationManager
                                  .ConnectionStrings["ProductConnection"]
                                  .ConnectionString;

            if (!String.IsNullOrEmpty(SqlConnectionString))
            {
                SqlDependency.Start(SqlConnectionString);
            }
        }

        protected void Application_End()
        {
            if (!String.IsNullOrEmpty(SqlConnectionString))
            {
                SqlDependency.Stop(SqlConnectionString);
            }
        }
    }

(9) 表示画面の作成

表示画面は、この記事では Controller / View を使いましたが、静的な html ページで作っても良いです。ただし、ブラウザにキャッシュされるので内容を変更するたびキャッシュを削除するのが面倒ですが。

自動生成されている HomeController に Product という名前のアクションメソッドを追加します。

public ActionResult Product()
{
    return View();
}

上の Product アクションメソッドを右クリックして表示されるダイアログで以下のように設定し、アクションメソッドに対応するView を自動生成させます。

View の作成

自動生成されたコードを以下のように書き換えます。

@{
    ViewBag.Title = "Product";
}

<h2>SignalR and SqlDependency Sample</h2>

<div id="pruductTable">
    <table border="1">
        <thead>
            <tr><th>ProductID</th><th>Name</th><th>UnitPrice</th><th>Quantity</th></tr>
        </thead>
        <tbody id="productbody">
            <tr class="loading"><td colspan="4">loading...</td></tr>
        </tbody>
    </table>
</div>

@section scripts {
    <!--jQuery.js は _Layout.cshtml で参照済み -->

    <!--SignalR ライブラリの参照 -->
    <script src="~/Scripts/jquery.signalR-2.2.2.min.js"></script>

    <!--サーバー側で自動生成されるプロキシの JavaScript コードを取得する -->
    <script src="~/signalr/hubs"></script>

    <script type="text/javascript">
        var signalRHubInitialized = false;

        $(function () {
            InitializeSignalRHubStore();
        });

        function InitializeSignalRHubStore() {

            if (signalRHubInitialized) {
                return;
            }

            try {
                // ハブ用に自動生成されたプロキシへの参照を作成
                var productHub = $.connection.productHub;

                // SqlDependency.OnChange イベントが発生すると呼び出される
                productHub.client.broadcastMessage = function (products) {
                    $('#productbody').empty();
                    $.each(products, function (index, product) {
                        $('#productbody').append(
                            '<tr><td>' + product.ProductID +
                            '</td><td>' + product.Name +
                            '</td><td>' + product.UnitPrice +
                            '</td ><td>' + product.Quantity +
                            '</td></tr >');
                    });
                };

                // start() で Hub への接続を開始。.done でクライアントから Hub の
                // パブリックメソッド GetAllProducts を呼び出す
                $.connection.hub.start().done(function () {
                    productHub.server.getAllProducts().done(function (products) {
                        $('#productbody').empty();
                        $.each(products, function (index, product) {
                            $('#productbody').append(
                                '<tr><td>' + product.ProductID +
                                '</td><td>' + product.Name +
                                '</td><td>' + product.UnitPrice +
                                '</td ><td>' + product.Quantity +
                                '</td></tr >');
                        });
                    });
                    signalRHubInitialized = true;
                });

            } catch (err) {
                signalRHubInitialized = false;
            }
        };
    </script>
}

上の html コードの table 要素内の tbody 要素をサーバーから送られてきたデータで書き換えるようにしています。

まず、初期画面が表示されると Hub への接続が開始され、接続が完了すると Hub の GetAllProducts が呼び出されサーバ側で保持されている List<Product> がクライアントに送信され、それが上の function (products) の引数に JavaScript オブジェクトとして渡されます。その products を使って tbody 要素を書き換えて初期データを表示します。

その後、SQL Server の Products テーブルが更新されるとサーバー側で SqlDependency.OnChange イベントが発生し、上の productHub.client.broadcastMessage に設定された function (products) が呼び出されます。その際、引数 products には更新後のデータを含 む JavaScript オブジェクトが渡され、それを使って tbody 要素を書き換えて更新後のデータを表示します。

アプリを実行して複数のブラウザでアクセスし、SQL Sever Management Studio などを使って Products テーブルを更新すると、更新結果がリアルタイムで接 続されているすべてのブラウザに反映されます。それを表示したのがこの記事の一番上の画像です。

Tags: , , ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年5月  >>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar