プログラミング: 2014年9月アーカイブ

前にも書いたように、.NET 系の言語(VBとC#)でデータセットを使ってほげほげという開発経験がないので、日々発見の毎日である(^^;
データセットのメソッドやプロパティを全て理解しているわけではないので、あるメソッドを使えば簡単に解決することを、ロジックでごにょごにょしたり、まあ、そういう無駄がまだ多い(^^;
今の仕事の契約が切れる頃にバッチリになりそうで若干申し訳ないなと思いつつ(^^;;;

データテーブルの中の何件かのレコードのフラグを変更する・・・なんて処理で、例えば keys() という配列に対象レコードのキーが入っているとして、

For i = 0 To dt.Count - 1
For j = 0 To keys.Length - 1
If dt(i).id = keys(j) Then
dt(i).chkFlg = 1
Exit For
End If
Next
Next

なんて多重ループにしてたんだけど、レコード数が増えたら遅くなるわなあ・・・と(^^;
実は、こんな風にループして総舐めにしなくても、ちゃんと更新対象を指定できるメソッドが DataSet クラスにはある。

FindBy だ。(ただし、データセットできちんと主キーの設定がされていないと使えない)

ちゃんと主キーとして指定されている項目でレコードを絞り込める環境であれば、

For j = 0 To keys.Length - 1
Dim row As DataSet.hogeRow
row = dt.FindByid(keys(j))
row.chkFlg = 1
Next

という感じで組める。
これならループが1個で随分処理がシンプルになる。

メソッド知らなくても、こまめにググればええやんという話だが、最初はその余裕すらなかったわ(^^;

<追記>
ちなみに、主キー設定がなければ、Select メソッドで対象レコードを絞り込めばよし。
ただ、結果が配列で返ってくるなど、若干動きは変わってくるが。
VB.NET で、データセットから作成したビューのレコードを、ループさせながらインデックス指定で直接削除すんなよという話。

インデックスを指定してレコードを削除する場合も、例えば、

view.AllowDelete = True
Do
    Dim i As Integer = view.Find("消したいレコードに含まれているキーワード")
    If i <> -1 Then
        view.Delete(i)
    Else
        Exit Do
    End If
Loop

みたいな使い方であれば問題ないんだけど、ループしながらインデックスをアップしつつ消しちゃうと、その瞬間にレコードが削除されたことでその後のレコードのインデックスが変化してしまい、正常にデリートできない(削除予定のレコードが並んでいた場合、次のレコードを飛ばしちゃうことになっちゃう)という話。
ビューの Delete メソッドは直接レコードを削除しちゃうんだよね。

例えば、

view.AllowDelete = True
Dim max As Integer = view.Count - 1
For i = 0 To max
    if view(i).hoge = "消したいレコードに含まれているキーワード" Then
        view(i).Delete()
    End If
Next

みたいにしちゃうと、view(i).Delete() しちゃった時点で、以降のレコードのインデックスがひとつ減っちゃう。
本来、view(i + 1) だったレコードが view(i) になっちゃうということ。
つまり、上に書いたように、削除予定のレコードが並んでた場合、削除したレコードの次のレコードの処理は飛ばされてしまうということだ。あかん。

どうしてもビューの中身を頭から舐めながら処理を行いたい場合は、一旦データテーブル等にコピーして処理を行う必要がある。

Dim dt As HogehogeDs.hogeDataTable = CType(view.Table, HogehogeDs.hogeDataTable)
Dim max As Integer = dt.Count - 1
For i = 0 To max
    If dt(i).hoge = "消したいレコードに含まれているキーワード" Then
        dt(i).Delete()
    End If
Next
dt.AcceptChanges()
view = dt.DefaultView

みたいな感じ。

データテーブルの Delete メソッドはビューと違って「実際に削除するのではなく、削除マークをつけるだけなので、最後に AcceptChanges するまではインデックスが変化することもなく、問題なく最後まで削除処理を行うことができる。

これも、知らずにやっちゃうと相当ハマる。特に、レコード件数が多い時はデバッグで問題を見つけ出すのも大変(^^;
今やってる仕事、帳票の出力には VB-Report 8 を使っている。
これ、帳票定義が Excel シートで作成出来るスグレモノなんだけど(Excel でセルを結合したり、罫線を引いたりして帳票のデザインを行ったら、あとは簡単なプログラムで帳票出力できる)、意外にネット上に情報がなくて俺みたいな初めて使うもんには困るわあ。

情報が無いっつうか、古いんだよな。だいたい、Ver 3 の頃の情報ばっか引っかかる。

開発ツールについてきてるヘルプを使えば良いっつう話なんだけど、このヘルプがこれまたなかなか情報までたどり着きづらいというか(^^;(あくまで、俺みたいに初めて使う人間の意見だけど)

Ver 3 の頃と 8 では大きくプロパティが変わってるんだけど、ヘルプの検索でもなかなか引っかからなくて弱った(^^;
例えばプログラムの中で動的に「セルの結合」がしたかったんだけど、「キーワード」や「検索」で「セル結合」とか入れてもなんもヒットせんし(^^;

結局、

【セルの結合】
3.0 Report.Pos(x1, y1, x2, y2).Attr.Joint = True
 ↓
8.0 Report.Pos(x1, y1, x2, y2).Attr.MergeCells = True

みたいに変わってたんだけど、「Joint」とか入れてもヘルプじゃなんもヒットせんしさあ。
こういうの、過去のバージョンの内容と変わったものについてもっと考慮してほしいなあ。

【縮小して全体表示】
3.0 Report.Pos(x1, y1).Attr.Fit = True
 ↓
8.0 Report.Pos(x1, y1).Attr.ShrinkToFit = True

これも、「Fit」じゃ何の情報も見つからなくて、もちろん「ShrinkToFit」って入れればヒットするんだけど(^^;
「ShrinkToFit」がわかんないから検索してんのに(^^;

まあ、さすがにこれだけ名前が変わってるってのがわかれば、PosHorz は、プロパティの一覧を頭から舐めていって、ああ、この HorizontalAlignment に変わったんだろうなあと想像はつくが・・・

ちなみに、中央揃えは、

Report.Pos(x1, y2).Attr.HorizontalAlignment = VBReport8.HorizontalAlignment.Center

だ。
相変わらず VisualBasic の話。

やりたかったことは、例えば

○ 全部表示
○ カテゴリAの商品だけ表示
○ カテゴリBの商品だけ表示

みたいなラジオボタンがあって、それを選択することで、そのボタンの内容に見合った一覧を DataGridView に表示したいということ。

DataGridView には BindingSource 経由で DataView の内容がバインドされている。

方法としては、何の検索(絞込)も行っていない状態の DataView をローカル変数で保持しておいて、「カテゴリBの商品だけ表示」ラジオボタンがチェックされたら、

  1. ローカル変数に保持していた全件が含まれた DataView を新しい DataView にコピー
  2. その新しい DataView で「カテゴリBの商品だけ」を検索(絞込)
  3. その結果を、BindingSource.DataSource にセット

という処理をするだけ。

で、この時、「ローカル変数に保持していた全件が含まれた DataView を新しい DataView にコピー」の部分を、

Dim newDsv As DataView = BaseDsv(←ローカル変数で保持してる DataView)

みたいに書いてはいけません・・・ということなんじゃね(^^;
これ、BaseDsv を newDsv に代入するって意味じゃなくて、newDsv と BaseDsv を関連付けるって意味なのね(^^;

これで、

Dim newDsv As DataView = BaseDsv
newDsv.RowFilter = "category = 'a'"

なんてやっちゃうと、BaseDsv の中身も RowFilter で絞り込んだ後の内容になっちゃう(^^;

こういう時は、

Dim newDsv New DataView
newDsv.Table = BaseDsv.Table.Copy()
newDsv.RowFilter = "category = 'a'"

ってやらなきゃダメ。
これで、newDsv だけが絞りこまれた内容になって、BaseDsv の内容は元のまま残る。
VB.NET の仕様というより、オブジェクト指向的な問題やね(^^;

こういう発見が日々あって楽しいんだけど、おかげでプログラミングは遅れ気味な今日このごろであった・・・(^^;;;;
VB.NET はちょっとしたツール(定例処理で使う超シンプルな FTP クライアントとか)を作るのに使ってたので(ちいさなプログラムでも、それなりに内部的にはややこしいことをしてたしな)、コーディング自体は問題なかったのだが、実は DB がらみのプログラムは全然書いたことがなかった。
なので、DB のデータを DataSet にセットして云々かんぬんなんてやったことなかったわけよ(^^;

なので、7月、8月に作ったプログラムって、無駄なことしてるわあ(^^;

ある DataSet の中身を並び替えて DataGridView にセットするんだけど、

' 並び替えのため DataSet を View にセット
Dim hogeDv As DataView = hogeDs.t_hoge.DefaultView

' 並びかえ(あのコード1、この日付1)
hogeDv.Sort = "ano_code1, kono_date1"

' 並び替えを行った View を新しい DataSet にコピー
Dim newDs As New HogeDataSet
For Each drv As DataRowView In hogeDv
    Dim dr As HogeDataSet.t_hogeRow = CType(drv.Row, HogeDataSet.t_hogeRow)
    Dim ndr As HogeDataSet.t_hogeRow = newDs.t_hoge.Newt_hogeRow
    ndr.ItemArray = dr.ItemArray
    newDs.t_hoge.Addt_hogeRow(ndr)
Next
' 更新の反映
newDs.AcceptChanges()

Me.HogeBindingSource.DataMember = newDs.t_hoge.TableName
Me.HogeBindingSource.DataSource = ds

みたいに、わざわざ View の内容を新しい DataSet にセットしてから BindingSource にバインドしてたけど、そんなことする必要なかったんやなあ。
別に View のままで突っ込めたんや。
この頃は、DataSet がらみのクラスのプロパティの内容とかあまり理解してなかったからなあ(^^;

実際は、

' 並び替えのため DataSet を View にセット
Dim hogeDv As DataView = hogeDs.t_hoge.DefaultView

' 並びかえ(あのコード1、この日付1)
hogeDv.Sort = "ano_code1, kono_date1"

Me.HogeBindingSource.DataMember = hogeDv.Table.TableName
Me.HogeBindingSource.DataSource = hogeDv

これでええんやな。

恥ずかしいから、過去のコードもこっそり直しとかないと(^^;(まあ、なんの問題もなく動いてるので、そんなにじっくりソースを見られちゃうこともないだろうけど)
結局、これってミリ秒までマッチングする必要がないのなら、格納時に、

Dim hogeTime As DateTime = Now

じゃなく、

Dim hogeTime As DateTime = CDate(Now.ToString)

みたいにして、ミリ秒を削った時間(まあ、実際は 0ミリ秒になるってことだけど)をセットしてやれってことか。

こうすると、hogeTime には実際の時刻が 2014/09/04 18:37:36.5685248 であろうと、2014/09/04 18:37:36.0000000 という値がセットされるので、

dv.RowFilter = "hoge_time = #" & hogeTime & "#"

という条件でマッチングするようになる。

dv.RowFilter = "hoge_time >= #" & hogeTime.ToString & ".00000# Or hoge_time <= #" & hogeTime.ToString & ".99999#"

みたいに長ったらしい条件書かなくてもいい。

つーか、この長ったらしい条件、やっぱダメだよね。
例えば、これじゃあ 2014/09/04 18:37:36.9999999(小数点以下 7桁)の時間はマッチしないよね。条件が <= 2014/09/04 18:37:36.99999(小数点以下 5桁)だから。18:37:36.9999900 ってことだもんね。
いや、それは小数点以下の桁数を 7桁にして <= 2014/09/04 18:37:36.9999999 となるようにすればいいやん・・・と思うかもしれんけど、小数点以下の最大桁数って、これ、絶対固定なの?

今、仕事で使っている Windows 8.1 Pro(64bit)の VB.NET で確認すると、7桁まで取れるようなんだけど(ToString("yyyy/MM/dd HH:mm:ss.FFFFFFFF") みたいにミリ秒以下を 8桁にすると例外エラーで落ちる)、これ、32bit 版でも一緒なの?あるいは、今後 CPU のビット数や OS のバージョンが上がっても、絶対 7桁なの?12桁とかにならないの?

そのあたりが担保できない限り、やっぱミリ数は含めるべきじゃないよね。青天井で桁数増えていく可能性あるんだから。この辺、「Microsoft の勝手」の世界だよね?
今は <= 2014/09/04 18:37:36.9999999 が有効でも、何年か後はわからんってことよね。

ということで、時間は CDate(Now.ToString) して突っ込んじゃうのが一番安全そう。

「業務アプリケーション」でミリ秒まで必要なケースって糞レアだからな(笑)
そういえば、トランザクションって DB を CloseConnection した後も生きてるんだっけ?

あるフレームワークを使って VB.NET の開発をしてるんだけど、DB を参照しかしない場合も、DB への OpenConnection 後で BeginTransaction が実行される。

まあ、DB を更新をした場合は、必ず CommitTransaction か RollbackTransaction 実行して処理を抜けるんだけど、参照しかしなかった場合はその処理を書いてなかった。
というのも、処理は全て try/catch/finally 文で囲まれていて、finally で DB への CloseConnection 処理を実行してるので「DB から切断するからトランザクションも終了するわな」とか思ってた(^^;
でも、実際には終わってなくて、次に BeginTransaction した時に、「トランザクションが開始されてます」のエラーになっちゃう。

いやあ、俺がまだこのフレームワークに慣れてないのと、他にちょっと疑わしい点があったので、結局そっちの的外れな方をいろいろ調べてて、今日の午前中を無駄に過ごしてしまった(^^;

しかし、DB から切断しても Transaction って生きてるものなのね(^^;(時間が経つとタイムアウトなのかなんなのか自動で死んじゃうみたいだけど)
これまで、ずっとそんなことは意識せず生きてきたわ。
てか、自分でフルスクラッチでプログラム作る時は、参照だけの時にもトランザクションを実行するなんてことないからな(^^;

やっぱ、たまに他社のプロジェクトに入ると色々勉強になるなあ(^^;
あと、VB.NET で「え?こんな面倒なことしないといかんの?」という話だと、「バイト数での文字列の分割」かなあ。

「全角文字を途中でぶった切ってしまうことは在り得ない」という条件での処理なので、「文字列の最後が『全角文字の前半分の1byte』じゃないか?」みたいなややこしいチェックはしないでいい。だから、純粋に「byte 数で文字列を分割できる split や substr」みたいなのがあれば嬉しかったんだけど、無いのね、そんなの(^^;
ちゃんと「byte 数」じゃなく「文字数」としてアレコレ処理してくれるようで、有難迷惑だなあ(^^;

今回は、

'わたしは慎吾   00100100'
'あの子はチンコ 00001110'
'あたいは茜     00101011'

みたいなデータ(固定長)を、15byte と 8byte にぶった切りたいだけ。
例えば、「わたしは慎吾   」と「00100100」という具合に。

あれこれ試して、

Dim bytes() As Byte = System.Text.Encoding.GetEncoding("Shift_JIS").GetBytes(hogehoge)
Dim byteF() As Byte = CType(Array.CreateInstance(GetType(Byte), 15), Byte())
Dim byteR() As Byte = CType(Array.CreateInstance(GetType(Byte), 8), Byte())
Array.Copy(bytes, 0, byteF, 0, 15)  ' 1~15byte
Array.Copy(bytes, 15, byteR, 0, 8)   ' 16~23byte
Dim strF As String = System.Text.Encoding.GetEncoding("Shift_JIS").GetString(byteF)
Dim strR As String = System.Text.Encoding.GetEncoding("Shift_JIS").GetString(byteR)

※hogehoge に「わたしは慎吾   00100100」のような文字列が入っている。

とかしたんだけど、上に書いたように「全角コードを跨ぐことはない」ので「全角コードである」ということを意識する必要もないのに、なんか、Shift_JIS エンコードしてからバイト文字列に分割したりと面倒臭いことこの上無い。

もっとスマートな方法があると思うので、ぜひ、VB.NET マスターの方はコメントを!!(笑)
VB.NET の話。

最終的にソートをしたいので、DataSet から

Dim dv As DataView = ds.hoge_table.DefaultView

という具合に DataView を作成し、その中からある日時のデータ(行)だけ抜き出そうとした。
抜き出した後にソートしたいわけね。

具体的には、send_time というカラムに 2014/09/03 17:51:29 と入っている行を抜く。

そこで、

dv.RowFilter = "send_time=#" & hoge_time & "#"

としてみたが、抜き出した行数 0 なのである。(hoge_time 変数には 2014/09/03 17:51:29 という時間が DateTime 型で格納されている)

イミディエイトウィンドウで RowFilter の内容と、データを(試しに200件目のデータを)確認すると、

?dv.RowFilter.ToString
"send_time=#2014/09/03 17:51:29#"
?ds.hoge(200)(68).ToString
"2014/09/03 17:51:29"

と、どっちも 2014/09/03 17:51:29 やないけ!!?
なんで抜けんの?

・・・と少々ハマったが、どうやら DataSet に入っている日時はミリ秒まで格納されているらしい。
ToString では表示されないけど。

なので、秒までの時間でマッチングさせようとすると、

dv.RowFilter = "send_time >= #" & hoge_time.ToString & ".00000# Or send_time <= #" & hoge_time.ToString & ".99999#"

としないといけないようだ。
これで、RowFilter は、

?dv.RowFilter.ToString
"send_time >= #2014/09/03 17:51:29.00000# Or send_time <= #2014/09/03 17:51:29.99999#"

こうなって、無事マッチングした行が抜き出せたのである。

面倒くせぇ~!!(^^;

なんか、他に方法があるのかもしれないが、結局見つけ出せず。
「もっとスマートな方法があるぜ!」という .NET マスターは、ぜひここにコメントを!(笑)

このアーカイブについて

このページには、2014年9月以降に書かれたブログ記事のうちプログラミングカテゴリに属しているものが含まれています。

前のアーカイブはプログラミング: 2014年7月です。

次のアーカイブはプログラミング: 2014年10月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。


月別 アーカイブ

電気ウナギ的○○ mobile ver.

携帯版「電気ウナギ的○○」はこちら