WebSurfer's Home

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

SqlDataSource を使わず GridView をソート

by WebSurfer 2018年10月22日 13:15

SqlDataSource などのデータソースコントロールを使わないで GridView の各列にソート機能を実装するにはどうすればいいかということを書きます。

GridView の Sorting

元は MSDN Forum のスレッド「SqlDataSourceを使わずに、GridViewの並べ替えができますでしょうか?」の話です。

MSDN ライブラリ GridView Class の「データへのバインド」のセクションに書いてありますが、GridView はデータソースコントロール(SqlDataSource, ObjectDataSource など)と連携して並べ替え、更新、削除、およびページング機能を提供しています。

逆に言えば、データソースコントロールを使わないと、ソート、更新、削除、およびページング機能が必要な場合、全て自力でコードを書いて実装することになります。

以下に、例としてソート機能を自力で GridView に実装する方法を書きます。

ソートを行うために GridView と SqlDataSource の中でどのような操作が行われているかは不明ですが、おそらく、取得したデータから DataView を作って、その Sort プロパティ に SortExpression を設定し、ソートした結果を GirdView に表示していると思われます。

そのあたりの処理を自力でコードを書いて実装してみます。

昇順に並べ替えるだけなら、GridView.Sorting イベントのハンドラで、DataView の Sort プロパティを引数の GridViewSortEventArgs オブジェクトから取得できる SortExpression に設定するだけで可能です。

SqlDataSource + GridView を使った場合、同じ LinkButton のクリックを繰り返すと ASC / DESC が切り替わりますが、そこまで同じにしようとするとちょっと面倒です。

SqlDataSource + GridView でそれをどのように実現しているか分かりませんが、GridView の属性に CurrentSortField, CurrentSortDir というのを追加し、それに前回クリックされた LinkButton の SortExpression およびその時 ASC or DESC どちらだったかの情報を保持することを考えてみました。

そのソースコードを以下にアップしておきます。このコードの実行結果が上の画像の GridView です。ヘッダの Name を 2 回クリックして降順に並べ替えています。

.aspx.cs ファイル

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;

public partial class _0055_GridViewSorting : System.Web.UI.Page
{
    // データソース用の DataView を作成
    protected DataView CreateDataSource()
    {
        DataTable dt = new DataTable();
        DataRow dr;

        dt.Columns.Add(new DataColumn("ID", typeof(Int32)));
        dt.Columns.Add(new DataColumn("Name", typeof(string)));
        dt.Columns.Add(new DataColumn("Type", typeof(string)));
        dt.Columns.Add(new DataColumn("Price", typeof(Int32)));
        dt.Columns.Add(new DataColumn("Qty", typeof(Int32)));
        dt.Columns.Add(new DataColumn("Amount", typeof(Int32)));
        dt.Columns.Add(new DataColumn("CatID", typeof(Int32)));
        dt.Columns.Add(new DataColumn("Note", typeof(string)));
        dt.Columns.Add(new DataColumn("Disc", typeof(bool)));
        dt.Columns.Add(new DataColumn("Date", typeof(DateTime)));

        for (int i = 0; i < 15; i++)
        {
            dr = dt.NewRow();
            dr["ID"] = i;
            dr["Name"] = "Product Name_" + i.ToString();
            dr["Type"] = "Product Type " + (100 - i).ToString();
            dr["Price"] = 123000 * (i + 1);
            dr["Qty"] = (i + 1) * 20;
            dr["Amount"] = 123000 * (i + 1) * (i + 1);
            dr["CatID"] = 100 - i;
            dr["Note"] = "Note_" + i.ToString();
            dr["Disc"] = (i % 2 == 0) ? true : false;
            dr["Date"] = DateTime.Now.AddDays(i);
            dt.Rows.Add(dr);
        }
        return new DataView(dt);
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            GridView1.DataSource = CreateDataSource();
            GridView1.DataBind();
        }
    }

    protected void GridView1_Sorting(object sender, 
                                      GridViewSortEventArgs e)
    {
        GridView gv = (GridView)sender;
        DataView view = CreateDataSource();
        string exp = e.SortExpression;

        // 同じ LinkButton をクリックした場合 ASC / DESC を切り
        // 替えるための処理。
        // GridView の属性に CurrentSortField, CurrentSortDir 
        // を追加し、それに前回クリックされた LinkButton の 
        // SortExpression およびその時 ASC or DESC どちらだった
        // かの情報を保持。
        if (gv.Attributes["CurrentSortField"] != null &&
            gv.Attributes["CurrentSortDir"] != null)
        {
            if (exp == gv.Attributes["CurrentSortField"])
            {
                if (gv.Attributes["CurrentSortDir"] == "ASC")
                {
                    exp = exp + " DESC";
                    gv.Attributes["CurrentSortDir"] = "DESC";
                }
                else
                {
                    exp = exp + " ASC";
                    gv.Attributes["CurrentSortDir"] = "ASC";
                }
            }
            else
            {
                gv.Attributes["CurrentSortField"] = exp;
                exp = exp + " ASC";
                gv.Attributes["CurrentSortDir"] = "ASC";
            }
        }

        view.Sort = exp;
        gv.DataSource = view;
        gv.DataBind();
    }
}

