ASP.NET Web Forms で「保存」ボタンを押したときにローディング(スピナー)を表示したいのに、ほんの一瞬チラッと出るだけですぐ消えてしまう――。UpdatePanel や PageRequestManager、BlockUI を駆使しているつもりなのに思った通り動かず、原因が分からずハマりがちなポイントです。本記事では、この「チラつくだけ」問題の正体と、実務で使える2つの解決パターン(フルポストバック用/UpdatePanel 用)を、コード例つきで詳しく解説します。
現象の整理:保存ボタンクリック時にローディングがチラつくだけ
前提となる環境は以下のようなものを想定します。
- ASP.NET Web Forms(.NET Framework)
- Bootstrap・jQuery・Select2 など複数のフロントエンドライブラリを併用
- 保存ボタン押下時に、BlockUI でオーバーレイ+GIF のローディングを表示したい
- ScriptManager / UpdatePanel / PageRequestManager の beginRequest / endRequest にフックしている
しかし実際には、保存ボタンを押すとローディングが一瞬表示されてすぐ消え、サーバー処理中は何も表示されない、という状態になります。
| 項目 | 期待している動作 | 実際の動作 |
|---|---|---|
| 保存ボタン押下 | 画面全体、または一部に半透明オーバーレイ+スピナー表示 | スピナーが一瞬だけ表示され、その後消える |
| サーバー処理中 | ユーザーはローディングを視認できる | 何も表示されず、画面が固まっているように見える |
| 処理完了後 | オーバーレイが自動的に消え、通常画面へ | 画面は更新されるが、処理中は無反応に見える |
この「一瞬だけ表示される」現象には、ASP.NET Web Forms 特有の仕組みが深く関わっています。
原因の全体像:ライフサイクルの誤解とポストバック種別
この問題の根本原因は、次の2つに集約できます。
- ページライフサイクルの誤解(サーバー処理中に UI を更新できない)
- ポストバックの種類(フルポストバックか UpdatePanel による部分更新か)
まずは、フルポストバックと部分更新の違いを簡単に整理しておきます。
| 項目 | フルポストバック | UpdatePanel(AJAX)部分更新 |
|---|---|---|
| リクエスト先 | サーバーへフォーム全体を送信 | ScriptManager 経由で非同期リクエスト |
| レスポンス | ページ全体が再描画される | UpdatePanel 内の一部だけが書き換えられる |
| PageRequestManager | フルポストバックでは実質意味をなさない | beginRequest / endRequest が有効に働く |
| ローディング表示 | ページ遷移が走るため「チラつき」になりやすい | 同一ページ内での書き換えなのでローディングを維持しやすい |
Web Forms のライフサイクルと「途中経過」が出せない理由
ASP.NET Web Forms は、リクエストごとにサーバーでページを再構築し、その結果をまるごと HTML としてクライアントに返す「ラウンドトリップ」型のフレームワークです。コードビハインドで重い処理をしている間、ブラウザ側では基本的に何も描画が更新されません。
- サーバー側(C#)の処理中に「ローディングを出す」ことはできない
- サーバーから返ってきたレスポンスがブラウザに適用されたタイミングで初めて UI 更新が反映される
そのため、「サーバー処理が走っている最中に見せたい UI」は、ポストバックが始まる前にクライアント側 JavaScript で表示しておく必要があります。サーバー処理の途中で何かする、という発想は Web Forms ではできません。
PageRequestManager が効くのは UpdatePanel の AJAX 部分更新だけ
次に、PageRequestManager についてです。以下のようなコードをよく見かけます。
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_beginRequest(function () {
// ローディング表示
});
prm.add_endRequest(function () {
// ローディング非表示
});
これは ScriptManager / UpdatePanel を利用したAJAX 部分更新(非同期ポストバック)が発生したときにだけ意味を持つ仕組みです。
- ボタンが UpdatePanel の中にない
- ボタンが UpdatePanel のトリガーになっていない
といった場合、クリックはフルポストバックになり、ブラウザはページ全体を再読み込みします。このとき、beginRequest で BlockUI を呼んでも、
- BlockUI がオーバーレイを表示する
- 直後にページ遷移(フルポストバック)で画面がリセットされる
という流れになり、結果としてローディングが一瞬チラついて消えるように見える、というわけです。
解決パターンA:フルポストバックでも確実にローディングを出す
まずは、もっとも単純かつ確実な方法です。ボタンの OnClientClick でローディング表示を出してから、通常どおりフルポストバックさせる、というパターンになります。
フルポストバックではページ全体が再描画されるため、サーバー処理完了後には新しい HTML が読み込まれ、ローディング用要素もリセットされます。そのため、サーバー側で「ローディングを消す」処理を書く必要がありません。
実装例(ASPX + C#)
ローディング用のオーバーレイとボタンを定義します。
<!-- ScriptManager は他で UpdatePanel 等を使っていなければ不要 -->
<asp:ScriptManager runat="server" ID="sm" />
<!-- ローディング用オーバーレイ -->
<div id="loadingOverlay" style="
display:none; position:fixed; inset:0; z-index:9999;
background:rgba(0,0,0,.6); text-align:center;">
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);">
<h4 style="color:#fff; margin-bottom:12px;">処理中です。しばらくお待ちください…</h4>
<img src="/images/Loader_Cube.gif" width="100" height="100" alt="処理中" />
</div>
</div>
<!-- 保存ボタン -->
<asp:Button ID="btnSave" runat="server" Text="保存"
OnClick="btnSave_Click"
OnClientClick="document.getElementById('loadingOverlay').style.display='block'; return true;" />
コードビハインド側では、通常どおりサーバー処理を行うだけです。
protected void btnSave_Click(object sender, EventArgs e)
{
// 実際は DB 更新やファイル出力等の重い処理
System.Threading.Thread.Sleep(2000); // 挙動確認用の遅延
// 処理完了後はページが再描画されるため、
// loadingOverlay は自動的に消える(初期状態に戻る)
}
OnClientClick の書き方のコツ
OnClientClick に直接長い JavaScript を書くと保守性が悪くなるため、小さな関数に切り出しておくのがおすすめです。
<script type="text/javascript">
function showLoading() {
var overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'block';
}
// true を返すことでポストバックを継続させる
return true;
}
</script>
<asp:Button ID="btnSave" runat="server" Text="保存"
OnClick="btnSave_Click"
OnClientClick="return showLoading();" />
- 必ず true を返す(false を返すとポストバックがキャンセルされる)
- overlay 要素が存在しない場合にもエラーにならないようガードを入れる
- バリデーションと組み合わせる場合は、
Page_ClientValidate()との連携を考える
フルポストバック方式のメリット/デメリット
| 項目 | メリット | デメリット |
|---|---|---|
| 実装のシンプルさ | OnClientClick だけで完結し、理解しやすい | ローディング対象の制御がやや粗い(基本は画面全体) |
| ライブラリ依存 | jQuery や BlockUI に依存しないシンプルな構成が可能 | 既存の BlockUI ベースの仕組みとは別実装になる場合がある |
| UX(ユーザー体験) | 「何も起きていない」状態が解消される | 毎回ページ全体が再読み込みされるため、部分更新に比べると体感レスポンスは劣る |
解決パターンB:UpdatePanel を使った AJAX 部分更新で BlockUI を活かす
すでに UpdatePanel や ScriptManager を利用している、あるいは部分更新でリッチな画面を構成したい場合は、ボタンと対象領域を UpdatePanel 内に入れたうえで PageRequestManager にフックする方法が有効です。
重要なポイントは次の2つです。
- ページ内に ScriptManager が存在すること
- 保存ボタンが UpdatePanel の中にある、または UpdatePanel のトリガーとして設定されていること
構成イメージ(役割の整理)
| 要素 | 役割 | 注意点 |
|---|---|---|
| ScriptManager | UpdatePanel を用いた AJAX 通信の中核。PageRequestManager も管理 | ページに 1 つだけ配置。重複配置は厳禁 |
| UpdatePanel | 部分更新する領域を囲むコンテナ | UpdateMode / ChildrenAsTriggers の設定に注意 |
| 保存ボタン | UpdatePanel のトリガーとなるボタン | UpdatePanel 内に置くのがもっともシンプル |
| BlockUI | UpdatePanel 内(または任意の要素)にオーバーレイ+ローディングを表示 | jQuery のバージョンと読み込み順を統一する |
実装例(ASPX + C#)
画面側の構成例は以下のとおりです。ここでは dvtable という div を BlockUI の対象としています。
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<div id="dvtable">
<!-- この中身が部分更新される想定 -->
<asp:Label ID="lblStatus" runat="server" Text="ここに結果を表示します。" />
</div>
<br />
<asp:Button ID="btnSave" runat="server" Text="保存" OnClick="btnSave_Click" />
</ContentTemplate>
</asp:UpdatePanel>
<!-- jQuery と BlockUI(必ず 1 回ずつだけ読み込む) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="https://malsup.github.io/jquery.blockUI.js"></script>
<script type="text/javascript">
(function () {
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_beginRequest(function () {
// 非同期ポストバック開始時に BlockUI を表示
$('#dvtable').block({
message: '<div style="text-align:center;">' +
'<img src="/images/Loader_Cube.gif" width="100" height="100" alt="処理中" />' +
'</div>',
css: {}, // メッセージ領域はデフォルト CSS をオーバーライドしない
overlayCSS: {
backgroundColor: '#000',
opacity: 0.6,
border: '3px solid #63B2EB'
}
});
});
prm.add_endRequest(function () {
// 非同期ポストバック完了時に BlockUI を解除
$('#dvtable').unblock();
});
})();
</script>
コードビハインド側は、部分更新が前提となるだけで特別なことはありません。
protected void btnSave_Click(object sender, EventArgs e)
{
// 重い処理を想定
System.Threading.Thread.Sleep(2000);
lblStatus.Text = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + " に保存しました。";
}
よくあるハマりどころとチェックポイント
- ScriptManager が存在しない
→ PageRequestManager のgetInstance()自体が失敗する(コンソールエラーになっていないか確認)。 - 保存ボタンが UpdatePanel の外にある
→ クリックがフルポストバックになり、BlockUI がチラつくだけになる。 - UpdatePanel のトリガー設定が誤っている
→ UpdateMode=”Conditional” の場合、AsyncPostBackTrigger の設定漏れで意図せずフルポストバックになることがある。 - jQuery のバージョンが複数読み込まれている
→ 3.6.0 と 3.6.4 が両方読み込まれていたりすると、最後に読み込まれた側でプラグインが解決されず、BlockUI が動かない原因に。 - BlockUI の対象要素が見えていない
→#dvtableがdisplay:noneの状態だとオーバーレイが期待どおり出ないことがある。
ライブラリ競合と読み込み順の整理
質問のコードでは、Bootstrap 4/5 が混在していたり、jQuery も 3.6.0 と 3.6.4 が両方読み込まれているケースがありました。これらはイベントハンドラ上書きやプラグイン競合の原因になり、ローディングが「出たり出なかったり」という不安定な挙動を引き起こします。
ライブラリ整理の基本ルール
- jQuery は1 バージョンだけ読み込む
- Bootstrap も1 系統だけ(4 なら 4 だけ、5 なら 5 だけ)に統一する
- Select2 や BlockUI などのプラグインはjQuery 以降に読み込む
- Bootstrap の JavaScript はプラグインのあとに読み込む(ツールチップ等を利用する場合)
推奨される読み込み順はおおよそ次のような形です。
| 順番 | ライブラリ | 例 |
|---|---|---|
| 1 | jQuery 本体 | https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js |
| 2 | jQuery プラグイン | BlockUI、Select2 など |
| 3 | Bootstrap JS | bootstrap.bundle.min.js など |
| 4 | プロジェクト固有のスクリプト | main.js、loading.js など |
デバッグのポイント:なぜまだチラつくのかを切り分ける
実際の現場では、「対策したはずなのにまだチラつく」「特定の画面だけうまくいかない」といった状況も起こりがちです。そのときに役に立つ切り分けポイントをいくつか挙げておきます。
1. フルポストバックか、AJAX ポストバックかを確認する
- ブラウザの開発者ツール(F12)を開き、「ネットワーク」タブで保存ボタンクリック後の通信を確認
- リクエストヘッダーに
X-MicrosoftAjax: Delta=trueが含まれていれば UpdatePanel の AJAX ポストバック - 逆にそれがなければフルポストバックになっている可能性が高い
2. JavaScript エラーが出ていないかを確認する
- コンソールタブに赤いエラーが出ていないか確認
Sys is undefinedやPageRequestManager is undefinedが出ている場合は ScriptManager の配置や読み込み順を疑う$.blockUI is not a functionの場合は BlockUI の読み込み順/重複読み込みを疑う
3. ローダー画像とパスを確認する
<img src="/images/Loader_Cube.gif" />のパスが正しいか- 相対パスを使っている場合、仮想ディレクトリ配下などで期待と異なるパスになっていないか
- ブラウザで直接 URL を叩いて画像が表示されるか確認
UX をさらに良くするための工夫
ローディング表示が安定して出るようになったら、次はユーザー体験をより良くするための工夫も検討しましょう。
ボタン連打を防ぐ
保存ボタンクリック時にローディング表示を出すタイミングで、ボタン自体も非活性にしておくと二重送信を防げます。
<script type="text/javascript">
function showLoadingAndDisable(button) {
var overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'block';
}
if (button) {
button.disabled = true;
}
return true;
}
</script>
<asp:Button ID="btnSave" runat="server" Text="保存"
OnClick="btnSave_Click"
OnClientClick="return showLoadingAndDisable(this);" />
検証(Validation)との組み合わせ
RequiredFieldValidator などの検証コントロールを使っている場合、検証に失敗したときはローディングを出したくありません。その場合は次のように Page_ClientValidate() を利用します。
<script type="text/javascript">
function validateAndShowLoading() {
if (typeof (Page_ClientValidate) === 'function' && !Page_ClientValidate()) {
return false; // 検証失敗時はポストバックしない
}
var overlay = document.getElementById('loadingOverlay');
if (overlay) {
overlay.style.display = 'block';
}
return true;
}
</script>
<asp:Button ID="btnSave" runat="server" Text="保存"
OnClick="btnSave_Click"
CausesValidation="true"
OnClientClick="return validateAndShowLoading();" />
どのパターンを選べばよいか
今回紹介した 2 つのパターンを、用途別に整理すると次のようになります。
| ケース | おすすめパターン | 理由 |
|---|---|---|
| まずはシンプルにローディングを出したい | パターンA(OnClientClick + フルポストバック) | 実装が簡単で、他の仕組みの影響を受けにくい |
| 既に UpdatePanel で部分更新している | パターンB(UpdatePanel + PageRequestManager + BlockUI) | 既存の構造を活かしつつ、部分更新のメリットを維持できる |
| 画面の一部だけローディングしたい | パターンB | BlockUI の対象を UpdatePanel 内だけに絞れる |
| jQuery や BlockUI を使いたくない | パターンA | 純粋な JavaScript と CSS のみで実現可能 |
まとめ:ローディングが「チラつくだけ」の正体を理解する
- サーバー側処理中に UI を更新することはできないため、ポストバック開始前にローディングを表示する必要がある
- PageRequestManager はUpdatePanel の AJAX 部分更新に対してのみ有効で、フルポストバックではローディングが「チラつく」原因になる
- フルポストバック前に確実にローディングを出したいなら、OnClientClick でオーバーレイを表示してからポストバックさせるのがもっともシンプル
- UpdatePanel を活用したい場合は、ScriptManager / UpdatePanel / トリガーの関係を整理し、beginRequest / endRequest で BlockUI を制御する
- jQuery・Bootstrap・BlockUI などのライブラリの重複や読み込み順の不整合があると、ローディング表示が不安定になりやすい
ASP.NET Web Forms のポストバックとライフサイクルの仕組みを理解できれば、「チラつくだけで表示されない」という現象は必ず理由を説明できます。この記事のコード例とチェックリストをベースに、自身のプロジェクトの構成に合わせた最適なパターンを選び、安定したローディング表示を実現してみてください。

コメント