先に結論
ClosedXMLは図形(Shape)に対応しておらず保存時に全部消えるから気をつけな!(コメントも消えるっぽい)
EEPlusは図形に対応してるみたいだけど商用ライセンスだから気をつけな!
話せば長くなるのだが...
IT土方あるあるネタの一つに「エクセル勤務表」というものがある。
その名の通り「Excelで勤怠管理をすること」を指すのだが、
単純に勤怠管理システムが存在せず(IT企業なのに!)Excelを使っているだけだったり、いわゆるSESで入っている他社メンバーの勤務実績を受け渡しするのにExcelファイルが便利だったり、理由は色々あるが、未だに根強く残っているのである。
あるところにC# / .NET Frameworkで作られている業務システムありけり。
ある日ユーザーから「メインメニューから子画面を開いた後、メインメニューがずっと残りっぱなしで移動もできないのは邪魔すぎる何を考えとるんか誰じゃこんなシステムを作ったやつは(以下略)」とお叱りを受けた。
※修正後、しばらくして今度は「何で子画面を開いたらメインメニューが消えるんだ何を考えとるんか誰じゃこんなシステムを作ったやつは(以下略)」とお叱りを受けたので元に戻した。何なんだ一体。
調べてみると子画面をモーダルウィンドウとして表示しており、ほんならモードレスに変えましょうと相成った。
この記事の続き。手こずっているシステムのバグ対応でまた躓いた。
Entity Frameworkなんて使ったことねぇよ。わかんねー
事の発端
とある画面を表示する際、初期表示データを取得するためのDBアクセス時にEntity Frameworkが例外エラーを吐くことがある。
A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
at NameSpace.DataAccess.CustomerMasterAccess.GetItem(String customerCode) in C:\*****.cs:line 74
at NameSpace.DataAccess.MasterAccess.GetCustomerName(String customerCode) in C:\*****.cs:line 125
at NameSpace.Forms.DataInput.SetCustomerName(String customerCode, String customerCode2, String customerCode3) in C:\*****.cs:line 475
at NameSpace.Forms.DataInput.DisplayItems() in C:\*****.cs:line 90
at NameSpace.Forms.DataInput.Initial() in C:\*****.cs:line 71
at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__140_0(Object state)
"A second operation started on this context before a previous operation completed"というエラーメッセージでググってみると、「DbContext はスレッドセーフではないので、スレッド間で使いまわしてはいけない」という文言が見つかる。
つまりこのエラーは複数スレッドから同じDbContextにアクセスしてしまったため発生しているようだ(エラーメッセージに書いている通りやね)
やたらawaitしてるコードに出会う
他人が作った業務システムの手直ししているとこんなコードに出会った。
string contents1 = await DoSomething1Async();
string contents2 = await DoSomething2Async();
string contents3 = await DoSomething3Async();
DoSomething*Async()を見るとasync
修飾子がついてて、それをawait
で受け止めてる形だ。
なんとなく非同期処理だなぁとは分かるが...恥ずかしながら「重い処理を実行するときにUIスレッドをブロックしないために使うアレね」ぐらいの認識。
これまでは「どうせ読み込み終わるまで次に進めないんだからUIなんてフリーズさせとけ!」の脳筋バカ思考だったので、ちょうどいい機会ということで勉強することに。
とりあえずasync/awaitだけでいい
↑にすべてが書いてある。
非同期にしたい関数の呼び出し元にawait
を付けて、呼び出し先の関数の戻り値をasync Task<T>
にする。基本これだけ。
Task.Run
とかWait
とかResult
要らんし使うな、ということらしい。
あとイベントハンドラのみ戻り値をasync void
に。その時はtry/catch
で例外をキャッチしよう。
まずこれを覚えた。
IEnumerableとListが別物であることは見りゃ分かるんだけど、実際にC#でLINQを使ってみたらIEnumerableとListの違いがいまいちよく分からず混乱したので復習してみた。
背景
C#でIEnumerableやListを使うとき、
- IEnumerable
- DapperなどのORMでデータベースから取得したデータをLINQで加工する際に使用する。遅延評価が有効なので、抽出やソートなどの加工を効率よく処理できる。
- List
- Dapperにクエリを投げる際のパラメータの作成や、要素のAdd/Remove、インデクサを使ってのアクセスなど、LINQを使わず手動でデータを加工するのに使用する。またIEnumerableを直接加工したいとき.ToList()メソッドでListに変換してから加工する。.ToList()で変換する時に実際の評価が走る。
このような認識、使い分けをしている。
IEnumerableでは遅延評価をめいっぱい活用してLINQでデータをガシガシ加工できるが、その代わりにListのようにAdd/Removeといった変更はできないし、hoge[1]といった形でインデクサによるアクセスもできない。ここがIEnumerableとListの違いで、それぞれ一長一短だと思っている。ここまでが知っていたこと。
IEnumerable、ICollection、IList
大抵はIEnumerableとIListインターフェイスがベースのものしか使わないが、実はICollectionというものもある。
この3つのインターフェイスの違いは以下のとおりである。
【追記】この現象はC#のDictionaryでも起きる様子(記事末尾参照)
なんだかんだDictionaryって便利だよね
ExcelのVBAを使う際、(C#でいうところの)Listに相当するものとしてDictionaryをよく使う。
古いプログラムでは配列を使っていることもあるが、項目をAddしたりFor Eachで回したりCountを取ったりをできるだけListに近い感覚で使うことができるDictionaryを使いたい。
Collectionでもいいのだが、機能的にもDictionaryのほうが優れていることもあって積極的な理由がない限りはCollectionは使わないことが多い。
この辺は好みなのでどちらでも構わないのだが、実はDictionaryには大きな落とし穴があり今回見事にはまってしまったのでご紹介しておく。
C#で作成したアプリケーションで起きた話。
ZIPファイルを展開して中のファイルのバイナリ読み込むアプリケーションを作成した。
サンプルコードはこんな感じ。
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
using (Stream entryStream = entry.Open()) {
//StreamをMemoryStreamに変換してデータを取り出す
}
}
}
ZIPファイルをZipArchiveクラスとして開くとZipArchive.EntriesプロパティでZIPファイル内の各ファイルにアクセスできる。
あとは各ファイルをOpenすればStream形式で取り出せるが、このStreamはSeekできない(データの途中から読みだせない)ため非常に扱いづらい。
MemoryStreamに変換すれば色々と便利に扱えるため、CopyToしてからbyte[]に切り出すことにした。
以前の記事の続き。実際に顧客の端末に導入しに行った時に起きた悲劇について。
端的に言えば「高DPI時に画面レイアウトが崩れて撃沈」した。
こちらで同様の事例が紹介されている。
画像は上記リンク先より引用。文字がはみ出してレイアウトが崩れている。
WindowsのDPI設定を変更することでフォントサイズが大きくなり、コントロールのサイズからはみ出してしまったことが原因だろう。
画面レイアウト作成時には高DPI環境も想定なければならなかったのだが完全に失念していた。
現在VB6.0からのシステムリプレース案件で.NET Framework (C#)を用いてアプリケーションを開発をしているが、顧客の業務端末スペックが厳しいため本番導入後に動作のもたつきが発生しそう、という懸念材料がある。
具体的には
- 現行環境
- OS: WindowsXP
- クライアント: VB6.0
- データベース: SQL Server 2000
- 二台の端末で運用(親機にはDBとクライアント、子機にはクライアントのみ)
という化石のような環境。まだ生きていたのか(案外残ってる)。
ひとまずWindows7に対応させたい
この現行WinXP端末が既に壊れそうということもあり、ひとまず現場にある(得体の知れない)Windows7端末に移し替えて本番稼働→現在調達中のWindos10端末が届けばそちらに再度移行、という話になった。
ただ、その(得体の知れない)Windows7端末は見るからに古く、まずSSDもメモリも碌に積んでいないだろうことは想像に容易い。
当方の開発環境もさほど良いものではないが、SSDに換装してディスクI/Oの遅延を減らすことで快適に動いている。が、はたして現場の端末でまともに動くのかどうか...。