.aspx ファイル

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="0055-GridViewSorting.aspx.cs" 
    Inherits="_0055_GridViewSorting" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:GridView ID="GridView1" runat="server" 
            AllowSorting="True"
            CurrentSortField=""
            CurrentSortDir=""
            onsorting="GridView1_Sorting">
        </asp:GridView>
    </form>
</body>
</html>

上のコードを見ると、自力で一行もコードを書く必要のない SqlDataSource を使う方が正解と思えます。何らかの理由で SqlDataSource を使えない場合でも、上に紹介した MSDN Forum の記事に書いてあるように ObjectDataSource を使うという手段もあります。

Tags: ,

ASP.NET

サーバー側で応答コンテンツの取得

by WebSurfer 2018年7月14日 13:26

ASP.NET Web アプリの応答コンテンツの文字列を、サーバー側でのログ取得などの目的で取得する方法を書きます。(あまり需要はないかもしれませんが)

参考にしたのは stackoverflow のスレッド Logging raw HTTP request/response in ASP.NET MVC & IIS7 です。その記事を見れば情報としては十分かもしれませんが、リンク切れになると困るし、記事にはない HTTP モジュールを使っての設定方法も追加して書いておきます。

基本的には、応答コンテンツは応答ストリームに含まれますのでそれを何らかの手段で取得するということになります。

stackoverflow の記事では、応答ストリームをラッピングするフィルターを作成し、フィルターの中に応答ストリームとは別に MemoryStream を用意し、ASP.NET が応答ストリームに書き込む際 MemoryStream にも同じ内容を書き込むようにし(要するに MemoryStream にコピーを作成し)、MemoryStream から応答コンテンツ���取得するという方法が紹介されています。

下のコード例を見てください。その中の OutputFilterStream クラスがラッピングフィルターです。stackoverflow の記事のコードをそのままコピーしたものです。

HTTP モジュール(下のコードでは ResponseContentLogHttpModule クラス)を用い、BeginRequest イベントのタイミングでラッピングフィルターを HttpResponse.Filter プロパティに設定します。(注:EndRequest のタイミングではダメです。ストリームにはその前に書き込まれるので)

HttpResponse.Filter プロパティの NSDN ライブラリの説明には "伝送する前に HTTP エンティティ本体を変更するために使用される、ラッピングフィルターオブジェクトを取得または設定します" とありますが、OutputFilterStream はコンテンツの変更は一切せずそのまま応答として返し、MemoryStream にコピーを取得することのみ行います。

BeginRequest のタイミングで設定したラッピングフィルターオブジェクトを EndRequest のタイミングで取得し MemoryStream にコピーされた応答コンテンツを取得します。

BeginRequest のタイミングから EndRequest のタイミングまでラッピングフィルターオブジェクトを保持するには HttpContext.Items プロパティを利用します。下のコード例を見てください。

クラスのフィールド等で保持するのは NG という報告がありましたので注意してください。HttpContext.Items を使えば、少なくとも HttpContext が存在する限りラッピングフィルターオブジェクトは保持されるが(GC の対象にはならないが)、クラスのフィールド変数ではその限りではないということのようです。

以下に HTTP モジュールとラッピングフィルターのコード例をアップしておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;

// HTTP モジュール
public class ResponseContentLogHttpModule : IHttpModule
{
  public ResponseContentLogHttpModule()
  {
  }

  public String ModuleName
  {
    get { return "ResponseContentLogHttpModule"; }
  }

  public void Init(HttpApplication application)
  {
    application.BeginRequest += this.BeginRequest;
    application.EndRequest += this.EndRequest;
  }

  private void BeginRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      HttpResponse response = context.Response;
      var filter = new OutputFilterStream(response.Filter);
      response.Filter = filter;

      // OutputFilterStream オブジェクトを EndRequest で
      // 利用できるよう HttoContext.Items に参照を保持。
      // (注:クラスのフィールド等で保持するのは NG)
      context.Items["FilterStream"] = filter;
    }
  }

  private void EndRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      // BegineRequest で HttpContext.Items に保持した
      // OutputFilterStream オブジェクトへの参照を取得
      var filter =
        (OutputFilterStream)context.Items["FilterStream"];

      // 応答コンテンツの文字列を取得
      string responseContent = filter.ReadStream();
    }
  }

  // IHttpModule に定義されているので空でも以下が必要
  public void Dispose() { }
}

