5. C#関連の糞雑魚メモ


Home -> 雑用 -> 雑用メモ -> [5. C#関連の糞雑魚メモ]

2015/03/18 作成
2015/03/19 更新
2015/04/02 更新
2015/04/09 更新
2015/04/10 更新
2015/04/12 更新
2015/05/06 更新
2015/05/08 更新
2015/05/12 更新
2015/08/12 更新
2015/08/25 更新
2015/09/28 更新
2015/10/26 更新
2015/11/03 更新
2016/02/07 更新
2016/02/17 更新
2016/03/18 更新
一切推敲していない糞文章故、大変読み難い代物となっております。

概要

C#関連の雑多なメモを集約。定期的に更新というか追記していくつもり。
糞記事を量産するなと言われてもそんなの知らん。これは備忘録、覚書だから中身はユルユルのスカスカ。
Windows8.1・Visual Studio Community 2013を使用。

Community版VS公開時にMSの戦略にまんまと乗っかる形でC#を独学で使い始めてみたらこれが結構いける。
今までPHP+JSでできないことは諦めていた。今はWindowsのFormアプリケーションもPHPだけで作れる便利な時代。
故にPHPだけで数年間騙し騙し過ごしてきたがやっぱり不便なもんは不便なんでCommunity版VS公開はいい切っ掛けとなった。
.NET依存でWindows本位ではあるけどもPHPを無理矢理仮想鯖で動かすというアレなやつよりは王道に近いかなーと思われる。

リファレンスも充実してるし困ったらググれば大体解決する、それがC#。
そんな中でも特に躓いたり、謎の感動を覚えたり、無駄に苦労した末に時間の無駄と帰してしまったことをダラダラ記す。

一覧

1. WebBrowserコントロールのIEバージョンが古い?

WebBrowserコントロール内でGoogleマップを開いたところ旧バージョンで開かれ、「使っているIEのバージョンが古い」みたいな感じの
警告文みたいなのが表示された。で、調べてみたらこういうことらしい。

という訳でレジストリを弄らなければいけない模様。糞面倒。
HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION」にexe名で
DWORD値のキーを作る。上記参考ページだとパスが「HKEY_CURRENT_USER\SOFTWARE\...」(大文字)になってますが同意です。
でもって値がIEのバージョンに対応するらしい。ググってみるとみんな「8000」とか「9999」とか入れてるけど
じゃあIE11は指定できんのか、ということで調べたらMS曰く「11000」でOKみたい。

一応書いておくと、アプリケーションAppNameのデバッグ時には「WindowsFormsApplication_AppName.exe」ではなくて
WindowsFormsApplication_AppName.vshost.exe」が実行されるので、そっちのキーも作らないといけない。
これでWebBrowserコントロールで正しくGoogleマップが表示できるようになった。
どうでもいいけどexe名をレジストリに登録ってどうよ。何たる糞っぷり。
こんなもんプログラム内から設定できるようにしろや、と。Microsoftどうにかしろ。

(2015/03/18)

1-1. WebBrowserコントロールでGPUレンダリング?

さっきの参考ページの続きに書いてあったからやってみる。今度もさっきの要領でexe名のキーを作る。
場所は「HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_GPU_RENDERING」。
フルで書いたけど「FEATURE_BROWSER_EMULATION」の部分が変わっただけだから場所はほぼ同じ。
値はDWORD値で有効/無効がそれぞれ1/0に対応している。2進数だけどMaxが1だから16進でも同じ。詳細は公式に書いてある。

偉そうに書いておきながら有効になった実感なし。GoogleマップってGPUレンダリングするんじゃないの?ワカラン。
今のところ特に支障もないので放置することにした。

(2015/03/18)

2. Json.NETでJsonを読み込むときのテンプレートクラスを楽して作りたい!

C#用の某TwitterライブラリをNugetからインストールしたら一緒に入ってたJson.NET。
そこで存在を知ってから愛用しているのだが読み込むjsonに合ったクラスを作っておかないといけないのは少々面倒。
手動でチマチマやってたけどやっぱ自動でやりたい。と言う訳で探したら一瞬で出てきた。jsonを突っ込むだけでクラスが出てくる。神。

因みに、C#というか.NETでjsonを扱うためのライブラリには他にDynamicJsonとかいうのもあるみたい。
JsonReaderWriterFactoryのラッパということはこっちの方が早かったりするのかな?時間があったら使ってみたい。
そういえばJsonReaderWriterFactoryって1回も使ったことない。
jsonをXmlDictionaryReaderやらXmlDictionaryWriterやらに持ってくらしいけどようわからん。なんでわざわざxmlにすんねん。

(2015/03/18)

3. RichTextBoxがクッソ重くなる問題

ログの表示といえば普通ただのtextBoxを使うものだが、エラーがあったら赤字で表示したいとか、
長ーい処理が終了したら緑で表示したいとか、そういう要求を満たそうとするとRichTextBoxを使うことになる。
でもこれが中々の曲者で、中のテキストが溜まるほどに1回のAppendText()に費やす時間が次第に長くなっていくのである。
数千行か数万行か分からないが、それくらいになると1行AppendText()するだけで数秒~数十秒かかったりする。
(一応書いておくがミリ秒ではなく秒である。)
さすがにこれはおかしいと思って調べてみたら似たような現象が他でも再現可能なようで以下の記事を発見。少々古いが参考になる。

つまり、richTextBox.AppendText()するとrichTextBox.Rtfがその都度書き換えられ、そちらの処理に時間がかかっているということらしい。
確かに色文字を使いまくっていると色テーブルの整理だけでも古いRtfを走査する訳だから時間がかかるのも頷ける。
ならば使用する色が限定されているなら自分で色テーブルを最初から用意しておいてRtfの構築も自分でやってしまおうというのが高速化の考え方。

そこでこれをやってみた。Wordが生成する.rtfとは違ってrichTextBox内部のRtfはフォーマットが単純なので人力でもrtfを生成できる。
class Form1の内部クラスとしてclass RichTextBoxWriterなんてのを用意してrtf生成に関する情報を保持するようにした。
色テーブルは最初から作っておいて色の指定はテーブルのインデックスで行うようにした。
で、RichTextBoxWriter.Append(string s, int colornum)みたいな感じで追加するテキストと色番号を指定してテキストを追加する。
完全なrtfが欲しい時にはRichTextBoxWriter.GetRtfString()みたいな関数で得られるようにしてみた。
それで、richTextBox.Rtfを更新したいときにはそれをそのまま代入すればいいかなーと。今思えば安直だった。

AppendText()だと自動的に最下部までスクロールしてくれるが、rtfを直に更新した場合はそれを自分でやらなければいけない。
しかしそれがどうも上手くいかない。
実装した手順は、richTextBox.SelectionStart = richTextBox.Text.Length;でキャレットを最後尾まで動かし、
richTextBox.Focus();richTextBox.ScrollToCaret();でキャレット位置までスクロールするというもの。
これをRtf更新の度に行うとその都度一瞬ではあるがスクロール前のログが見えてしまう。
スクロールは一瞬で終わるけど、画面がものすごくチラチラするのでこの方法では使い物にならない。

色々めんどくさくなって結局普通のtextBoxに落ち着いた。また暇があったら弄ってみたい。

(2015/03/19)

4. FiddlerCoreでHTTPSもキャプチャしたい!

FiddlerはHTTPSの通信もちゃんと捕まえてくれる。じゃあFiddlerCoreはどうかというと、初期状態ではキャプチャしてくれないっぽい。
なら設定項目があるはずだと調べてみたらなんか出てきた。

つまりはスタートアップ時にDecryptSSLのフラグを立てておけばいいそうだ。なるほどなるほど。早速やってみる。
すると何やらメッセージボックスで警告が出てきた。下のページの「Security Concerns」のところのダイアログと同じものが出てきた。

つまりは、「DO_NOT_TRUST_FiddlerRoot」っていうオレオレ証明書を追加しろということらしい。
「信用するな」って書いてある証明書を信用しろという中々おもしろい要求である。何だか心配になってくるが迷っても仕方がない。
別にPCがぶっ壊れる訳でもあるまいし、と思いダイアログメッセージに言われるがままにオレオレ証明書を追加する。
やったぜこれでHTTPSも覗き放題だ!と思ったらHTTPSを全然捕まえてくれない。何故だろう?いろいろ調べたけどさっぱりわからん。
そんな中ついに有力な記事を発見。

これによると、「FiddlerCoreのオレオレ証明書は捨ててFiddler本体からmakecert.exeをパクってそれ使え」ということらしい。
それにしても↑のリンク先の記事、めんどくさい書き方してるよなあ。FiddlerCoreに含まれてるcertmaker.dllを使った方法を長々と
書いておいて最後の方に「実はもっと簡単な方法があります」ってオイオイ。ここまで読んだのは一体何だったんだと。
そんなこんなで上の記事は「MakeCert provides sticky Certificates and the same functionality as Fiddler」の部分だけ読めばOK。
記事通りにやればめでたくC#からHTTPS覗き放題となる。

(2015/03/19)

5. 本のページ綴じ部分が歪んだデジカメ画像を補正したい!

→「5.5 本のページ綴じ部分が歪んだデジカメ画像を補正したい

(2015/04/02)

6. 機械学習を用いた褪色画像・動画の復元

→「5.6 機械学習を用いた褪色画像・動画の復元

(2015/04/09)

7. タスクバーのアイコン背景にプログレスバーを表示させたい!

Windows7あたりから追加された機能の中には割と便利な機能も有ったりする。
その一つがタスクバーのアイコン裏にプログレスバーが表示されるアレである。
Formを開いていなくても進捗が確認できるので是非とも実装したい。という訳でググったら出てきた。

なるほど。「Windows API Code Pack for Microsoft. NET Framework」なるものを使えばいいらしい。
しかし上のページにあったリンクはどちらも切れていた。公式がリンク切れってどうよ。
で、またまたググったらNuGetでお手軽導入できるっぽい。

どちらがいいのかよく分からなかったので適当に下の方をインストールしてみた。で、以下のように書く。
using Microsoft.WindowsAPICodePack;してもいいけど。

int currentValue = 55;
int maximumValue = 100;
Microsoft.WindowsAPICodePack.Taskbar.TaskbarManager.Instance.SetProgressValue(currentValue, maximumValue);

注意事項が一つ。Formのコンストラクタ内では使えない。System.InvalidOperationExceptionが発生する。
タスクバーの更新には有効なアクティブウィンドウが1つ必要である。
テスト用コードはいつもコンストラクタに突っ込む癖だったので引っ掛かってしまった。
WindowsAPICodePackには他にも便利機能が盛沢山なのでぜひ使ってみたい。

(2015/04/10)

8. FiddlerCoreでWebSocketもキャプチャしたい!

FiddlerはそのままだとWebSocketの中身をキャプチャしてくれない。
ググったらどうもRules→Customize Rules→ScriptEditorでルールを編集すればちゃんとキャプチャしてくれるっぽい。
でもちゃんと書いたのに表示されない。なんでや。ということで少々手間を掛けて調べた。
実はこの件の為に「実践Fiddler」なる書籍を借りてきたのだが、その96頁~「4.7 HTML5 WebSocket」には以下のような記述が。

ウェブセッションリストや要求/応答インスペクタには WebSocket メッセージは表示されません。

Fiddler は WebSocket メッセージをパースする能力を持っており、将来のバージョンの Fiddlerでは、WebSocket トラフィックを表示、変更できる新しいタイプのインスペクタが導入されます。

今の段階では、クライアントとサーバーからの WebSocket メッセージは Log タブに表示されます。

なるほど。セッションリストじゃなくてLogタブに表示されるということだった。

ちなみに、ただログを出力したいだけなら数行書くだけでも大丈夫。
ただし、それだとそのうちログが一杯になってFiddler全体を巻き込んでハングアップしてしまう。
だkら色々書いてセッションリストに表示されるようにしよう、というのが上の参考記事。

で、FiddlerCoreの場合どうすればいいのか調べたけど全然出てこない。 唯一出てきたのがこれ。

なるほど。さっきFiddlerの方で書いたルールと同じことをC#で書けば良さそう。
てなわけでサクッと書いたらちゃんとキャプチャできた。ちゃんとパースもしてある。めでたしめでたし。

(2015/04/12)

9. CAPTCHAを機械学習の力で突破したい

→「5.9 [C#]CAPTCHAを機械学習の力で突破したい

(2015/05/06)

10. WebBrowserコントロールでUserScriptを実行したい

WebBrowserコントロールのDOMをC#側から弄りたい場合、どうやってアクセスするのがいいか。
思いつく方法としては恐らく以下の4つくらい。

FiddlerCoreを通すというのは直感的な方法ではあるが大袈裟すぎるのでコードを書きたくない。
そこで最初は「[4]その他の方法でどうにかしてJavaScriptを突っ込んでDOMを弄る」をやってみた。

そんなこと言ってもどうすりゃいいのさとなる訳だが実はググったら面白いものが出てきた。

ブックマークレットと同じ形式にしてUrlプロパティに突っ込めばいいんだと。
そんな馬鹿な、と思うかもしれないがちゃんと動く。

string s = "alert(location.href);";//なんかJavaScriptのコードを適当に書いておく
webBrowser1.Url = new Uri("javascript:" + Uri.EscapeDataString(s));

斜め上な発想に驚くばかりである。一昔前に少し流行った「アハ体験」的なものの片鱗を感じた。

でも何かおかしい。DOMを弄った瞬間に他の要素が消えてページが真っ白になってしまう。どういうこっちゃ。
Fiddlerを無効にしても変わらなかった。もうどうしたらいいのやら。
てな訳で調べてみたけど同じ症状は全くヒットしなかった。仕方ないのでいろいろ試してたらできた。

以下の2つはダメな書き方。

string s = "document.title='a';";
webBrowser1.Url = new Uri("javascript:" + Uri.EscapeDataString(s));
//DOMに代入操作とかやるコードを生で書くと駄目。
string s = @"document.write(""<script src=\""http://example.com/test.js\"" type=\""text/javascript\""></script>"");";
webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();");
//document.write()も駄目。生で書こうがクロージャー使おうが駄目なもんはダメ。

でもって以下が正しい書き方。

string s = "document.title='a';";
webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();");
//クロージャーを使って書く。document.write()以外はこれで動く。
string s = @"
var script = document.createElement( 'script' );
script.type = 'text/javascript';
script.src = ""http://example.com/test.js"";
var firstScript = document.getElementsByTagName( 'script' )[ 0 ];
firstScript.parentNode.insertBefore( script, firstScript );";//ここまで全部Javascript
webBrowser1.Url = new Uri("javascript:(function(){" + Uri.EscapeDataString(s) + "})();");
//要素を追加するならdocument.write()じゃなくてinsertBeforeなどを使う。

別に短いコードならURI内に全部書いちゃえばいいけど長いコードの場合はそれをやっちゃ駄目。
IEやWebBrowserコンポーネントではURLの最大長は2083文字なのでそれを超えた分はちょん切られてしまう。
自分で調べたらURLとしては2085文字まで入ったけど2文字違うだけだから影響は皆無、よって2083文字と考えておいた方がいい。

また、これも自分調べなので出典がある訳ではないが、実行できるスクリプトの長さは更に短いようなので注意が必要。
300字を超えたあたりから実行してくれなくなる。

(2015/05/08)

11. WebBrowserコントロールでCOMException

WebBrowserコントロールを使って簡易ブラウザ的なものを作って遊んでいたらSystem.Runtime.InteropServices.COMExceptionが発生。
ユーザから見て同じ条件でも発生したりしなかったりする。「例外の詳細をクリップボードに追加する」でコピペしたのが以下。

System.Runtime.InteropServices.COMException はユーザー コードによってハンドルされませんでした。
  HResult=-2147024726
  Message=要求されたリソースは使用中です。 (HRESULT からの例外:0x800700AA)
  Source=System.Windows.Forms
  ErrorCode=-2147024726
  StackTrace:
       場所 System.Windows.Forms.UnsafeNativeMethods.IWebBrowser2.Navigate2(Object& URL, Object& flags, Object& targetFrameName, Object& postData, Object& headers)
       場所 System.Windows.Forms.WebBrowser.PerformNavigate2(Object& URL, Object& flags, Object& targetFrameName, Object& postData, Object& headers)
       場所 System.Windows.Forms.WebBrowser.set_Url(Uri value)
       場所 WindowsFormsApplication_appname.Form1.backgroundWorker1_RunWorkerCompleted(Object sender, RunWorkerCompletedEventArgs e) 場所 c:\Users\(略)\WindowsFormsApplication_appname\Form1.cs:行 331
       場所 System.ComponentModel.BackgroundWorker.OnRunWorkerCompleted(RunWorkerCompletedEventArgs e)
       場所 System.ComponentModel.BackgroundWorker.AsyncOperationCompleted(Object arg)
  InnerException: 

イベントハンドラForm1.backgroundWorker1_RunWorkerCompleted()の中でwebBrowser1.Urlnew Uriを代入する部分で例外が発生する。
原因が分からんのでググった。

結局のところメモリ不足かな?という感じなので適当な間を見つけてGC.Collect();するしかなさそう。
これだけ書き足して暫く走らせてるけど今のところ問題は起きていないからこれで解決可能なのかも?

(2015/05/12)

12. CsvHelperのヘッダまわりの挙動

NuGetから適当にインスコすれば何となく使えちゃう便利なCSV入出力ライブラリのCsvHelper。
そのCsvHelperを適当に使っていたらヘッダが付いたり付かなかったりして挙動がイマイチつかめない。
付けるか付けないか指定すればちゃんとその通りになるけど指定しなかったらどうなるのか…?

コードを書き始めた最初のうちはその辺りが適当だったのでヘッダは付かないものだと思い込んで処理していたら不都合が生じた。
ヘッダが付いているのに付いていないつもりでCSVを読み込むとエラーを吐いて止まってしまう
逆にヘッダが付いていないのに付いているつもりでCSVを読み込むと1行目のレコードはヘッダとして処理されてデータとして取り込まれない。
どちらにしろ不便なので挙動をちゃんと調べてみた。

まずCSV出力時にヘッダが付くかどうか確認。標本として用いるCSVを生成するためのクラスは以下のような感じにしてみる。
時刻とそれに対応する数値を順次追記していくという想定である。

using CsvHelper.Configuration;

public class CsvObject
{
    public DateTime Time { get; set; }
    public long Count { get; set; }
    public CsvObject()
    {
        this.Time = new DateTime();
        this.Count = 0;
    }
}
public sealed class CustomClassMap : CsvClassMap<CsvObject>
{
    public override void CreateMap()
    {
        Map(m => m.Time).Index(0); // ヘッダが無いときもレコードが読み込まれるようにひとまずインデックスを指定する
        Map(m => m.Count).Index(1);
    }
}

出力の方法はいろいろあるが例えば以下のような感じである。CsvWriter.WriteRecord<T>(RecordObject)でレコードを1つだけ書き込む。
これはどんどん追記したいときなんかに使える。

using CsvHelper;
using System.IO;

using (FileStream fs = new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) // FileMode.Createだと…?
using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
using (CsvWriter writer = new CsvWriter(sw))
{
    writer.Configuration.RegisterClassMap<CustomClassMap>();
    //writer.Configuration.HasHeaderRecord = false; // これを書くか書かないか…?
    CsvObject LastRecordObject = new CsvObject();
    LastRecordObject.Time = DateTimeNow; // 適当な日時を代入
    LastRecordObject.Count = CountNow; // 適当な数値を代入
    writer.WriteRecord<CsvObject>(LastRecordObject);
}

こんなのもある。レコードのリストを一気に書き込む時に使える。

using (StreamWriter sw = new StreamWriter(filename, System.IO.File.Exists(filename)))
using (CsvWriter writer = new CsvWriter(sw))
{
    writer.Configuration.RegisterClassMap<CustomClassMap>();
    //writer.Configuration.HasHeaderRecord = false; // これを書くか書かないか…?
    // ここまでの間に List<CsvObject> list を用意しておく
    writer.WriteRecords(list);
}

実はヘッダをどうするかは設定できる。CsvWriterCsvReaderごとにtrue/falseで指定する。

CsvWriter.Configuration.HasHeaderRecord = false; // ヘッダがないCSV

最初のうちはこれを知らずに使っていたのでヘッダが出力されたりされなかったりで大変だった。

一方で読込はどんな感じかというとだいたい以下のようにして書けばおk。
単一レコードの読み込みについては今は考えないことにする(というか使ったことないし使う予定もない)。

using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
using (CsvReader reader = new CsvReader(sr))
{
    //reader.Configuration.HasHeaderRecord = false; // これを書くか書かないか…?
    reader.Configuration.RegisterClassMap<CustomClassMap>();
    List<CsvObject> records = reader.GetRecords<CsvObject>().ToList(); // CSVに含まれる全てのレコードを読み込む
    Dictionary<DateTime, long> dic = records.ToDictionary(x => x.Time, x => x.Count);
}

CSV入出力の際にヘッダの存在を前提とするかどうか調べて表にしてみた。
「0」: ヘッダが存在しないことを前提として処理を行う
「1」: ヘッダが存在することを前提として処理を行う
※ … レコードを既存ファイルに追記しようとするとヘッダも追記されてしまうためCSV追記時このようなコードを書いてはいけない
※※ … 既存ファイルに追記しようとすると元のCSVが消えて上書きされてしまうためCSV追記時このようなコードを書いてはいけない
★ … CSVファイル内の1行目は無視されるのでヘッダがない場合はレコードが1つ無駄になる
★★ … ヘッダがない場合、ヘッダを無理矢理レコードとして読み込もうとするので型変換でエラーが発生する

入出力 FileMode HasHeaderRecord まとめ
未指定 true false
出力 WriteRecord Append 0 0 0 ヘッダは出力されない
Create 0※※ 0※※ 0※※
FileStreamを用いない 0 0 0
WriteRecords Append 1※ 1※ 0 基本的にはヘッダが出力される
出力したくなかったらHasHeaderRecordを指定する
Create 1※※ 1※※ 0※※
FileStreamを用いない 1※ 1※ 0
入力 GetRecords Open 1★ 1★ 0★★ 基本的にはヘッダが存在する前提で読み込む
ヘッダがないならHasHeaderRecordを指定する
FileStreamを用いない 1★ 1★ 0★★

パーミッションのこともあるので面倒でもFileStreamから書いた方が安全。
ヘッダをくっ付けたいならCSV作成時と追記時で条件分けしなければならない。
面倒事を避けたいならヘッダなんて最初から出力しないようにした方がいい。

(2015/08/12)

13. HTMLをパースしたい

HTMLをパースしたい時に何を使うべきか。そんな内容の記事はネットの海に腐るほど転がっている。
じゃあ何でこんな項目を設けたのかということだが、コード丸写ししたら動いてくれなかったからである。
まずHtml Agility Packを試した。NuGetから楽ちんインスコできたけど雑魚コードをコピったらエラーが出ちゃう。

HtmlDocument html = new HtmlDocument();

using(WebClient wc = new WebClient())
using(Stream s = wc.OpenRead(url)) {
    html.Load(s);
}

仕方がないので断念。でも後から調べたら原因が分かった。
HtmlDocumentはてっきりSystem.Windows.Forms.HtmlDocumentだと思ってコード書いたけど違うらしい。
mshtml.HTMLDocumentかと思ったらこれも違って正しくはHtmlAgilityPack.HtmlDocumentらしい。
いくつも同じ名前付けやがって、フザケンナよ!

何はともあれ次に試したのがSGMLReader。こっちもNuGetで楽々インスコ。
コードもさっきと違って迷うところがなく快適。こんな感じのやつを作っておくといろいろ捗る。

static XDocument ParseHtml(string url)
{
    XDocument xml;
    using (SgmlReader sgml = new SgmlReader { Href = url })
    {
        sgml.IgnoreDtd = true; // 「これが無いと例外で死ぬ」…らしい
        xml = XDocument.Load(sgml);
    }
    return xml;
}

で、こんな感じで抽出ができちゃったりして面白いね、という話でした。

XDocument xml = ParseHtml(url);
XNamespace ns = xml.Root.Name.Namespace;

XElement parentDiv = xml.Descendants(ns + "div")
    .Where(ul => ul.Attribute("class") != null && ul.Attribute("class").Value == "thumbnail");
XElement childDiv = parentDiv.Element(ns + "div");
var images = childDiv.Descendants(ns + "img");
foreach (XElement image in images)
{
    string ThumbnailUrl = image.Attribute("src").Value;
    Debug.WriteLine(ThumbnailUrl);
}

(2015/08/25)

14. 特殊な木構造のデータ管理と図への描画

→「5.14 [C#]特殊な木構造のデータ管理と図への描画

(2015/09/28)

15. DataGridViewとContextMenuStripとBindingSourceの話

こんな記事は探せばいくらでもあるが、今まで冗長な書き方をしていたので。
例えば以下のようなクラスを用意してリストを表示したいとする。

[Serializable]
public partial class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }

    public Person()
    {
        this.Id = 0;
        this.Name = "";
        this.Score = 0;
    }
}

そんな時、今までは以下のような感じで書いていた。とりあえず動くのでいつもこんな感じで書いてきた。
事前にCellContextMenuStripNeededイベントハンドラを作ってDataGridViewと関連付けておく。

public partial class Form1 : Form
{
    public BindingList<Person> BindingListMembers = new BindingList<Person>();
    public int iRowIndex = 0; // DataGridViewの右クリックメニュー用インデックス
    
    public Form1()
    {
        InitializeComponent();
        
        this.dataGridView1.DataSource = this.BindingListMembers;
        this.dataGridView1.Refresh();
        this.dataGridView1.Columns["Id"].HeaderText = "番号";
        this.dataGridView1.Columns["Id"].Width = 80;
        this.dataGridView1.Columns["Name"].HeaderText = "名前";
        this.dataGridView1.Columns["Name"].Width = 200;
        this.dataGridView1.Columns["Score"].HeaderText = "得点";
        this.dataGridView1.Columns["Score"].Width = 80;
    }
    private void dataGridView1_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
    {
        if (e.RowIndex >= 0 && e.ColumnIndex >= 0)
        {
            e.ContextMenuStrip = contextMenuStrip1;
            this.iRowIndex = e.RowIndex;
        }
    }
    // 右クリックメニューで「この人の情報を表示」が選択されたとき
    private void この人の情報を表示ToolStripMenuItem_Click(object sender, EventArgs e)
    {
        Person SelectedPerson = this.BindingListMembers[this.iRowIndex];
        // ここまで来たら情報表示なり何なり好きにする
    }
    // 右クリックメニューで「この人を削除」が選択されたとき
    private void この人を削除ToolStripMenuItem_Click(object sender, EventArgs e)
    {
        Person SelectedPerson = this.BindingListMembers[this.iRowIndex];
        if(MessageBox.Show("[" + SelectedPerson.Name + "]の情報を削除しますか?", "情報の削除", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes)
        {
            this.BindingListMembers.RemoveAt(this.iRowIndex); // 削除時はインデックスがそのまま使える
            dataGridView1.Refresh();
        }
    }
}

ところが、このままだとDataGridViewが増えるたびにint iRowIndexのような邪魔者が増えてしまう。
コードも冗長なので今までDataGridViewへのContextMenuStrip関連付けは敬遠してきた。
しかし、少し調べたら以下のような感じで割ときれいにまとめることもできるようで、こっちだったら分かりやすそう。

public partial class Form1 : Form
{
    public BindingList<Person> BindingListMembers = new BindingList<Person>();
    
    public Form1()
    {
        InitializeComponent();
        
        this.dataGridView1.DataSource = this.BindingListMembers;
        this.dataGridView1.Refresh();
        this.dataGridView1.Columns["Id"].HeaderText = "番号";
        this.dataGridView1.Columns["Id"].Width = 80;
        this.dataGridView1.Columns["Name"].HeaderText = "名前";
        this.dataGridView1.Columns["Name"].Width = 200;
        this.dataGridView1.Columns["Score"].HeaderText = "得点";
        this.dataGridView1.Columns["Score"].Width = 80;
    }
    private void dataGridView1_CellContextMenuStripNeeded(object sender, DataGridViewCellContextMenuStripNeededEventArgs e)
    {
        if (e.RowIndex < 0 || e.ColumnIndex < 0)
        {
            return;
        }
        e.ContextMenuStrip = this.contextMenuStrip1;
        DataGridView d= (DataGridView)sender;
        d.CurrentCell = d[e.ColumnIndex, e.RowIndex];
    }
    private void この人の情報を表示ToolStripMenuItem_Click(object sender, EventArgs e)
    {
        Person SelectedPerson = (Person)this.dataGridView1.CurrentRow.DataBoundItem;
        // ここまで来たら情報表示なり何なり好きにする
    }
    private void この人を削除ToolStripMenuItem_Click(object sender, EventArgs e)
    {
        Person SelectedPerson = (Person)this.dataGridView1.CurrentRow.DataBoundItem;
        if(MessageBox.Show("[" + SelectedPerson.Name + "]の情報を削除しますか?", "情報の削除", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == DialogResult.Yes)
        {
            this.dataGridView1.Rows.Remove(this.dataGridView1.CurrentRow); // BindingList を使っているならこんな感じでおk
        }
    }
}

たまにListBindingListとでどちらを使っているのかあやふやになって毎回適当に誤魔化してきたが、
BindingListならDataGridView側を弄るだけでBindingList側も勝手に変わってくれる。
ところがどっこい、まだまだ改善の余地があって、BindingSourceを使うともっといい感じにやってくれる。
ちょっと下ごしらえが面倒だがこれさえ乗り越えれば列の幅・名前設定やthis.dataGridView1.Refresh();が不要になる。

  1. データを関連付けたいBindingList<Person>をあらかじめコード上で作成しておく
  2. [プロジェクト] メニューの [新しいデータ ソースの追加] を選択する
  3. [オブジェクト] を選択し、リストに表示したいクラス(例では [Person] )を選択する
  4. dataGridView1DataSourceにさっき追加したデータソースを指定すると、BindingSourceが自動的に作成される(この場合だとpersonBindingSourceといった名前で作成される)。ここまでやると、デザイナ上のdataGridView1にヘッダが表示される。
  5. デザイナ上でdataGridView1を右クリックし、[列の編集...] からヘッダの名前や幅を調整する。
  6. フォームのコンストラクタかどこか適当な所(例ではForm1のコンストラクタ内)でpersonBindingSource.DataSourceBindingList<Person>を突っ込む。
public partial class Form1 : Form
{
    public BindingList<Person> BindingListMembers = new BindingList<Person>();
    
    public Form1()
    {
        InitializeComponent();
        
        this.personBindingSource.DataSource= this.BindingListMembers; // これだけ!
    }
    
    // 後は同じ
    // ただしリストをごっそり入れ替えたいときは注意が必要
    private void newList(BindingList<Person> List)
    {
        // これが今までの書き方
        this.dataGridView1.DataSource = List;
        this.dataGridView1.Refresh();
        
        // こんな風に書いちゃうと駄目、DataGridViewは更新されない
        // DataSourceにインスタンスを突っ込む必要がある
        this.BindingListMembers = List;
        
        // こういう風に書けばおk DataGridViewは自動的に更新される
        this.personBindingSource.DataSource = List;
        
        // あるいはこういう風に書いた方が後々でコード側からBindingListを弄れて便利かも
        this.BindingListMembers = List; // これだけだと駄目
        this.personBindingSource.DataSource = this.BindingListMembers;
    }
}

結局のところ、長々とコードを書いているのに気づいたらもっと簡単な方法がないか調べましょう、ということであった。

(2015/10/26)

追記 (2015/10/28): 双方向データバインドと要素内の変更通知

BindingList<T>BindingSource.DataSourceに突っ込んでおけば、リストの要素の追加・削除なんかは自動でシンクロしてくれる。
じゃあリスト内のある要素となっているオブジェクトの中身をちょっと弄ったらどうなるかというと、この時はすぐにUIには反映されない。

色々書いてあるが、こんな感じのことが書いてある。

Windowsフォームアプリケーションに貼ったDataGridView内にPersonオブジェクトのリストを表示したい。
コード上でのリストの変更がDataGridViewに反映されたりその逆ができるようにしたい。
BindingSourceを使えばDataGridViewと簡単に双方向連携できるという風に解釈してるだけど、
データバインドってこういう風に書けばいいのかな?
//擬似コード
BindingSource.DataSource = IBindingList<Person>
それともこんな感じ?
BindingSource.DataSource = IList<Person>
これって何が違うの? どっちの方法でもリストを変更したらDataGridViewに反映されるの?
もしBindingListじゃないと駄目ってなると依存関係の都合上よろしくないんだけどこの辺どうにかなる?
Microsoftは「BindingList使う代わりにBindingSource使っとけばおk」って言ってるけど…
BindingList使っとけばコードからの変更はコントロールにすぐ反映されるよ。
BindingListはリストが変更されたら特定のイベントを発生させるけど普通のリストはそうなってない。

普通のリストを使ったら、コントロールを通した変更やBindingSourceを通した変更はすぐに反映されるように見えるけど
コード上のリストは直接は変更されないよ。
BindingSource使うならBindingList使わなくても大丈夫とか言ってる人もいてゴチャゴチャするけど、
たぶん君の言いたいことはBindingSourceを通してリストを変更した時の話だよね。
IList<Person>にバインドすると一方通行のバインディングしかできないよ。
つまりこの場合、コードからのリスト自体やリストの要素への変更はUIには反映されないんだよね。
(管理人注: DataGridView上の編集はリストに反映されるのかな?)
BindingListBindingSourceを使っとけば双方向連携できるけど、
PersonクラスはINotifyPropertyChangedをサポートしてないと駄目だよ。
でないと、リストの要素の追加・削除は同期してくれるけどリストの要素自体に変更が加えられたときにそれがUIに反映されないよ。

System.Windows.Formsに依存させたくなかったら、代わりにObservableCollection<Person>ってのを使うこともできるよ。
これなら必要に応じた変更通知をしてくれるし、同期もできるよ。
.NET 4 を使っていない限り、ObservableCollection使うと依存関係が更に厄介になっちゃうよ。
こいつはWPFアセンブリ(WindowsBase)の中で定義されてるからね。.NET 4 ではSystem.dllに移ったけど。

調べてみたらObservableCollectionを使った場合でもINotifyPropertyChangedを実装する必要があるらしい。
だったらもうBindingListでいいじゃんって思うのでそのままこっちで書き進める。
総括するとPersonクラスにINotifyPropertyChangedを実装すればおkということになるのだが、
こいつが真面目にやろうとするとクソ面倒な代物で、楽に書くために色々なものが出ているようだ。

その一つとしてやたら名前が挙がっているのが「Notify Property Weaver」という Visual Studio の拡張機能。
ところが、こいつは既に削除されてしまっている。作者ページから辿ると「PropertyChanged.Fody」というパッケージが見つかるので、これを使う。

導入はNuGetが全部やってくれるから特に難しいことはないので細かいことは書かない。
コードも以下のように書き加えてやるだけで残りは全部いい感じにやってくれる。INotifyPropertyChangedのことを考える必要は全くない。

[ImplementPropertyChanged]
[Serializable]
public partial class Person
{
    // 中身は今までと全く同じ、[ImplementPropertyChanged] を書き加えただけ
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }

    public Person()
    {
        this.Id = 0;
        this.Name = "";
        this.Score = 0;
    }
}

16. SGMLReaderでSystem.ArgumentException

以前にも扱ったSGMLReader。中々融通の利くこいつは今まで散々使い込んできたが、特にエラーが出たりということはなかった。
ところが最近、どう頑張ってもエラーとなるページが出現。どことは言わないが、cgi出力のかなり普通のページ。
それも「http://example.com/test/201510/6」は正常に読み込めて「http://example.com/test/201510/7」だとエラーが出る、といった感じ。
HTMLの構造も文字コードもヘッダも殆ど同じなのに、なぜか特定のページだけ「System.ArgumentException」が投げられる。
追加情報で「空白以外の文字をコンテンツに追加できません」などといわれる。

static XDocument ParseHtml(string url)
{
    XDocument xml;
    using (SgmlReader sgml = new SgmlReader { Href = url })
    {
        sgml.IgnoreDtd = true;
        xml = XDocument.Load(sgml); // ここで System.ArgumentException が投げられる
    }
    return xml;
}

このエラーは日本語で検索しても殆ど情報がなく、英語版の追加情報「Non white space characters cannot be added to content.」で検索するといろいろ出てくる。
ざっくりといえばXMLの構造が不正だと投げられるエラーということらしい。

そもそもXMLとして構造が不完全なHTMLをXMLとして読み込めるようにするのがSGMLReaderな訳で、こんなエラーを出されては困る。
検索をすれども同じ状況に関する情報は皆無で、コードを思いのまま書き換えたりしてみたが改善する兆しは無し。
そんなこんなで試行錯誤すること約3時間、ようやく正しく読み込めるようになった。

static XDocument ParseHtml(string url, Encoding enc)
{
    XDocument xml;
    using (WebClient wc = new WebClient())
    {
        wc.Encoding = enc; // 文字コードをユーザに指定させる
        string s = wc.DownloadString(url); // 文字列として先に全て読み込む
        using (StringReader sr = new StringReader(s)) // 文字列をストリームとしてSgmlReaderに渡すためにStringReaderを使う
        using (SgmlReader sgml = new SgmlReader())
        {
            sgml.IgnoreDtd = true;
            sgml.InputStream = sr; // ここでさっきのStringReaderを指定する
            xml = XDocument.Load(sgml);
        }
    }
    return xml;
}

結局原因の詳細は不明なままだが動くようになったので、今のところはこれで良しということで。
文字コードが事前にわかる場合はこっちのオーバーロード使えばおk。

(2015/11/03)

17. TaskAsyncに頼りすぎないで時々別スレッドに分けましょう

BackgroundWorkerでゴリゴリやっていた時に比べてTaskAsyncが便利なので乱用して以下のようなコードを書いたとする。

// ログ出力用
private void WriteLog(string s)
{
    this.WriteDate();
    this.textBoxLog.AppendText(s + "\r\n");
}
private void writeErrorLog(string s)
{
    this.WriteDate();
    this.textBoxLog.AppendText("エラー: " + s + "\r\n");
}
private void WriteDate()
{
    this.textBoxLog.Focus();
    this.textBoxLog.AppendText("[" + DateTime.Now.ToString() + "] ");
}

// 画像ダウンロード用
private async void button1_Click(object sender, EventArgs e)
{
    this.button1.Enabled = false;
    this.button1.Text = "処理中...";
    
    WebClient wc = new WebClient();
    string UrlBase = "http://nsmr-hryk.koroshitai.jp/img/";
    string Url;
    string FileName;
    int minNum = 1;
    int maxNum = 10000;
    for (int i = minNum; i <= maxNum; i++)
    {
        FileName = i.ToString("00000") + ".jpg";
        Url = UrlBase + FileName;
        if (!File.Exists(FileName))
        {
            await Task.Delay(1000);
            this.WriteLog("取得開始: " + FileName);
            try
            {
                await wc.DownloadFileTaskAsync(Url, FileName);
                this.WriteLog("取得完了");
            }
            catch (Exception ex)
            {
                this.writeErrorLog("取得失敗: " + ex.Message);
            }
        }
    }
    this.WriteLog("処理完了");
    
    this.button1.Text = "処理開始";
    this.button1.Enabled = true;
}

button1を押したら特定の場所から1万枚の画像をダウンロードする、といった感じで書いたつもり。
動作の内容はフォーム内のテキストボックスにログを表示することでユーザに示す。
手元に画像がまだダウンロードされていない段階では約1秒おきに画像を"00001.jpg"から順番に保存していく、という内容。
wc.DownloadFileTaskAsyncを使っているのでダウンロード中もUIは固まらない、というのを期待して書いたとしよう。

で、手元に画像が既に保存されている段階ではどうなるのかというと、1万枚の画像があるかどうか1枚ずつ確かめているので
それだけでも結構な時間が掛かる。Task.Delayを間に挟まないのでその間はずっとUIが固まった状態になる。
しかも2~3秒程度のフリーズならまだしも、この動作内容だと60秒以上掛かってしまうのでVisualStudioからお叱りを受けてしまう。
(先にフォルダ内のファイル一覧を取得すればいいじゃんとかそういうのか今回は関係ないので気にしないでくださいね。)

じゃあどうにかしないといけないよね、って事になる訳だが、本質的にどうしても動作に時間が掛かる場合はやっぱり別スレッドに分けるしかない。
BackgroundWorkerでもいいけど、コードがあちこちに分散するのは面倒って考えるとやっぱりこうするしかない。

// ログ出力用に以下を追加
// 別スレッドからUIスレッドにアクセスするためにデリゲートを用意しておく
// 引数は2つ以上でもおk、objectっぽくないけどintやboolもおk
public delegate void WriteLogDelegate(string Message, bool IsSuccess);
public void WriteLogMethod(string Message, bool IsSuccess)
{
    if (IsSuccess)
    {
        this.WriteLog(Message);
    }
    else
    {
        this.writeErrorLog(Message);
    }
}

// 画像ダウンロード用
private async void button1_Click(object sender, EventArgs e)
{
    this.button1.Enabled = false;
    this.button1.Text = "処理中...";
    
    await Task.Run(() =>
    {
        WebClient wc = new WebClient();
        string UrlBase = "http://nsmr-hryk.koroshitai.jp/img/";
        string Url;
        string FileName;
        int minNum = 1;
        int maxNum = 10000;
        for (int i = minNum; i <= maxNum; i++)
        {
            FileName = i.ToString("00000") + ".jpg";
            Url = UrlBase + FileName;
            if (!File.Exists(FileName))
            {
                Thread.Sleep(1000); // Task.Delayから書き換え
                Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得開始: " + FileName, true });
                try
                {
                    wc.DownloadFile(Url, FileName); // ここではawaitとか使ってない
                    Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得完了", true });
                }
                catch (Exception ex)
                {
                    Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "取得失敗: " + ex.Message, false });
                }
            }
        }
        Invoke(new WriteLogDelegate(WriteLogMethod), new object[] { "処理完了", true });
    });
    
    this.button1.Text = "処理開始";
    this.button1.Enabled = true;
}

ちなみに、Task.Runの中にawaitが沢山あって書き換えが面倒だという場合には以下のようにすることもできるようだ。
動作の効率という面では微妙と言わざるを得ないが、所詮は.NETなのでこんなもん。

// ここにasyncを追加しておけば
await Task.Run(async () =>
{
    // Task.Runの中でもawaitが使える(でもどちみちUIスレッドから切り離されているので見た目は一緒)
    await Task.Delay(1000);
    await wc.DownloadFileTaskAsync(Url, FileName);
});

(2016/02/07)

18. 写真の透かしを除去したい

→「5.18 [C#]写真の透かしを除去したい

(2016/02/17)

19. XMLシリアライズはXmlSerializerのゴリ押しではイマイチ

設定を保存したいときとかに多用してきたXmlSerializer。例えば以下のような感じ。
昔からあるせいか、そこらじゅうで紹介されている。分かりやすいし。

private BindingList<Person> Persons; // Personクラスのインスタンスたち。これを保存したい。

// Serialize
private void 設定を保存ToolStripMenuItem_Click(object sender, EventArgs e) // MenuStripから「設定を保存」を選択したのちに
{
    if (this.saveFileDialog1.ShowDialog() != DialogResult.OK) // ファイル名を確定したら
    {
        return;
    }
    Type[] et = new Type[] { typeof(Person) };
    XmlSerializer serializer = new XmlSerializer(typeof(BindingList<Person>), et);
    using (StreamWriter sw = new StreamWriter(this.saveFileDialog1.FileName, false, Encoding.UTF8)) // MemoryStreamとかでもOK
    {
        serializer.Serialize(sw, this.Persons); // ファイルに書き込む。
    }
}

// Deserialize
private void 設定を開くToolStripMenuItem_Click(object sender, EventArgs e) // MenuStripから「設定を開く」を選択したのちに
{
    if (this.openFileDialog1.ShowDialog() != DialogResult.OK) // ファイル名を確定したら
    {
        return;
    }
    Type[] et = new Type[] { typeof(Person) };
    XmlSerializer serializer = new XmlSerializer(typeof(BindingList<Person>), et);
    using (StreamReader sr = new StreamReader(this.openFileDialog1.FileName, Encoding.UTF8))
    {
        var doc = new XmlDocument();
        doc.PreserveWhitespace = true; // こいつを噛ませないと読み込み時に改行が消えちゃうので注意
        doc.Load(sr);
        using (XmlNodeReader reader = new XmlNodeReader(doc.DocumentElement))
        {
            this.Persons = (BindingList<Person>)serializer.Deserialize(reader); // ファイルを読み込む。
            this.personBindingSource.DataSource = this.Persons; // 新しいインスタンスを突っ込むとバインド先UIも更新される
        }
    }
}

StreamWriter周辺は少し書き換えるだけで他のストリームが使える。
あまりに便利なもんだからこれで満足していたが、どうやら設定の保存というときにはDataContractSerializerを使った方がいいらしい。
今まで知らなかったけど便利そう。

System.Runtime.Serializationを参照に追加・usingに追加する必要があるが、コードはそれほど変わらないので使い勝手はよい。

// 保存時
List<Type> et = new List<Type>() { typeof(Person) };
DataContractSerializer serializer = new DataContractSerializer(typeof(BindingList<Person>), et});
XmlWriterSettings xws = new XmlWriterSettings { Indent = true };
using (FileStream fs = new FileStream(this.saveFileDialog1.FileName, FileMode.Create))
using (XmlWriter xw = XmlWriter.Create(fs, xws)) // インデントを付けるためにXmlWriterを噛ませる
{
    serializer.WriteObject(xw, this.Persons);
    xw.Flush();
}
// 読込時
List<Type> et = new List<Type>() { typeof(Person) };
DataContractSerializer serializer = new DataContractSerializer(typeof(BindingList<Person>), et}); // ここまで同じ
XmlReaderSettings xrs = new XmlReaderSettings { IgnoreWhitespace = false };
using (FileStream fs = new FileStream(this.openFileDialog1.FileName, FileMode.Open))
using (XmlReader xr = XmlReader.Create(fs, xrs))
{
    this.Persons = (BindingList<Person>)serializer.ReadObject(xr);
}

読込について、FileModeが適当でも構わないなら以下のようにしてもおk。

// DataContractSerializer のインスタンス作るところまでは同じ
XmlReaderSettings xrs = new XmlReaderSettings { IgnoreWhitespace = false };
using (XmlReader xr = XmlReader.Create(this.openFileDialog1.FileName, xrs))
{
    this.Persons = (BindingList<Person>)serializer.ReadObject(xr);
}

またXmlReaderSettingsが適当でも構わないなら以下のようにしてもおk。

// DataContractSerializer のインスタンス作るところまでは同じ
using (FileStream fs = new FileStream(this.openFileDialog1.FileName, FileMode.Open))
{
    this.Persons = (BindingList<Person>)serializer.ReadObject(fs);
}

何でもかんでもシリアライズできると噂のDataContractSerializer
調子に乗ってCookieContainerとかExpandoObjectとかを突っ込んでみたが、こいつらはダメらしい。
本当はJintからもらったExpandoObjectをImpromptuInterfaceに通して作り変えたものを保存したかった。
これならリストにしてデータバインドも簡単にできるのだが、内部で用いられる型名が実行時に生成されるために
DataContractSerializerのコンストラクタにTypeを渡せず、保存が利かないのでは不便極まりないので
結局、同一のインターフェースを継承する保存用クラスを作成して新たなインスタンスを用意することで回避した。

(2016/03/18)

20. BindingListをバインドしたDataGridViewでソートしたい

DataGridViewって便利。適当にリストを突っ込んでおけば綺麗に表示してくれる。
でもBindingListをデータソースにしてもソートはできない。
一応、バンドするクラスにIComparableインターフェイスを実装したりしてソートの操作を別に行うことはできる。

ところがやっぱりこの方法だとソート用のコンテキストメニューやらイベントやらを別で作らないといけないので面倒。
で、検索したら便利なものがすぐに出てきた。

動物病院とか書いてあって心配になるが、ちゃんとC#の話が書いてある。
ただ、そのままだと#region#endregionで閉じられていないのでエラーが出る。
ちゃんと修正したら便利な感じ。シリアライズしてファイルに保存すると後からソート後の順番を復元できるのも便利。
ソート前の順番はどこにも記録されず消えちゃうのでそこが気にならないならおk。

下のコードは殆どコピペだが#regionの修正以外にもコンストラクタあたりを少し書き換えてある。
インスタンスを新しく作る時点で既にリストがあるならいいが、自分の場合は後からBindingList.Add()することの方が多いので。

#region SortableBindingList 関連一式
//IBindingListのソート機能を実装したジェネリックリストクラス
public class SortableBindingList<T> : BindingList<T>
{
    public static SortableBindingList<T> ToSortableBindingList(List<T> list)
    {
        return new SortableBindingList<T>(list);
    }

    public SortableBindingList(List<T> list) : base(list) { }

    public SortableBindingList() : base() { } // 追加分

    protected override bool SupportsSortingCore
    {
        get
        {
            return true;
        }
    }

    protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
    {
        List<T> items = this.Items as List<T>;

        if (items != null)
        {
            PropertyComparer<T> pc = new PropertyComparer<T>(prop, direction);
            items.Sort(pc);
            _isSorted = true;
        }
        else
            _isSorted = false;

        _direction = direction;
        _SortProperty = prop;
        this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
    }

    private bool _isSorted;
    protected override bool IsSortedCore
    {
        get { return _isSorted; }
    }

    private ListSortDirection _direction;
    protected override ListSortDirection SortDirectionCore
    {
        get
        {
            return _direction;
        }
    }

    private PropertyDescriptor _SortProperty;
    protected override PropertyDescriptor SortPropertyCore
    {
        get
        {
            return _SortProperty;
        }
    }
}

//汎用コンペアラークラス
public class PropertyComparer<T> : IComparer<T>
{
    public PropertyComparer(PropertyDescriptor propertyName, ListSortDirection direction)
    {
        this.name = propertyName;
        sortDirection = (direction == ListSortDirection.Ascending) ? 1 : -1;
    }

    private PropertyDescriptor name;
    private int sortDirection;

    #region IComparer<T> メンバー

    public int Compare(T x, T y)
    {
        IComparable left = name.GetValue(x) as IComparable;
        IComparable right = name.GetValue(y) as IComparable;

        int result;

        if (left != null)
            result = left.CompareTo(right);
        else if (right == null)
            result = 0;
        else
            result = -1;

        return result * sortDirection;
    }
    #endregion
}

//呼び出し用拡張メソッド
public static class BindingListExtensions
{
    public static SortableBindingList<T> ToSortableBindingList<T>(this List<T> list)
    {
        return SortableBindingList<T>.ToSortableBindingList(list);
    }
}
#endregion

というか#regionって最近になって初めて存在を知ったけど便利だなー。
VisualStudioは折りたたみたいところは大体折りたためるようになっているから不要だと思う人もいるかもしれないが、
たまに気が利かなくて折り畳んだコードが勝手に展開されてしまうことがあるので、こういう機能を有効に使いたい。

(2016/03/20)


管理人Twitter: @su_te_ak/◆mmft4k9vgtL6
要望等はTwitterへ

Home -> 雑用 -> 雑用メモ -> [5. C#関連の糞雑魚メモ]

ここ以降は鯖が勝手に付加するやつです