WebSurfer's Home

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

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

EF でレコードの削除

by WebSurfer 2015年12月21日 16:34
2016/9/13 書換
間違いや新事実を見つけて部分的な書き換えや追記を行っているうちに意味がよく分からない記事になってしまったので、内容を整理して全面的に書き換えました。

Entity Framework Code First の機能を利用して SQL Server データーベースに作った親子関係のあるテーブルで、レコードの削除を行った際にハマって悩んだ話を書きます。

以下のコードは、Microsoft のチュートリアル「新しいデータベースの Code First」に記載されていたクラス定義ですが、これをそのままサンプルとして使用して説明します。

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
} 

上記のコードをベースに、チュートリアルの手順通りに SQL Server 2008 Express にデータベースを作ると以下の画像のテーブルとフィールドが生成されます。上に親子関係のあるテーブルと書きましたが、Blogs が親、Posts が子です。「Code First の規約 (Code First Conventions)」に従い、Posts テーブルに BlogId という名前の外部キーフィールドが生成され、NULL 不可になっているところに注目してください。

生成された DB

これに上の画像に示すデータを追加した後、例えば以下のようなコードで、コンテキストから一つの親(Blog オブジェクト)を取得し、その中の子のコレクション(List<Post>)から最初の要素を削除した後、SaveChanges メソッドでデータベースに結果を反映しようとしたとします。(Posts テーブルの中の PostId が 4 のレコードを削除しようと試みたということです)

class Program
{
  static void Main(string[] args)
  {
    using (var db = new BloggingContext())
    {
      var b = db.Blogs.Single(i => i.BlogId == 3);
      b.Posts.Remove(b.Posts[0]);
      db.SaveChanges();
    }
  }
}

そうすると、db.SaveChanges(); で InvalidOperationException がスローされます。エラーメッセージは以下のようになります。

"The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted."

上のエラーメッセージが何を言っているかを簡単に言うと「Posts テーブルの BlogId フィールドは NULL 不可だが、それを NULL にしようとして失敗した」ということです。

つまり、上記のコードは Posts テーブルの当該レコードを DELETE するのではなく、当該レコードの外部キーフィールド BlogId を NULL にしようとします。結果、BlogId フィールドは NULL 不可なので失敗します。

コードを少し変更(b.Posts.Remove... の b を db 変更)して以下のようにすると削除に成功します。上の画像の Posts テーブルのレコード一覧で赤枠で囲った部分が下のコードの実行結果ですが、元あった PostId が 7 のレコードが削除されています。

class Program
{
  static void Main(string[] args)
  {
    using (var db = new BloggingContext())
    {
      var b = db.Blogs.Single(i => i.BlogId == 4);
      db.Posts.Remove(b.Posts[0]);
      db.SaveChanges();
    }
  }
}

ハマったのはこの違いです。b と db で何が違うのでしょうか? 以下にその説明を書きます。

前者のコード b.Posts.Remove(b.Posts[0]);

b は Blog クラスのオブジェクトです。詳しく言うと、BloggingContext オブジェクトから Blogs プロパティを使って DbSet<Blog> オブジェクト(親のコレクション)を取得し、その中から BlogId == 3 の条件で取得した Blog オブジェクトです。

なので、前者のコードの意味は、Blog オブジェクトから Posts プロパティを使って List<Post>(外部キーで関連付けられた Post オブジェクトのコレクション)を取得し、それから b.Posts[0] に該当する子を削除するということになります。

つまり、親子の関係を絶つという指示を出しただけで、データベースの Posts テーブルから該当するレコードを削除していいとは誰も言ってないです。

親子の関係を絶つというのは、データベース上では Posts テーブルの当該レコードの外部キーフィールド BlodId を NULL に設定することに相当し、db.SaveChanges() でその操作が行われたということのようです。

なお、外部キーフィールド BlodId を NULL に設定する動きになると言っても、b.Posts[0] の BlogId プロパティが null に書き換えられるわけではないので注意してください(元の値のまま変わりません)。

コード上では、(1) 親が保持する子のコレクション List<Post> から b.Posts[0] に該当する子が外され、(2) b.Posts[0] に該当するエンティティの EntityState が Unchanged から Modified に変わるのみです。

結果からの想像ですが、フレームワークは上の (1), (2) を見て、db.SaveChanges() でデータベースの Posts テーブルの当該レコードの外部キーフィールド BlodId を NULL に設定しようとするようです。

後者のコード db.Posts.Remove(b.Posts[0]);

前者のコードからは、b(Blog オブジェクト)が db(BloggingContext オブジェクト)に変わっている点に注意してください。

なので、後者のコードの意味は、BloggingContext オブジェクト の Posts プロパティを使って DbSet<Post> オブジェクト(子エンティティのコレクション)を取得し、それから b.Posts[0] に該当する子エンティティを削除するということになります。

データベース上では Posts テーブルの当該レコードを削除することに相当するので、当該子エンティティの EntityState は Deleted に設定され、db.SaveChanges() で当該レコードは削除されます。


Posts テーブルの BlogId フィールドが NULL 不可になるのは Post クラスの外部キープロパティ BlogId を int 型にしたからです(クラスに定義されるプロパティが null にできない型の場合は、Code First の規約に従って、データベースのフィールドも NULL 不可になります)。

Microsoft の文書「Code First の規約」によると、外部キーフィールドの NULL 可 / 不可によって DELETE 操作の結果に以下の違いがあるそうです。

"依存エンティティの外部キーで null 値が許容されない場合、Code First はリレーションシップに連鎖削除を設定します。依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます"

上の例では Posts テーブルの BlogId フィールドは NULL 不可なので cascade delete が設定され、例えば以下のようなコードで親を DELETE すると関連する子も DELETE されます。

Blog b = db.Blogs.Single(i => i.BlogId == 6);
db.Blogs.Remove(b);
db.SaveChanges();

Post クラスの外部キープロパティ BlogId を int? 型(null 可)に変更して、Code First の機能を使って Posts テーブルを作ると、Code First の規約に従って、外部キーフィールド BlogId は NULL 可になります。

そのようにして作成したデータベースに対しては、外部キーフィールドBlogId は NULL 可なので当然ですが、上の「前者のコード」でエラーにならず、Posts テーブルの当該レコードの BlogId は NULL に設定されます。

「後者のコード」では Posts テーブルの当該レコードは削除されます。

一つ分からないのが、Microsoft の文書に "依存エンティティの外部キーで null 値が許容される場合は、Code First はリレーションシップに連鎖削除を設定しないため、プリンシパルが削除されると外部キーが null に設定されます" と書いてあったのに、実際に試したらそうなならなかった点です。

子を持つ親を削除しようとしたら FK 制約に引っかかって SqlException がスローされました。子があるのに先に親を削除しようとしたようです。なぜ Microsoft の文書と違うのか理由は不明です。

Tags:

ADO.NET

VB.NET の List(Of T) と Count プロパティ

by WebSurfer 2015年12月12日 15:09

VB.NET の場合、定義されているプロパティが同じクラスで定義されている(もしくは継承したクラスで定義されている)同名のメソッドを隠してしまうという話を、List(Of T) と Count プロパティを例にとって説明します。

例として以下のコードを考えます。

Dim e As IEnumerable(Of Int32) = Enumerable.Range(1, 10)

' これは OK。拡張メソッド Count を呼び出す
Console.WriteLine(e.Count(Function(n) n >= 5))

Dim x As List(Of Int32) = e.ToList

' ビルドエラー「Public ReadOnly Property Count As Integer' には引
' 数がないため、戻り値の型をインデックス化できません。」になる
Console.WriteLine(x.Count(Function(n) n >= 5))

'明示的に拡張メソッドを呼び出せば OK
Console.WriteLine(Enumerable.Count(x, Function(n) n >= 5))

e は IEnumerable(Of T) 型で Count という名前では拡張メソッドの Count のみを実��しています。(同名のプロティは実装されていません)

なので、e.Count(Function(n) n >= 5) で拡張メソッドの Count を呼び出せています。

一方、x は IEnumerable(Of T) から ToList メソッド を使って作成した List(Of T) 型になります。List(Of T) 型には Count プロパティCount 拡張メソッド(Enumerable.Count のオーバーロード)の両方が実装されています。

VB.NET の場合、List(Of T) 型に実装されている Count プロパティによって拡張メソッドの Count が隠されてしまい、コンパイラは Count プロパティが呼ばれていると判断して上のソースにコメントしたようなビルドエラーになります。

これは VB.NET のコンパイラの問題で、同名のプロパティとメソッドを区別できないところからきているようです。

そのことを書いた Micosoft の公式文書は見つかりませんでしたが、MSDN フォーラムの記事で、Matt Warren - MSFT さんが、

"VB compiler is matching the lists Count property instead of the Enumerable.Count() extension method and the Count property is blocking the visibility of the extension method's other signature."

Timothy Ng MSFT さんが、

"in VB, properties shadow methods (and thus, extension methods) by name."

と書いた説明で自分的には納得しています。

そのことは List(Of T) と Count プロパティ / メソッドに限った話ではなく、以下のような自作のクラスで定義した同名のプロパティとメソッドでも再現できます。

Module Module1
    Sub Main()
        Dim sample As SampleClass = New SampleClass()

        ' SampleClass の Name プロパティが呼ばれる
        Console.WriteLine(sample.Name)

        ' 同じく SampleClass の Name プロパティが呼ばれる。
        ' VB.NET の ( ) は、C# の [ ] と同様に、配列の要素にアク
        ' セスするので、sample.Name(1) は "Property called." の 
        ' 2 文字目の 'r' となる。
        Console.WriteLine(sample.Name(1))

        ' 同じく SampleClass の Name プロパティが呼ばれる。
        ' ( ) は文字列(配列)"Property called." の要素にアクセ
        ' スする。引数 "abc" は Int32 型に変換できないのでビルド
        ' エラーになる。
        Console.WriteLine(sample.Name("abc"))
    End Sub
End Module

Public Class SampleClass
    Inherits SampleBaseClass
    ' Shadows キーワードを付けないと以下の警告が出るが、付けな
    ' くても結局は shadow される。
    ' 「property 'Name' は、function 'Name' とベース class 
    ' 'SampleBaseClass' で競合しています。'Shadows' として宣言
    ' されなければなりません。}
    Public ReadOnly Property Name() As String
        Get
            Return "Property called."
        End Get
    End Property
End Class

Public Class SampleBaseClass
    Public Function Name(ByVal i As Int32) As String
        Return "Instanse Method called."
    End Function
End Class

Public Module MyExtension
    <System.Runtime.CompilerServices.Extension()>
    Public Function Name(ByVal x As SampleBaseClass, _ 
                         ByVal s As String) As String
        Return "Extension Method called."
    End Function
End Module

なお、C# の場合はこのような問題なく、以下のコードでいずれも期待した通りの結果 6 を取得できます。

IEnumerable<int> e = Enumerable.Range(1, 10);

Console.WriteLine(e.Count(n => n >= 5));

List<int> x = e.ToList();

Console.WriteLine(x.Count(n => n >= 5));
Console.WriteLine(Enumerable.Count(x, n => n >= 5));

Tags: , ,

.NET Framework

About this blog

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

Calendar

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

View posts in large calendar