// ラッピングフィルタークラス
// stackoverflow の記事のコードをそのままコピー
public class OutputFilterStream : Stream
{
  private readonly Stream InnerStream;
  private readonly MemoryStream CopyStream;

  public OutputFilterStream(Stream inner)
  {
    this.InnerStream = inner;
    this.CopyStream = new MemoryStream();
  }

  public string ReadStream()
  {
    lock (this.InnerStream)
    {
      if (this.CopyStream.Length <= 0L ||
          !this.CopyStream.CanRead ||
          !this.CopyStream.CanSeek)
      {
        return String.Empty;
      }

      long pos = this.CopyStream.Position;
      this.CopyStream.Position = 0L;
      try
      {
        return new StreamReader(this.CopyStream).ReadToEnd();
      }
      finally
      {
        try
        {
          this.CopyStream.Position = pos;
        }
        catch { }
      }
    }
  }

  public override bool CanRead
  {
    get { return this.InnerStream.CanRead; }
  }

  public override bool CanSeek
  {
    get { return this.InnerStream.CanSeek; }
  }

  public override bool CanWrite
  {
    get { return this.InnerStream.CanWrite; }
  }

  public override void Flush()
  {
    this.InnerStream.Flush();
  }

  public override long Length
  {
    get { return this.InnerStream.Length; }
  }

  public override long Position
  {
    get { return this.InnerStream.Position; }
    set
    {
      this.CopyStream.Position =
      this.InnerStream.Position = value;
    }
  }

  public override int Read(byte[] buffer,
                          int offset, int count)
  {
    return this.InnerStream.Read(buffer, offset, count);
  }

  public override long Seek(long offset, SeekOrigin origin)
  {
    this.CopyStream.Seek(offset, origin);
    return this.InnerStream.Seek(offset, origin);
  }

  public override void SetLength(long value)
  {
    this.CopyStream.SetLength(value);
    this.InnerStream.SetLength(value);
  }

  public override void Write(byte[] buffer,
                             int offset, int count)
  {
    this.CopyStream.Write(buffer, offset, count);
    this.InnerStream.Write(buffer, offset, count);
  }
}

上の HTTP モジュールが動く ようにするには web.config での設定が必要ですので注意してください。以下に設定例を書いておきます。

<system.webServer>
  <modules>
    <add name="ResponseContentLogHttpModule" 
         type="ResponseContentLogHttpModule"/>
  </modules>
</system.webServer>

Tags: , ,

ASP.NET

ASP.NET 4.5 スクリプト マッピング

by WebSurfer 2018年4月24日 15:58

先の記事「ASP.NET 4.5 ScriptManager」で "テンプレートで「空」を選択した場合は、最低でも jQuery, MSAjax, WebForms の 3 つの NuGet パッケージはインストールして、ScriptManager に登録し、全てのページに ScriptManager を配置するのがよさそうです" と書きました。その手順を備忘録として残しておきます。

「空」のテンプレート

まず、「空」のテンプレートというのは何かですが、上の画像のように Visual Studio で Web アプリケーションを作成する際に「空」を選択して Web Forms にチェックを入れたものです。(画像は Visual Studio Community 2015 のもの)

上の設定で作ったアプリケーションには、先の記事で書いたような ScriptManager へのスクリプトマッピング等は一切含まれず、自力で NuGet パッケージをダウンロードして必要な設定することになります。

その手順は以下の通りです。

(1) jQuery, MSAjax, WebForms の NuGet パッケージ

jQuery, MSAjax, WebForms の NuGet

Visual Studio の[ツール(T)]⇒[NuGet パッケージマネージャー(N)]⇒[ソリューションの NuGet パッケージの管理(N)...]と進んで管理画面を開き、以下の 3 つの NuGet パッケージをインストールします。(もし Bootstrap 等も必要でしたら追加してください)

  • AspNet.ScriptManager.jQuery
  • Microsoft.AspNet.ScriptManager.MsAjax
  • Microsoft.AspNet.ScriptManager.WebForms

上の画像はインストール後のもので、バージョンはこの記事を書いた時点で最新のものです。

NuGet パッケージのインストールが完了すると、下の画像の通り JavaScript ファイルが Scripts フォルダ下にインストールされます。

スクリプトファイル

その他、ASP.NET 4.5 ScriptManager Improvements in WebForms に書いてありますが、スクリプトマッピングのためのコードが PreApplicationStart メソッドに追加されるそうです。

追加されているのを目で見て確認する方法は分かりませんが、ScriptManaget に以下の設定をすれば jQuery 3.3.1 のスクリプトファイルへの参照は正しくレンダリングされるようになりますので、間違いなく追加されていると思います。

<asp:ScriptManager runat="server">
  <Scripts>
    <asp:ScriptReference Name="jquery" />
  </Scripts>
</asp:ScriptManager>

ただし、MsAjax と WebForms の方は NuGet パーッケージのインストールだけでは不足で、Web.Optimization 関係の NuGet パッケージのインストール、バンドル定義の作成とそれの登録が追加で必要になります。

(2) Web.Optimization の NuGet パッケージ

 の NuGet

上記 (1) の手順と同様に Visual Studio で NuGet パッケージの管理管理画面を開き、以下の 2 つの NuGet パッケージをインストールします。上の画像はインストール後のもので、バージョンはこの記事を書いた時点で最新のものです。

  • Microsoft.AspNet.Web.Optimization
  • Microsoft.AspNet.Web.Optimization.WebForms

これらの NuGet パッケージのインストールはバンドル定義を行うために必要です。

(3) バンドル定義と登録

「Web フォーム」のテンプレートを使って作成すると自動生成される App_Start フォルダの BundleConfig.cs のコードを参考にバンドル定義を作成します。

参考と言っても、そのままコピペして不要な部分を削除するだけですが。コードは以下のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Optimization;
using System.Web.UI;

namespace WebFormsEmpty
{
  public class BundleConfig
  {
    public static void RegisterBundles(BundleCollection bundles)
    {
      bundles.Add(new ScriptBundle("~/bundles/WebFormsJs").
      Include("~/Scripts/WebForms/WebForms.js",
              "~/Scripts/WebForms/WebUIValidation.js",
              "~/Scripts/WebForms/MenuStandards.js",
              "~/Scripts/WebForms/Focus.js",
              "~/Scripts/WebForms/GridView.js",
              "~/Scripts/WebForms/DetailsView.js",
              "~/Scripts/WebForms/TreeView.js",
              "~/Scripts/WebForms/WebParts.js"));

      bundles.Add(new ScriptBundle("~/bundles/MsAjaxJs").
      Include(
      "~/Scripts/WebForms/MsAjax/MicrosoftAjax.js",
      "~/Scripts/WebForms/MsAjax/" + 
                      "MicrosoftAjaxApplicationServices.js",
      "~/Scripts/WebForms/MsAjax/MicrosoftAjaxTimer.js",
      "~/Scripts/WebForms/MsAjax/MicrosoftAjaxWebForms.js"));
    }
  }
}

上記のバンドル定義を Global.asax の Application_Start メソッドで登録します。コードは以下のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using System.Web.Optimization;

namespace WebFormsEmpty
{
  public class Global : System.Web.HttpApplication
  {
    protected void Application_Start(object sender, 
                                           EventArgs e)
    {
      BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
  }
}

(4) ScriptManager への登録

「Web フォーム」のテンプレートを使って作成すると自動生成されるマスターページ Site.Master の ScriptManager のコードを参考に、クライアントにレンダリングされるスクリプトを登録します。

そのままコピペして、今回は使わない bootstrap, respond の部分を削除するだけです。コードは以下のようになります。

<asp:ScriptManager runat="server">
  <Scripts>
    <asp:ScriptReference Name="MsAjaxBundle" />
    <asp:ScriptReference Name="jquery" />
    <asp:ScriptReference Name="WebForms.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/WebForms.js" />
    <asp:ScriptReference Name="WebUIValidation.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/WebUIValidation.js" />
    <asp:ScriptReference Name="MenuStandards.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/MenuStandards.js" />
    <asp:ScriptReference Name="GridView.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/GridView.js" />
    <asp:ScriptReference Name="DetailsView.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/DetailsView.js" />
    <asp:ScriptReference Name="TreeView.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/TreeView.js" />
    <asp:ScriptReference Name="WebParts.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/WebParts.js" />
    <asp:ScriptReference Name="Focus.js" 
        Assembly="System.Web" 
        Path="~/Scripts/WebForms/Focus.js" />
    <asp:ScriptReference Name="WebFormsBundle" />
  </Scripts>
</asp:ScriptManager>

上のコードで Assembly と Path を指定した ScriptReference がありますが、それらは ASP.NET 4.5 ScriptManager Improvements in WebForms に書いてありありますように、deduping(重複排除)のための "special arrangement"だそうです。(どのような仕組みで排除されるのかは分かりませんが)

結果、上記 (3) のバンドル定義に従ってバンドルされたスクリプト定義への参照と、jQuery 3.3.1 のスクリプトファイルへの参照が、正しい順序でクライアントにレンダリングされます。

<script src="/bundles/MsAjaxJs?v=c42ygB2...></script>
<script src="Scripts/jquery-3.3.1.js" .....></script>
<script src="/bundles/WebFormsJs?v=AAyiA...></script>

Tags: ,

ASP.NET

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar