WebSurfer's Home

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

ポストバック前後でスクロール位置維持

by WebSurfer 2016年1月11日 14:11

ASP.NET Web Forms アプリには MaintainScrollPositionOnPostBack プロパティというものがあって、ポストバック前後で上下左右のスクロール位置を維持できる仕組みが用意されています。

ただし、ASP.NET 2.0, 3.0, 3.5 でブラウザに Safari, Chrome を使用した場合は動きません。理由は、先の記事「Safari は downlevel browser?」に書きましたように、Safari, Chrome のブラウザ定義に問題があるからです。(ASP.NET 4 は問題なし)

以下に、上下左右のスクロール位置を維持する仕組み、ASP.NET 2.0, 3.0, 3.5 での Safari, Chrome のブラウザ定義は何故ダメか、その対処方法を書きます。

まずどういう仕組みでスクロール位置を維持するかですが、Page で MaintainScrollPositionOnPostback が true に設定されていると、ブラウザ定義に問題がなければ以下のようなスクリプトと隠しフィールドが html ソースに含まれてレンダリングされます。(注:初期画面では隠しフィールドの value は "0" になり、下の方のインラインスクリプトで window.onload での操作を行う 2 行は含まれません)

<script type="text/javascript">
//<![CDATA[
var theForm = document.forms['form1'];
if (!theForm) {
    theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}
//]]>
</script>


<script src="/WebResource.axd?d=Dgk..." type="text/javascript">
</script>


<input type="hidden" 
  name="__SCROLLPOSITIONX" 
  id="__SCROLLPOSITIONX" 
  value="50" />
<input type="hidden" 
  name="__SCROLLPOSITIONY" 
  id="__SCROLLPOSITIONY" 
  value="200" />


<script type="text/javascript">
//<![CDATA[
theForm.oldSubmit = theForm.submit;
theForm.submit = WebForm_SaveScrollPositionSubmit;

theForm.oldOnSubmit = theForm.onsubmit;
theForm.onsubmit = WebForm_SaveScrollPositionOnSubmit;

theForm.oldOnLoad = window.onload;
window.onload = WebForm_RestoreScrollPosition;
//]]>
</script>

上のコードで真中あたりにある WebResource.axd は HTTP ハンドラで、Page の埋め込みリソースとして設定されている外部スクリプトファイルを取得します。

その外部スクリプトファイルの中に、上にアップしたコードの下の方にあるインラインスクリプトで使う WebForm_SaveScrollPositionSubmit メソッド他の定義が含まれています。(興味があれば開いて中身を見てください。Visual Studio でデバッグ実行すれば開いてブレークポイントを設定したりできます)

これらのスクリプトによって、(1) ポストバックされる前の画面のスクロール位置を取得、(2) 隠しフィールドの value に画面のスクロール位置を設定してサーバーに送信、(3) サーバーから応答が戻ってきて画面が再描画されるときに隠しフィールドからポストバック前のスクロール位置を取得、(4) 画面のスクロール位置をポストバック前と同じに設定・・・という操作を行います。

しかしながら、ブラウザ定義ファイルで capability 要素 (capabilities の子要素) の supportsMaintainScrollPositionOnPostback が true に設定されてないと ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

ASP.NET 2.0, 3.0, 3.5 用のブラウザ定義ファイルは以下のフォルダにあります。

C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\Browsers

2016/1/11 時点では mozilla.browser ファイルの中に id="Safari1Plus" という browser 要素があって、Safari(かなり古いものは除く)および Chrome はその定義の適用を受けます(継承元の Default ← Mozilla ← Gecko ← Safari の定義も)。

Default のブラウザ定義で supportsMaintainScrollPositionOnPostback は false に設定されていますが、Default ← Mozilla ← Gecko ← Safari ← Safari1Plus という継承の過程でそれを true に設定するものがありません。(注:ASP.NET 4 のブラウザ定義ファイルは問題ありません)

結果、Page で MaintainScrollPositionOnPostBack プロパティを true に設定しても ASP.NET は必要なスクリプトや隠しフィールドを生成しません。

解決策は、アプリケーションルート直下に App_Browser フォルダを追加し、さらにそのフォルダに .browser ファイル(名前は任意)を追加し、その中で refID 属性を使用して既存の id="Safari1Plus" のブラウザー定義で supportsMaintainScrollPositionOnPostback が true になるように設定してやります。

具体的には、以下のコードを新たに作った .browser ファイルに記述します。これによって、Safari, Chrome でも必要なスクリプトや隠しフィールドが生成されるようになるはずです。

<browsers>
  <browser refID="Safari1Plus">
    <capabilities>
      <capability 
        name="supportsMaintainScrollPositionOnPostback"
        value="true" />
    </capabilities>
  </browser>
</browsers>

CONFIG\Browsers フォルダの中の mozilla.browser 他の定義済みのブラウザ定義ファイルを修正するのは NG ですので注意してください。修正しても修正結果は反映されませんので。(詳しくは MSDN Blog の記事「ASP.NET の IE10 対応について」を見てください)

Tags:

ASP.NET

進捗状況の表示

