mikeo_410


SQLiteとDataGridのバインド

  1. JPN.mdbの読み取り
  2. System.Data.Sqlite、Linq
  3. LINQ to SQL クラス
  4. LINQ to SQL クラスの自動生成
  5. SQLite の dbml

  で、試したことから、データベースを使う方法には次の2つがあるようです。

  1. dbmlファイルを用意して、DataContext でアクセスする。
  2. データソースの追加で作られる xsd を使って、テーブルアダプターでアクセスする。

  この話は、SQLite に限ったことではありません。1.の方法では、LINQ流の記述で自由にクエリを書くことができました。
  こんどは、更新、追加、削除を見ようと思います。

1.DataGrid に繋いで見る

1.1.DataGrid の準備

  1. WPF Toolkit をインストールします。
  2. Visual Studio のソリューションエクスプローラで Window1.xaml をダブルクリックして開きます。
  3. XAML記述ではなく「デザイン」の方を表示します。
  4. 「ツールボックス」から「DataGrid」を貼りつけます。
  5. XAML記述に切り替えて編集します。
    1. <Window x:Class="WpfApplication3.Window1"
    2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    4.     Title="Window1" Height="300" Width="300"
    5.     xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit">
    6.     <Grid Name="LayoutRoot" Loaded="Grid_Loaded">
    7.         <my:DataGrid AutoGenerateColumns="True" Margin="0,0,0,0" Name="dataGrid1"
    8.                      CanUserAddRows="True" CanUserDeleteRows="True"
    9.                      HeadersVisibility="All" 
    10.                      RowEditEnding="dataGrid1_RowEditEnding"/>
    11.     </Grid>
    12. </Window>
  6. WPF Toolkit のネームスペースの宣言は、自動的に挿入されています。
  7. DataGrid には、AutoGenerateColumns="False" が設定されますが、Trueにして、自動的にテーブルの内容を反映するようにします。
  8. データベースへの変更を試すので、CanUserAddRows="True"、CanUserDeleteRows="True"、HeadersVisibility="All" を追加しました。
  9. DataGrid に変更があればデータベースに反映させたいので、CurrentCellChanged イベントを使います。

1.2.DataContext でのアクセス


  「SQLite の dbml」のテスト用のコードを修正して、検索結果をDataGridに表示して見ます。

  表示は問題ありませんが、編集することができません。
  rows2 のプロパティが読み取り専用だからです。

  また、rows2はテーブルそのものでもないので、これを編集したとしてもデータベースの更新にはコードを書く必要があります。
  LINQ の検索、表示は魅力的ですが、更新はどうなのでしょうか。

  1. private void Grid_Loaded(object sender, RoutedEventArgs e)
  2. {
  3.     System.Data.IDbConnection con
  4.         =new System.Data.SQLite.SQLiteConnection(
  5.             "DbLinqProvider=Sqlite; Data Source=sample.sqlite");
  6.     DataClasses1DataContext db = new DataClasses1DataContext(con);
  7.     var rows2 = from t1 in db.Table_1 
  8.                 select new { t1.title, t1.Table_2.content };
  9.     dataGrid1.ItemsSource = rows2;
  10. }

1.3.テーブルアダプタでのアクセス


  今度は、データソースの追加操作で作られる xsd を使って、テーブルを表示してみます。
  一方のテーブルをそのまま表示しているので、前の例とはフィールドが異なります。

  この例は、取得したテーブルをそのまま DataGrid の ItemsSource に設定しています。
  この場合は、DataGrid は編集可能になっています。セルの編集、行挿入、行削除ができます。

  しかし、これだけでは、データベースの内容は更新されません。
  やはり、データベース更新のトリガが必要なようです。

  1. sampleDataSetTableAdapters.Table_1TableAdapter adp1;
  2. sampleDataSet.Table_1DataTable table;
  3. private void Grid_Loaded(object sender, RoutedEventArgs e)
  4. {
  5.     adp1 = new WpfApplication3.sampleDataSetTableAdapters.Table_1TableAdapter();
  6.     table = new sampleDataSet.Table_1DataTable();
  7.     adp1.Fill(table);
  8.     dataGrid1.ItemsSource = table;
  9. }

  DataGrid の CurrentCellChanged イベントに以下のように記述するとデータベースも更新できました。

  1. private void dataGrid1_CurrentCellChanged(object sender, EventArgs e)
  2. {
  3.     adp1.Update(table);
  4. }

  ただし、これはテーブル単体での修正なので、もっとデータベースらしくする方法がありそうです。

2.何をしたいのか表現して見る

  LINQでスマートに(変数を扱うように)データが更新できることを期待しているわけですが一向に方法がわかりません。
  そこで、考え付く方法で試してみます。

  1. テーブルアダプタを使う。
  2. 2つのテーブルを関連付け、コレクションを作る。
  3. このコレクションを DataGrid のソースとする。
  4. DataGrid 上で変更があった場合、コレクションが修正されるので、これをテーブルに反映する。
  5. この際には、SQLite の自動発番機能を考慮する。

