ASP.NET Web Forms アプリで、サーバー側にて非同期で行われている処置の進捗状況を Label コントロールや jQuery UI の Progressbar を使って表示する方法を書きます。
ScriptManager, UpdatePanel, UpdateProgress を使えば非同期要求をかけて応答が戻ってくるまでの間、サーバーで処理中という情報をユーザーに示すことはできます。具体的な方法は MDSN ライブラリの記事UpdateProgress コントロールの概要を見てください。
しかしながらサーバー側での処置が何 % 終わったかというような進捗状況を表示することはできません。何故ならサーバー側での処理が終わるまでサーバーからは何の応答も返ってこないからです。
サーバー側での処理の進捗状況をブラウザに表示する場合、サーバー側での処理は別スレッドで行うようにし、ブラウザからタイマーを使って定期的に AJAX で進捗状況をサーバーに問合せ、サーバーからその応答をもらって表示するということは可能です。
時間のかかる処理をこのような方法で行うのが適切か、Web ファーム / ガーデン対応やワーカープロセスのリサイクル対応はどうするかという話はちょっと置いておいて、こうすれば何とかできるという具体的な例を以下に書きます。
(Thread を使うのは "don't even think about it" という意見もありますので注意してください。詳しくはこの記事の下の方の 2015/12/29 追記を見てください)
まず「時間のかかる処理」ですが、処置を行うだけでは当然ダメで、何らかの方法で進捗状況を取得できる必要があります。(進捗状況が取得できないような処理もあるかと思いますが、そういう場合はお手上げです)
具体例は以下のコードを見て下さい。DoTask メソッドで処置を行うと同時に進捗(以下の例では 0 ~ 100 まで)を記録し、Progress プロパティで進捗状況を取得できるようにしています。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
// 下の Task は自作のカスタムクラス
// System.Threading.Tasks 名前空間の Task クラスではない
public class Task
{
public int Progress { get; set; }
public Task()
{
this.Progress = 0;
}
public void DoTask()
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
this.Progress = i + 1;
}
}
}
ASP.NET Web Forms アプリのページで上のコードの処理を別スレッドで実行します。Timer を使って 1 秒毎に非同期要求をかけ、Progress プロパティで進捗(上のコード例では 0 ~ 100 まで)を取得し、Label コントロールと jQuery UI の Progressbar に表示するようにしています。
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Threading" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
// Task は App_Code に定義したカスタムクラス
// その中の DoTask メソッドで処理を行う
// 進捗状況は Progress プロパティで取得する
private Task task = null;
protected void Page_Load(object sender, EventArgs e)
{
// Session["Task"] が null でなければ処理中
object obj = Session["Task"];
if (obj != null)
{
task = (Task)obj;
}
}
protected void Button1_Click(object sender, EventArgs e)
{
// 処理中の場合は何もしないで return
if (task != null)
{
return;
}
// Task クラスを初期化して Session に保持
task = new Task();
Session["Task"] = task;
// 別スレッドで処理を実行
Thread newThread = new Thread(task.DoTask);
newThread.Start();
}
// Timer.Interval プロパティに設定したインターバル
// (この例では 1 秒)で非同期呼び出しがかかって
// Page_Load メソッドのあとこのメソッドが実行される
protected void Timer1_Tick(object sender, EventArgs e)
{
if (task != null)
{
if (task.Progress == 100)
{
Label1.Text = "完了";
// ラベルに表示しない場合もこれだけは必要
Session.Remove("Task");
}
else
{
Label1.Text = task.Progress.ToString() + "%";
}
// Progress Bar に表示する進捗データを設定
if (ScriptManager.GetCurrent(this).IsInAsyncPostBack)
{
ScriptManager.GetCurrent(this).
RegisterDataItem(this, task.Progress.ToString());
}
}
else
{
Label1.Text = "処置は行われていません";
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="/jquery.js" type="text/javascript"></script>
<script src="/jquery-ui.js" type="text/javascript"></script>
<link href="/jquery-ui.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.ui-progressbar {
position: relative;
}
.progress-label {
position: absolute;
left: 50%;
top: 4px;
font-weight: bold;
text-shadow: 1px 1px 0 #fff;
}
</style>
<script type="text/javascript">
//<![CDATA[
function pageLoad(sender, args) {
if (args.get_isPartialLoad() === false) {
var manager =
Sys.WebForms.PageRequestManager.getInstance();
manager.add_endRequest(OnEndRequest);
}
}
// Timer で非同期要求がかかり応答が戻ってくるたびに以下の
// メソッドが実行される
function OnEndRequest(sender, args) {
// サーバー側の Timer1_Tick メソッドで設定した進捗
// データをクライアント側で取得
var progress = args.get_dataItems()["__Page"];
// 進捗データを Progress Bar に表示する
if (progress) {
$("#progressbar").
progressbar("value", Number(progress));
}
}
// jQuery UI のデモのコードを借用
$(function () {
var progressbar = $("#progressbar"),
progressLabel = $(".progress-label");
progressbar.progressbar({
value: false,
change: function () {
progressLabel.
text(progressbar.progressbar("value") + "%");
},
complete: function () {
progressLabel.text("Complete!");
}
});
});
//]]>
</script>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<%--これが ProgressBar になる
jQuery UI のデモのコードを借用--%>
<div id="progressbar">
<div class="progress-label">Loading...</div>
</div>
<asp:Button ID="Button1" runat="server"
Text="Start Task" OnClick="Button1_Click" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Label ID="Label1" runat="server" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Timer1"
EventName="Tick" />
</Triggers>
</asp:UpdatePanel>
<asp:Timer ID="Timer1" runat="server"
Interval="1000" OnTick="Timer1_Tick">
</asp:Timer>
</form>
</body>
</html>
上記のコードを Chrome から呼んで、[Start Task]ボタンクリックで処置を開始し、その進捗状況を表示したのが上の画像です。
-------- 2015/12/29 追記 --------
MSDN Blog の記事 Performing Asynchronous Work, or Tasks, in ASP.NET Applications によると、Thread を使うのは、その記事を書いた Microsoft の開発者よりはるかにスマートに実装できるのでなければ "don't even think about it" だそうです。(汗)
理由はその記事の FAQ の 4 番目に書いてありますが、以下に概略を書いておきます(誤訳はあるかも)。
-
CLR ThreadPool を使用するのに比べて非常にコストが高い。
-
自分で作った Thread に I/O 要求が残ってないか終了前にチェックしなければならない。
-
システムのパフォーマンスを保つには実行されている Thread の数が適切でなければならないが、自分で Thread を作るのであればパフォーマンスを保つのは自分の責任になる。
暇なサイトならともかく、要求が多いサイトの場合はやはり問題になりそうです。