by WebSurfer 2015年12月28日 18:07

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 番目に書いてありますが、以下に概略を書いておきます(誤訳はあるかも)。

  1. CLR ThreadPool を使用するのに比べて非常にコストが高い。
  2. 自分で作った Thread に I/O 要求が残ってないか終了前にチェックしなければならない。
  3. システムのパフォーマンスを保つには実行されている Thread の数が適切でなければならないが、自分で Thread を作るのであればパフォーマンスを保つのは自分の責任になる。

暇なサイトならともかく、要求が多いサイトの場合はやはり問題になりそうです。

Tags:

AJAX

w2ui Grid

by WebSurfer 2015年12月26日 16:17

w2ui が提供している JavaScript ライブラリに Grid という表を表示するための widget があります。訳あって ASP.NET Web Forms アプリでちょっと使ってみましたので、使い方を忘れないように書いておきます。

w2ui Grid

(注:先の記事 GridView のヘッダ、列を固定(その 2)でも Grid という名前の同様な JavaScript ライブラリを紹介していますが、それとは別物です)

今回使った w2ui ライブラリはこの記事を書いている時点での最新版 1.4.3 です。jQuery も必須でとりあえず今回は自分が持っていたバージョン 1.8.3 を使いました。

たぶん、SQL Server などのデータベースからデータを取得して Grid で表示する際、どうやって Grid にデータを渡すかというところが一番の課題になると思いますので、そこのところを書いておきます。(他は w2ui の記事を読めば分かると思いますので割愛。手抜きでスミマセン)

簡単に書くと、Grid Overview のページの Example 2 に書いてあるように url にリソースを指定しておくと、Grid が url に指定されたリソースを非同期要求するので、要求を受けたら Example 2 に書いてある形式の JSON 文字列を応答として返すようにしておけば、後は表示まで全部 Grid が面倒見てくれます。

url に指定するリソースは、同じドメインにあって(AJAX なのでドメインが異なるのは NG)指定された形式の JSON 文字列を返すことができれば、.aspx ページ、HTTP ハンドラ、Web サービス、WPF、MVC のアクションメソッド、Web API などを使用できます。

ただし、Grid が非同期要求する際、{ "cmd":"get-records", ...} というデータが application/x-www-form-urlencoded 形式に変換され(JSON 文字列でないことに注意)、フォームデータとして POST されてきますので、そのフォームデータを使ってサーバー側で何か処置を行う場合は JSON 文字列を受けることが前提の Web サービス、WPF は使い勝手が悪そうです。

ASP.NET Web Forms アプリなら .aspx ページまたは HTTP ハンドラを使うのが都合がよさそうです。なので、今回は HTTP ハンドラを使用する例を書きました。

HTTP ハンドラ

Microsoft が提供している SQL Server サンプルデータベース Northwind の Orders テーブル(14 フィールド x 830 レコード)から全レコードを取得し、指定された形式の JSON 文字列にシリアル化して応答として返します。

シリアル化の方法は MSDN ライブラリの方法 : JSON データをシリアル化および逆シリアル化するを参考にしました。

コード内のコメントにも書きましたが、DateTime 型は \/Date(836406000000+0900)\/ のようにシリアル化されます。(詳しくは MSDN ライブラリの記事「スタンドアロン JSON のシリアル化」の「高度な情報 / DateTime ワイヤ形式」のセクションの説明を見てください)

クライアント側で形式変換できなければ、サーバー側で適当な文字列に変換する必要があります。(Grid を使った場合、形式変換するコードを割り込ませる場所がなさそうです。今回のサンプルではそのままにしています)

<%@ WebHandler Language="C#" 
    Class="_0139_w2uiOrdersHandler" %>

using System;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.Serialization;
using System.Web.Configuration;
using System.Collections.Generic;
using System.Runtime.Serialization.Json;

public class _0139_w2uiOrdersHandler : IHttpHandler 
{    
  public void ProcessRequest (HttpContext context) 
  {
    GridData data = new GridData();
    data.records = new List<Record>();
        
    string connString = WebConfigurationManager.
                        ConnectionStrings["Northwind"].
                        ConnectionString;
    string query = "SELECT [OrderID], [CustomerID]," +
        "[EmployeeID], [OrderDate], [RequiredDate]," +
        "[ShippedDate], [ShipVia], [Freight]," +
        "[ShipName], [ShipAddress], [ShipCity]," + 
        "[ShipRegion], [ShipPostalCode], [shipCountry]" + 
        "FROM [Orders]";

    using(SqlConnection conn = new SqlConnection(connString))
    {
      conn.Open();
      using (SqlCommand cmd = new SqlCommand(query, conn))
      {
        using (SqlDataReader reader = cmd.ExecuteReader())
        {
          if (reader != null)
          {
            while (reader.Read())
            {
              Record record = new Record();
                            
              record.OrderID = reader.GetInt32(0);
              record.CustomerID = reader.IsDBNull(1) ? 
                      null : reader.GetString(1);
              record.EmployeeID = reader.IsDBNull(2) ? 
                      null : (int?)reader.GetInt32(2);
              record.OrderDate = reader.IsDBNull(3) ? 
                      null : (DateTime?)reader.GetDateTime(3);
              record.RequiredDate = reader.IsDBNull(4) ? 
                      null : (DateTime?)reader.GetDateTime(4);
              record.ShippedDate = reader.IsDBNull(5) ? 
                      null : (DateTime?)reader.GetDateTime(5);
              record.ShipVia = reader.IsDBNull(6) ? 
                      null : (int?)reader.GetInt32(6);
              record.Freight = reader.IsDBNull(7) ? 
                      null : (decimal?)reader.GetDecimal(7);
              record.ShipName = reader.IsDBNull(8) ? 
                      null : reader.GetString(8);
              record.ShipAddress = reader.IsDBNull(9) ? 
                      null : reader.GetString(9);
              record.ShipCity = reader.IsDBNull(10) ? 
                      null : reader.GetString(10);
              record.ShipRegion = reader.IsDBNull(11) ? 
                      null : reader.GetString(11);
              record.ShipPostalCode = reader.IsDBNull(12) ? 
                      null : reader.GetString(12);
              record.ShipCountry = reader.IsDBNull(13) ? 
                      null : reader.GetString(13);
                            
              data.records.Add(record);
            }
          }
        }
      }
    }

    HttpResponse response = context.Response;
        
    // キャッシュは無効にする
    response.Cache.SetCacheability(HttpCacheability.NoCache);
    response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
    response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

    response.ContentType = "application/json; charset=utf-8";
        
    data.status = "success";
    data.total = data.records.Count;

    // JSON 文字列にシリアル化
    DataContractJsonSerializer ser = 
        new DataContractJsonSerializer(typeof(GridData));
    ser.WriteObject(response.OutputStream, data);

    // DateTime 型は \/Date(836406000000+0900)\/ のように
    // シリアル化される。
  }

  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }

}

[DataContract]
internal class GridData
{
    [DataMember]
    internal string status { get; set; }

    [DataMember]
    internal int total { get; set; }

    [DataMember]
    internal List<Record> records { get; set; } 
}

[DataContract]
internal class Record
{
    [DataMember]
    internal int OrderID { get; set; }
    
    [DataMember]
    internal string CustomerID { get; set; }
    
    [DataMember]
    internal int? EmployeeID { get; set; }
    
    [DataMember]
    internal DateTime? OrderDate { get; set; }
    
    [DataMember]
    internal DateTime? RequiredDate { get; set; }
    
    [DataMember]
    internal DateTime? ShippedDate { get; set; }
    
    [DataMember]
    internal int? ShipVia { get; set; }
    
    [DataMember]
    internal decimal? Freight { get; set; }
    
    [DataMember]
    internal string ShipName { get; set; }
    
    [DataMember]
    internal string ShipAddress { get; set; }
    
    [DataMember]
    internal string ShipCity { get; set; }
    
    [DataMember]
    internal string ShipRegion { get; set; }
    
    [DataMember]
    internal string ShipPostalCode { get; set; }
    
    [DataMember]
    internal string ShipCountry { get; set; }    
}

Grid を表示する .aspx ページ

上の HTTP ハンドラを以下のように url に設定すれば(以下のコードで 0139-w2uiOrdersHandler.ashx が上記の HTTP ハンドラ)、自動的に HTTP ハンドラに非同期呼び出しがかかって JSON 文字列が取得され、Grid にデータが表示されます。その結果が上の画像です。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title></title>
  <script src="/jquery.js" type="text/javascript"></script>
  <script src="/w2ui.js" type="text/javascript"></script>
  <link href="/w2ui.css" rel="stylesheet" type="text/css" />
  <script type="text/javascript">
  //<![CDATA[
  $(function () {
    $('#myGrid').w2grid({
      name: 'myGrid',
      url: '0139-w2uiOrdersHandler.ashx',
      columns: [
       {field:'OrderID',caption:'Order ID',size:'7%'},
       {field:'CustomerID',caption:'Customer ID',size:'7%'},
       {field:'EmployeeID',caption:'Employee ID',size:'7%'},
       {field:'OrderDate',caption:'Order Date',size:'7%'},
       {field:'RequiredDate',caption:'Required Date',size:'7%'},
       {field:'ShippedDate',caption:'Shipped Date',size:'7%'},
       {field:'ShipVia',caption:'Ship Via',size:'7%'},
       {field:'Freight',caption:'Freight',size:'7%'},
       {field:'ShipName',caption:'Ship Name',size:'7%'},
       {field:'ShipAddress',caption:'Address',size:'7%'},
       {field:'ShipCity',caption:'City',size:'7%'},
       {field:'ShipRegion',caption:'Region',size:'7%'},
       {field:'ShipPostalCode',caption:'Postal Code',size:'7%'},
       {field:'ShipCountry',caption:'Country',size:'7%'}
      ]
    });
  });
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div id="myGrid" style="height: 400px"></div>
  </form>
</body>
</html>

上のサンプル(14 フィールド x 830 レコード)でスクロールすると、Chrome 最新版ならそれなりに動きますが、IE9 あたりだと使い物にならないレベルの遅さでした。古いブラウザでの使用は考えてないのかもしれませんね。

Tags:

JavaScript

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar