カテゴリー別アーカイブ: .NET Framework

NativeアプリとManaged DLLの橋渡しをするMixed mode assembly

SQLiteのアカウントデータベースを使って、ユーザー名、パスワードで認証の可否、アカウント情報を取得するようなコンソールアプリを作った。

C++からパイプ経由で呼び出されるのだが、EF使ってたりデータベースのパスワード保護をやってるせいか、接続の初期化に時間がかかって非常に遅かった。

パイプ繋ぎっぱなしというのも考えたけど、使う人が嫌がったので、
ラッパーDLLをリンクしてC++アプリの起動時にだけ接続処理がされるようにしようと思った。
最終的には以下のような構成になった。

accounts.db (SQLite3データベース System.Data.SQLiteの機能で暗号化)
AuthLib.dll (Managed DLL、System.Data.SQLite.dllを使ってデータベースを操作する)
AuthLibHost.exe (Native C++、メインの実行ファイル)
AuthLibHost.exe.config (接続文字列などの情報が書かれた設定ファイル)
AuthLibNative.dll (Mixed mode assembly、AuthLibHostとAuthLibの橋渡し役)
System.Data.SQLite.dll (Mixed mode assembly)

データベースに関するビジネスロジックはAuthLib.dllにすべて実装されているが、
これに対応するラッパー関数をAuthLibNativeにたくさん書くのが面倒だったので、
すべてEvalという一つの関数を経由するようにした。

Evalの引数はコマンドラインインターフェースと類似したargc, argsといったものにして、
加えて出力を格納するための、linesという文字列の配列とその要素数の参照を渡せるようにする。
成功、失敗をEvalの戻り値にする。

ラッパーが不要なC++/CLIを選ぶか、ラッパー書くのが面倒だけど
LINQ to Entitiesとか使えるC#を選ぶかというところですな。

UI Automation Clients

UI Automationについて参考になりそうなページを50個ほどブックマークしたものの、多すぎるので入口として役立ちそうなページを抜粋しておく。
本来はUIテストやアクセシビリティを目的としているようだが、今回は他人が作ったソフトを自動制御するために使う。
プロバイダとクライアントの説明があるが、今回はクライアントのことが分かればよいはず。
Windows Forms、WPF、サードパーティのUIコンポーネントでもたいていはオートメーションプロバイダとして扱えるようだ。
確証はないがUISpy.exeを使ってAutomationIdなどが確認できれば対応していると思っていいのではないかと思っている。

UI Automation のススメ

UI Automation Fundamentals

UI Automation Client Programmer's Guide (Windows)

Use the AutomationID Property

Bugslayer - こちらGUIコントロール、トム少佐応答せよ

UIツリーを辿り、プロパティを確認するにはUISpyやInspect Objectというのがある。
UI Spy(UISpy.exe)が含まれているWindows SDK Version

現状はC#でやるのが無難そうだ。
TclでもGarudaを使うと、インプロセスで.NET Frameworkのクラスを利用できるのだが、まだ未完成、ドキュメントがないに等しいので難しい。クラスメソッドやイテレーションを使えるのかすら分からない。
COMは茨の道だと思われるのでできれば避けたい。

ちまちまUI Treeを歩き回るコードを書く代わりに、もっと楽にしてくれるライブラリというのもあるみたいだ。
White
White Tutorial
StackOverflow White - File Open Dialog Box
White - Working with window

UISpyみたいなインターフェースと、UI Automationプロバイダとして必要な機能を持っているかチェックするためのソフト。
Visual UI Automation Verify
UIVerifyが内部で使っているライブラリがこれ。
UI Automation COM-to-.NET Adapter
.NET版のAPIがあるのに何でラッパーを作る必要があるのかは気になるが、まだ調べてない。

Jet/ACEでExcelファイルをCSVに変換する

ある分析装置がxlsx形式でレポート出力するのでExcelのCOMインターフェース経由でCSVに落としてから処理していました。
この方法だとMicrosoft Excelが必須になってしまいます。
あとExcel本体経由なので、セキュリティ設定によっては手動でロック解除しないと開けないとかいう場合もあります。
そこでライセンス料をケチるべく、Excelなしでxls/xlsx -> CSV変換プログラムを書く方法を調べました。

// filename: Excel2CSV.cs
using System;
using System.Data;
using System.Data.OleDb;
using System.IO;
 
namespace Excel2CSV
{
    public class Excel2CSV
    {
        const int SUCCESS = 0;
        const int MISSING_FILE_ERROR = 1;
        const int FILE_EXTENSION_ERROR = 2;
        public static int Main(string[] args)
        {
            string path = args[0];
            var finfo = new FileInfo(path);
            if (!finfo.Exists)
            {
                return MISSING_FILE_ERROR;
            }
            string connectionString;
            switch (finfo.Extension.ToLower())
            {
                case ".xls":
                    connectionString = String.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Extended Properties=\"Excel 8.0;HDR=NO;IMEX=1;TypeGuessRows=0;\"", path);
                    break;
                case ".xlsx":
                    connectionString = String.Format("Provider=Microsoft.ACE.OLEDB.12.0;Data Source={0};Extended Properties=\"Excel 12.0 Xml;HDR=NO;IMEX=1;\"", path);
                    break;
                default:
                    return FILE_EXTENSION_ERROR;
            }
 
            OleDbConnection oleConn = new OleDbConnection(connectionString);
            OleDbCommand oleCmd = new OleDbCommand();
            OleDbDataReader oleReader;
 
            oleConn.Open();
            oleCmd.Connection = oleConn;
 
            DataTable tables = oleConn.GetSchema("Tables");
            foreach (DataRow row in tables.Rows)
            {
                string sheetName = row["TABLE_NAME"].ToString();
                string sheetText = "";
                try
                {
                    sheetText += String.Format("[{0}]", sheetName) + Environment.NewLine;
                    oleCmd.CommandText = "SELECT * FROM [" + sheetName + "]";
                    oleReader = oleCmd.ExecuteReader();
                    while (oleReader.Read())
                    {
                        int fieldCount = oleReader.FieldCount;
                        string[] line = new string[fieldCount];
                        for (int i = 0; i < fieldCount; i++)
                        {
                            string val = oleReader[i].ToString();
                            double dval;
                            if (double.TryParse(val, out dval))
                            {
                                line[i] = dval.ToString(); // 12,345.67みたいな数字を12345.67に変換。
                            }
                            else
                            {
                                line[i] = val;
                            }
                        }
                        sheetText += String.Join(",", line) + Environment.NewLine;
                    }
                    oleReader.Close();
                    Console.WriteLine(sheetText);
                }
                catch
                {
                }
            }
            oleConn.Close();
            oleCmd.Dispose();
            oleConn.Dispose();
            return SUCCESS;
        }
    }
}

コンパイルは、

csc Excel2CSV.cs

ファイル名を引数に与えると、標準出力にCSVを出力します。エラーは終了コードで通知します。

Excel2CSV.exe sample.xlsx > sample.xlsx.csv
echo %ERRORLEVEL%
0

必須コンポーネント

Microsoft Access Database Engine 2010 Redistributable

.NET Framework 2.0 以上?

.NET Framework 4.0向けに コンパイルしたもの Excel2CSV

ただ、COM経由で取得したCSVとは、数値の値が違うことがあります。
原因はDAOを使った場合は、表示書式を適用した後の値しか取得できないためのようです。
たとえば、12345.6789という数値を持つセルに、"小数以下桁数2桁、桁区切り"という書式が設定されている場合、12,345.68が抽出されます。
考え方によってはこの方がいいというケースも、だめなケースもあるかも知れません。
とりあえず、桁区切り(,)の書式はCSVにとって邪魔なので、上記のプログラムでは数値とみなせる文字列は一旦数値に変換しています。

ちなみにxdoc2txtの場合は、桁区切りなし、四捨五入済みの数値を吐きます。
というか、いろいろ実験してたらxdoc2txtの出力ってゴミが入ってたりしてプログラムから後処理するのがめんどくさげ。
そもそも商用ライセンスは1000本単位じゃないと買えないので却下。

あと、xlsxをxlsに変換してExcel2CSVにかけると、可視シートだけが出力されるという微妙な違いもありました。
普通は隠しシートの内容を見たいということはないと思いますが。

C#のプロパティに初期化の方法があればいいのにと思った

C#のプロパティは宣言と一緒に初期値指定することはできない。フィールドと組み合わせるか、コンストラクタで初期化するとかしないといけない。

class Person
{
    string _Name = "名無し"; // フィールドでやるか
    public string Name
    {
        get
        {
            return _Name;
        }
        set
        {
            _Name = value;
        }
    }
    public Person()
    {
        Name = "名無し"; // コンストラクタでやるか
    }
}

これまでは値をセットしたときにイベントを起こすとか、設定ファイルと連動させるときとかに使ったことがあったくらい。

もしかしたら次期バージョンではできるのかなと思って、「C# プロパティ 初期 .net framework 4.0」とかで調べてみるけど、無さそうですね。

そのかわりちょっと興味をひく記事を見つけました。

[C#]自動プロパティの必要性
[C#]自動プロパティの必要性(その2)

出水さんという方のコメント「アセンブリ公開するのがわかっていれば、もともとフィールドでは置かないですしね」

なるほど、先日からCOMを使っているので意味分かります。インターフェースにフィールドは置けないんですよね。メンバ変数みたいなのをCOMに公開したければパブリックプロパティを使う必要があります。

インターフェースに初期化データを置くというのも変な気がするので、その辺で初期化は導入されないのかもしれないですね。詳しいことは知りませんが。

LINQ to Entities + SQLiteでCAST(expression AS type)

前回のエントリにて、アルファベットと数字の混在する部屋番号というのを扱ったけれど、結局、文字部分を削除しても問題ないということになったので、それはまあ、良くなった。実はいろいろ調べてAlphanumeric sortというのがあると知ったのだが、まあ、いろいろ役にたつことだろう。

さて、それならわざわざ文字列にしなくてもよいだろうということで、INTEGERに変更した。それでずいぶん無駄なロジックやキャストなんかが減って、すっきりした。

しかし、そこに新たな要求が来た。

  1. 部屋番号の範囲を指定して、複数の居住者を検索する。
  2. 部屋番号を文字列とみなして前方一致検索する。

1. の場合、INTEGERなので問題ない。

2. の場合、WHERE句で文字列にキャストしないとLIKEが使えない。クライアントサイドでの絞込みは馬鹿らしい。

正直こんなもん、SQLiteなら文字列も数値も同じだから、LIKEが使えていいんじゃないのかと思うけど、LINQ to Entitiesではそうもいかない。ObjectQuery<T>のWhereは条件に文字列とプレースホルダが使えるから、その線で探してみた。CASTを使えばよいらしい。Googleで調べるのに2時間もかかったよ。はー疲れた。

ObjectQuery<User> userQuery = entities.User.Include("Room")
  .Where("CAST(it.Room.room_no AS System.String) LIKE @keyRoom",
  new ObjectParameter("keyRoom", keyRoom + "%"));

CAST (expr AS type) というのがSQLiteにもあって、それは結構すぐ見つかったけど、typeのところがC#の型になるとは思い浮かばず、TEXTか?VARCHAR(4)とかか?と意味不明なエラーに苦しんでいました。System.Data.SQLiteのせいじゃないかと思って、dotConnect for SQLiteを自腹で買おうか真剣に悩んで見つけたサンプルをCASTでgrepしたら見つかりました。

Download dotConnect for SQLite

WCFでエンドポイントを追加したらエラーになって起動できなかった

スクリーンキャストを見ながらWCFのデモを打ち込んでいたところ、2番目のデモでいきなり躓いた。

“System.InvalidOperationException: 構成で指定された X.509 証明書 ID を読み込めません。”

エンドポイントのIDタブには、mexのだけDns=localhostになっていたので、ためしに他のにも入力してみたらうF5で動くようになった。

 

じっさいのところ、Dnsはたぶん関係なくて、このタブで設定されるCertificateReferenceの部分が、App.configにはデフォルトで追加されないので、このタブを修正したことで追加されたらしい。

結局Dnsのlocalhostは元通り空白に戻したが、ちゃんと動いている。まあ、不要な値は追加しないということで、バグではないんだろうなあ、偶然でなきゃ絶対気づけなかったと思うけど。

Hatena タグ: ,,