2.1.データベースの内容の表示

  まず、表示にバインドするデータソースの行を表すクラス Class1 を作ります。

  1. using System.ComponentModel;
  2. namespace WpfApplication3
  3. {
  4.     public class Class1 : INotifyPropertyChanged
  5.     {
  6.         //自動生成のINotifyPropertyChangedの実装
  7.         #region INotifyPropertyChanged メンバ
  8.         public event PropertyChangedEventHandler PropertyChanged;
  9.         #endregion
  10.         //プロパティ変更通知のサブルーチン
  11.         void SendPropertyChanged(string propertyName)
  12.         {
  13.             if ((PropertyChanged != null))
  14.                 PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  15.         }
  16.         //引数のないコンストラクタ。DataGridが行挿入に使用する。無いと挿入操作ができない。
  17.         public Class1()
  18.         {
  19.         }
  20.         //最初の表示に使うコンストラクタ
  21.         public Class1(long id, string title, long content_id,string content)
  22.         {
  23.             this._id = id;
  24.             this._title = title;
  25.             this.content_id = content_id;
  26.             this._content = content;
  27.         }
  28.         // content_id は、両方のテーブルにあり同じ値を保持する
  29.         // この値は、表示対象でない
  30.         public long content_id;
  31.         public void SetContentId(long content_id) { this.content_id = content_id; }
  32.         // id は、データベースが発番する。読み取り専用項目。
  33.         // 行挿入時には、データベースが発番した値を表示する必要があるがメソッドで実装
  34.         object _id;
  35.         public object id { get { return _id; } }
  36.         public void SetId(long id) { _id = id; SendPropertyChanged("id"); }
  37.         string _title;
  38.         // title
  39.         public string title
  40.         {
  41.             get { return _title; }
  42.             set { _title = value; SendPropertyChanged("title"); }
  43.         }
  44.         // content
  45.         string _content;
  46.         public string content
  47.         {
  48.             get { return _content; }
  49.             set { _content = value; SendPropertyChanged("content"); }
  50.         }
  51.     }
  52. }

  2つのテーブルの関係を以下のようにして、itemsSource に移して dataGrid1.ItemsSource にセットします。

  1. //Table_1 のアダプター
  2. sampleDataSetTableAdapters.Table_1TableAdapter ta1;
  3. //Table_2 のアダプター
  4. sampleDataSetTableAdapters.Table_2TableAdapter ta2;
  5. //DataGridにバインドするコレクション
  6. ObservableCollection<Class1> itemsSource = new ObservableCollection<Class1>();
  7. private void Grid_Loaded(object sender, RoutedEventArgs e)
  8. {
  9.     ta1 = new WpfApplication3.sampleDataSetTableAdapters.Table_1TableAdapter();
  10.     ta2 = new WpfApplication3.sampleDataSetTableAdapters.Table_2TableAdapter();
  11.     using (sampleDataSet.Table_1DataTable table_1 = new sampleDataSet.Table_1DataTable())
  12.     {
  13.         ta1.Fill(table_1);
  14.         for (int i = 0; i < table_1.Rows.Count; i++)
  15.         {
  16.             long c_id = Convert.ToInt64(table_1.Rows[i]["content_id"]);
  17.             using (sampleDataSet.Table_2DataTable table_2 = new sampleDataSet.Table_2DataTable())
  18.             {
  19.                 ta2.FillBy(table_2, c_id);
  20.                 itemsSource.Add(
  21.                     new Class1(
  22.                         (long)table_1.Rows[i]["id"],
  23.                         (string)table_1.Rows[i]["title"],
  24.                         c_id,
  25.                         (string)table_2.Rows[0]["content"]));
  26.             }
  27.         }
  28.     }
  29.     dataGrid1.ItemsSource = itemsSource;
  30.     itemsSource.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(itemsSource_CollectionChanged);
  31. }
  1. Table_1 の id は、データベースが発番する値で、新しい行を入力するときは空にしたいので object 型にする。
    null なら空で表示される。
  2. Table_1 の id は、set がないので、読み取り専用になる。
  3. DataGrid は、引数のないコンストラクタがあると、挿入可能と見なす。
  4. Table_2から content_id で一行読み出すように FillBy() を追加した。(後述)
  5. itemsSource は、ObservableCollection<Class1>型にする。CollectionChanged イベントを利用するためで、List<Class1> でも行削除以外は可能。

