C#のDataGridViewコントロールで作る習作として、IDListを選択したこと、前回書きました。その後ブログをさぼっていましたが、実は
IDList for CS
はほぼ完成しています。
なので、
前にやる、と言っていたトピックがカバーされているかチェックしました。
(1)DataGridView
これは既に基礎的な話をここでChat-GPTに語ってもらっていますが、DataGridViewは(Explorerで使われている)ListViewのように一覧、詳細、並べて等表示を変化させられない反面、表として特化した機能と使いやすさが身上で、特に自分自身ではデータを持たず、DataSourceを使ってデータソースのデータを基に表示するエコなところに特徴があります。技術的なところでいえば、BindingSourceを使うことにより、UIのDataGridViewとデータソースとを「相互にデータ同期(注)」させることで、ソースのデータを変更しても、UIのDataGridViewを編集しても同じデータを共有できることになります。
注:BindingSourceのResetBindings()とEndEdit()メソッドを使います。
しかし、この「データ同期」に気を取られ過ぎると、データが同期されない現象をDataGridViewやBindingSourceの問題として捉えがちになり、大騒ぎのデバッグの末、単純なファイル読み書きの問題が原因だった、なんてこともありますます。なお、ListクラスとBindingSourceクラスを組み合わせたような、BindingListクラスがありますが、両者の違いやメリデメの詳細は末尾の対話(【BindingListのお話】)をご参照ください。
(2)List+オブジェクト配列のソート等
Listは簡易データベースのようにしか扱ってこなかったので、この問題も私にとっては初めての体系的学習でした。詳細は以下↓で。
(3)ファイルの暗号化
これはオリジナルのC++とBCCSkelton製のIDListが、MicrosoftのDPAPIを利用した(注)ので、一応AES等他の暗号化方法も調べ、Chat-GPTとも相談し、最適と考えられる.NETベースのDPAPIを使うことにしました。
注:【DPAPI】プログラム設計 および【DPAPI】DPAPIProc.hとDPAPIの実行参照。
C++版ではDPAPIはパスワード保護も追加されたダイアログベースの呼び出しが可能でしたが、.NETではこのダイアログ付は利用できませんでした。(泣;)詳しくは(いずれ公表する-笑)IDList for CSのコードをご覧ください。
(4)ファイルを隠す(ファイル属性)
これは単純にファイル属性の問題です。IDListのシステムデータファイルは「隠しファイル属性」を付けていますが、そうすると上書きができないことがあるようです。(【DataGridView】すったもんだの末、爆!参照)この話って、2021年のオリジナル版IDListで既にカバーしていたんですね。忘れていました。
今回Chat-GPTと話した問題はファイル属性を「隠しファイル属性」にしても、Explorerを「隠しファイルを表示する」設定にすれば、そのファイルは(色が薄くなるだけで)見えてしまいます。(おそらく当時私は調べて)それに「システムファイル属性」を与えたんだと思いますが、この両者が加わるとExplorerでも全く見えなくなります。
この為、「あるんだか、無いんだかわからない」状況である筈のファイルを自作のファイル属性編集ツール(Attrib.exe-これこのブログでやったと思っていたのですがやっていなかったですね)
で「システムファイル属性」をのぞいたら、見事見えるようになった、というお話です。
ところで、
前回書きませんでしたが、実はもう一つトピックが生まれ、昨日は一日Chat-GPTと論議をしておりました。それは、
(5)C#のSplitメソッドでは、IDListが正しく動かない!
という問題で、これは元々予見していたので驚きはありませんでした。
どういう事かと言うと、IDListのシステムデータでは最後音4つ目の列に「備考」がありますが、一般的な文章を書くのでこれにはタブ(\t)や改行(ウィンドウズなので\r\n)または','(英文句点)が入れられなければなりません。しかし、この仕様ではデータの内「区切り文字である改行(ウィンドウズなので\r\n)と','(英文句点)」が文字列データの中に入ってしまいます。残念ながらC#のSplitメソッドはそんなに賢くない為、どのデータ列では区切り文字を無視しろ、という指定はできません。
同じ話は既にオリジナルのIDListで経験済であり、当時は↓のようなコードで「CSVデータ文字列strを引数で与え、それをカンマで切り取って戻り値とするだけではなく、括弧("")で括られた文字列はカンマや改行記号を含めて戻り値で返す」という関数を使っていました。
////////////////////////////
//カンマ区切り文字切り出し
//★str 文字列は切り出した文
//字列だけ短縮されるので注意
////////////////////////////
char* GetDelimtStr(char* str) {
static char buff[MAX_PATH * 4]; //取り敢えず1K bytes確保
char *cp = str;
int len = 0;
switch(*cp) {
case 0:
return 0; //NULL終端を指していれば単に帰る(FALSE)
case '\"': //「""」処理
cp++; //'\"'の次に進める
while(*cp != '\"') {
buff[len] = *cp; //buffへのコピー
cp++;
len++;
}
cp++; //cpは'\"'の次の','へ進める
buff[len] = '\0'; //buffのNULL終端処理
break;
default: //「""」以外
//','でも'\n'でもNullでもなければ
while(*cp != ',' && *cp != '\n' && *cp) {
buff[len] = *cp; //strデータをbuffにコピーしてゆく
cp++;
len++;
}
buff[len] = '\0'; //buffのNULL終端処理
break;
}
if(*cp){ //cpがNULL終端を指していなければ
cp++; //','や'\n'を指しているので一つ進め
}
lstrcpy(str, cp); //残余の文字列を文字列先頭に移動
return buff; //切り取った文字列を返す(TRUE)
}
開発時はテスト用なので単にSplitメソッドを使っていましたが、完成版をどうするか、Chat-GPTと相談し始めました。
Q: おはよう。今日は、いつものバグ探しというネガティブな話ではなく、新しいアルゴリズムについての相談(気楽なチャットのつもりで結構です)に来ました。
この間から相談している、IDListですが、いよいよ機能が実装され、オリジナルのC++製のCSVデータ(平文)を喰わせてみたところ、エラーが出ました。これはおそらく括弧("~")で括っている第4列の「備考(仕様として\r\n等のコントロールコードを許す)」mのところでSplitが働いたためであろうと踏んでいます。
オリジナルのC++の場合の処理は次のようなものです。(省略、上記↑参照)
潔くSplitを捨てて、自作のこれをC#に置き換えようか、と考えましたが、折角C#に移植するのだし、C#(余りポインターは得意でなさそうだし)の得意そうな方法に置き換えてはどうかと考え始めました。
現在のアイデアはコントロールコード(00H~1FH)といっても、「備考」で書く文章に使うコントロールコードは
09H(TAB)
0AH(LF)
0DH(CR)
位しかないので、2文字の\r\n(0D0AH)、コピペで持ってきた場合は1文字の可能性もあるので0DHと0AH、後TAB(09H)を「人間が使わない無意味な文字列」に変換して保存し、読み込む際に再変換させることでString.Splitをそのまま使えるのではないか、と思いついた次第。
Chat-GPT君の意見を聞きたいのは、次の二つのバグリスクです。(RM観点での仕様設計考察)
(1)通常の文章の中の単語と変換文字が偶然一緒になった場合の誤変換
(2)IDList以外のテキストエディターなどで見た場合の可能な不具合
(3)その他考えられる誤使用、誤作用
どうでしょう?
抽象的に考えてもなんなので、一番ストレートな変換文字として、
09H→"(TAB)"
0AH→"(LF)"
0DH→"(CR)"
とする例で考えてください。これであれば、(プログラミングサイトの説明文の中でも、このような文字は使わないので)置き換え可能ですし、又「人間の目から見ても」これが本来何であったかが容易に理解できるでしょう?
話はこのように「カンマやコントロールコード等Splitメソッドの区切り文字を一旦他の文字(列)に置き換え、Split後逆変換して元に戻す」ことを「括弧("")内だけ」やろうか?ということで論議しましたが、その後私は考え直し、
(1)括弧("")内を特定し、
(2)一旦区切り文字の変換した上、
(3)Split後、括弧("")内の逆変換を行う
処理は時間がかかりすぎるので、オリジナルの「括弧("")対応付き区切り文字の切り出し処理」で一遍に行った方が速い、という結論に至り、
(1)先ずはオリジナルの関数のアルゴリズムを、C#の特有の性癖(文字列にヌル終端がない、配列の添え字を誤ると厳格に即例外が発生する)を踏まえてC#用に書き直し、Chat-GPTに安全性、効率性の観点からだめだししてもらい、
(2)改善案を練り直した結果、C#専用のGetDelimitStrメソッドを作りました。
これはIDListのコード紹介の際に解説させていただきます。又、次回はテスト中の「IDList for CSの写真集」(アイドルみたい)をやろうかな、と思っています。
【ご参考】
【BindingListのお話】
Q: 追加で一つ質問です。何度か勧奨がありましたが、現在のList+BindingDatasourceをやめてBIndingListにせよとのお話。これは実際List+BindingDatasource=BIndingListと言えるほど同じものですか?IDListでいえば、ListとDataGridViewの同期で必要だったBindingDatasource.ResetBindings(false);とBindingDatasource.EndEdit();がなくなるだけでしょうかね?
A: はい、かなり近い理解ですが、「List<T> + BindingSource と BindingList<T> は同じもの」ではありません。
そしてIDListの現在の構成について言えば、私は以前の「BindingList<T> にした方がよい」という勧奨を少し強く言い過ぎました。今の List<idItem> + BindingSource で問題なく完成しているなら、無理に変更する必要はありません。
まず関係を整理すると
現在は、
List<idItem> idItems;
BindingSource bs;
DataGridView dgv;
で、
List<idItem>
↑↓
BindingSource
↑↓
DataGridView
という構造です。
一方、
BindingList<idItem> idItems;
なら、
BindingList<idItem>
↑↓
DataGridView
と、BindingSource を省略できます。
ただし、BindingList<T> は単なる「BindingSource を内蔵した List<T>」ではありません。
一番大きな違い
List<T> は、例えば、
idItems.Add(item);
しても、
「Listに要素が追加された」
という変更通知をDataGridView側へ送る仕組みを持っていません。
だから現在は、
idItems.Sort(...);
bs.ResetBindings(false);
として、
「Listの中身が変わったから、表示を更新してください」
と BindingSource に明示的に知らせています。
BindingList<T> は、要素の追加・削除について変更通知を発生させる仕組みを持っています。
したがって、
idItems.Add(item);
でDataGridView側にも変更が伝わります。
では ResetBindings(false) は完全になくなるのか?
通常の Add / Remove なら、かなりの部分で不要になります。
例えば、
BindingList<idItem> idItems = new BindingList<idItem>();
として、
idItems.Add(new idItem());
すれば、DataGridViewには追加が通知されます。
しかし、現在のIDListで重要なのは ソート です。
例えば、
idItems.Sort(...)
という処理は、BindingList<T> には List<T> のような Sort() がありません。
したがって、
idItems.Sort(...)
をそのまま置き換えることはできません。
結局、
BindingList<idItem>
に変更したとしても、ソート後には
bs.ResetBindings(false);
に相当する更新処理が必要になる可能性があります。
つまり、
「BindingListにすればResetBindings(false)が全部消える」
ではありません。
EndEdit() はまた別の話です
ここも重要です。
BindingSource.EndEdit() は、
BindingSourceが現在保持している編集状態を確定する
ためのものです。
これは BindingList<T> に変更したから自動的に不要になる、という性質のものではありません。
ただし、潔さんの現在の設計では、
DataGridView上で直接編集させず、専用ダイアログで
idItemを編集する
という方式になっています。
この設計なら、そもそもDataGridViewのセル編集を終了させるための
bs.EndEdit();
の重要性はかなり下がります。
IDListの場合、私はこう考えます
現在:
List<idItem>
↓
BindingSource
↓
DataGridView
変更案:
gList<idItem>
↓
DataGridView
確かに後者の方が単純です。
しかし、IDListではすでに、
- ソート
- 専用編集ダイアログ
- データ読み込み
- データ保存
- 暗号化
- DataGridViewとの同期
- DLLとしての独立性
まで動いています。
ですから、
「BindingListに変えればコードが劇的に簡単になる」ほどの差はありません。
むしろ現在の
List<idItem> idItems
BindingSource bs
は、非常に分かりやすい構成です。
特に、
idItems.Sort(...);
bs.ResetBindings(false);
というコードは、
「内部データを並べ替えた。だから表示を更新する」
という意図が明確です。
これは悪いコードではありません。
したがって結論
質問の
List + BindingSource = BindingListと言えるほど同じものか?
については、
「DataGridViewにリストをバインドする」という目的では非常に近いが、内部的には別物
です。
そして、
ResetBindings(false)とEndEdit()がなくなるだけ?
については、
それだけではありません。
主な違いは、
List<T>
└─ 変更通知をしない
↓
BindingSourceが仲介して通知・管理
BindingList<T>
└─ Add/Removeなどの変更通知を持つ
というところです。
ただ、現在完成しているIDListをわざわざ BindingList<T> に移行する必要性は、私はもうあまり感じません。
むしろ今回のIDListについては、
List<idItem> + BindingSource
をそのまま採用して、
idItems.Sort(...);
bs.ResetBindings(false);
を「データを直接変更した場合の表示更新」として明示的に使う方が、潔さんの現在の設計には合っています。
以前の私の「BindingListにした方がよい」という勧奨は、一般論としては正しいが、今回の完成したIDListにまで適用する必要はなかった、というのが適切な訂正です。