2.2.xsd にメソッドを追加

   前述の FillBy() など、必要なデータベースへの操作メソッドを追加して置きます。 
  「JPN.mdbの読み取り」に書いた手順で、sampleDataSet.xsd を開いて行います。
   sampleDataSet.xsd は、sample.sqlite データベースを「新しいデータソースの追加」を行った結果、自動生成されたものです。

  それぞれのメソッドのプロパティに、引き数とSQL文を設定します。引数は、Parameters、SQL文は、CommandText に設定されます。
  以下に、行削除や更新に使うメソッドと合わせて記して置きます。

Table_2の1行読取 int FillBy(sampleDataSet.Table_2DataTable dataTable,
long content_id)
SELECT [content], [content_id] FROM [Table_2] WHERE ([content_id]=@content_id)
Table_1の挿入 object Insert_LastRowId(string title, decimal content_id)

INSERT INTO  Table_1 (title,content_id)VALUES (@title,@content_id); 
SELECT last_insert_rowid() AS id;

Table_2の挿入 object Insert_LastRowId(string content)
INSERT INTO Table_2(content) VALUES (@content);
SELECT  last_insert_rowid() AS content_id;
Table_1の行削除 int DeleteQuery(long id)
DELETE FROM [Table_1] WHERE ([id] = @id)
Table_2の行削除 int DeleteQuery(long content_id)
DELETE FROM [Table_2] WHERE ([content_id] = @content_id)
更新 int UpdateQuery(long id, string title, decimal content_id, string content)

UPDATE [Table_1] SET  [title] = @title WHERE ([id]=@id);
UPDATE [Table_2] SET [content]=@content WHERE ([content_id]=@content_id);

2.2.行の挿入と更新

  content_id は、Table_2 に行を挿入した結果として得られる、データベースが発番する値です。これは、並列処理などでも一意性を保つようにするためです。
  したがって、DataGrid 上で1行挿入されたら、まず Table_2 を書き込み、発番された値を取得して、Table_1 の content_id に設定します。
  その上で、Table_1 の書き込みます。

  追加したメソッドを使って、DataGrid の RowEditEnding イベントで以下のように処理しました。
  最初から表示されている最下段の空行にデーターを入れます。Enter キーを押したり、別な行を選択したりするとイベントが発生します。
  新しい行は、Class1 の引数なしコンストラクタで作成されて、id が null になっていることを利用しました。

  1. private void dataGrid1_RowEditEnding(object sender,
  2.     Microsoft.Windows.Controls.DataGridRowEditEndingEventArgs e)
  3. {
  4.     Debug.WriteLine("RowEditEnding");
  5.     Class1 c1 = (Class1)e.Row.Item;
  6.     if (c1.id == null)
  7.     {
  8.         //新しい行
  9.         long new_content_id = (long)ta2.Insert_LastRowId(c1.content);
  10.         long new_id = (long)ta1.Insert_LastRowId(c1.title, new_content_id);
  11.         c1.SetId(new_id);
  12.     }
  13.     else
  14.     {
  15.         //行の内容が変更された
  16.         ta1.UpdateQuery((long)c1.id, c1.title, c1.content_id, c1.content);
  17.     }
  18. }

  id が null でない時には、更新をします。2つのテーブルが1つのトランザクションとして処理されることを期待して1つのメソッドにしました。
  このイベントはキャンセル可能なようです。必要なら確認の問い合わせができます。

2.3.行の削除

  DataGrid のイベントから行削除に使えるものが見つからないので、ObservableCollection の CollectionChanged イベント を使います。
  行選択が1行にできなかったので、複数行の削除のために繰り返しになっています。
  行削除は、行を選択して、Delete キーを押すと行われます。

  1. void itemsSource_CollectionChanged(object sender, 
  2.     System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  3. {
  4.     if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
  5.     {
  6.         //行の削除(複数行)
  7.         object[] o = (object[])e.OldItems.SyncRoot;
  8.         for (int i = 0; i <o.Length; i++)
  9.         {
  10.             Class1 c1 = (Class1)o[i];
  11.             ta1.DeleteQuery((long)c1.id);
  12.             ta2.DeleteQuery(c1.content_id);
  13.         }
  14.     }
  15. }

  このイベントは、削除されてから呼び出されるものと思います。削除の確認操作が必要なら、削除がキャンセルされたら、データを追加するなど工夫が必要です。
  このイベントの Add のときに、行挿入することはできません。イベントが発生するのは、DataGrid が 空行を挿入したときでした。

3.LINQ のデータベース更新処理

  まだ、わかりません。
  複数のテーブルから抽出したフィールドからなるコレクションをバインドすれば表示されることは便利ですが、更新する仕組みは想像が付きません。前述の例だと、表示の行削除は2つのテーブルの行削除になっていますが、注文レコードを消すと顧客が消えるのでは困るので一様ではないことが推測できます。
  なにか汎用的な仕組みがあるのでしょうか。


mikeo_410@hotmail.com